summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/images/web-ide-promo-popover.svg101
-rw-r--r--app/assets/javascripts/admin/background_migrations/components/database_listbox.vue6
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/base.vue5
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue47
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form.vue225
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue34
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue19
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/constants.js35
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/edit.js43
-rw-r--r--app/assets/javascripts/admin/broadcast_messages/index.js5
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_table.vue7
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_form.vue2
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue6
-rw-r--r--app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue1
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/base.vue (renamed from app/assets/javascripts/cycle_analytics/components/base.vue)6
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue (renamed from app/assets/javascripts/cycle_analytics/components/filter_bar.vue)55
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue (renamed from app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue (renamed from app/assets/javascripts/cycle_analytics/components/metric_tile.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue (renamed from app/assets/javascripts/cycle_analytics/components/path_navigation.vue)3
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue (renamed from app/assets/javascripts/cycle_analytics/components/stage_table.vue)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue (renamed from app/assets/javascripts/cycle_analytics/components/total_time.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue (renamed from app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/constants.js (renamed from app/assets/javascripts/cycle_analytics/constants.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/index.js (renamed from app/assets/javascripts/cycle_analytics/index.js)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/actions.js (renamed from app/assets/javascripts/cycle_analytics/store/actions.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/getters.js (renamed from app/assets/javascripts/cycle_analytics/store/getters.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/index.js (renamed from app/assets/javascripts/cycle_analytics/store/index.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js (renamed from app/assets/javascripts/cycle_analytics/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/mutations.js (renamed from app/assets/javascripts/cycle_analytics/store/mutations.js)0
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/store/state.js (renamed from app/assets/javascripts/cycle_analytics/store/state.js)2
-rw-r--r--app/assets/javascripts/analytics/cycle_analytics/utils.js (renamed from app/assets/javascripts/cycle_analytics/utils.js)0
-rw-r--r--app/assets/javascripts/api/analytics_api.js5
-rw-r--r--app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql1
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue12
-rw-r--r--app/assets/javascripts/badges/constants.js8
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue50
-rw-r--r--app/assets/javascripts/behaviors/copy_code.js5
-rw-r--r--app/assets/javascripts/behaviors/index.js1
-rw-r--r--app/assets/javascripts/behaviors/markdown/init_gfm.js13
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js77
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_math.js6
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_observability.js33
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js1
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/openapi/index.js2
-rw-r--r--app/assets/javascripts/blob/viewer/index.js2
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js1
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue10
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue25
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue4
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue30
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue3
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js6
-rw-r--r--app/assets/javascripts/boards/stores/actions.js1
-rw-r--r--app/assets/javascripts/branches/components/sort_dropdown.vue30
-rw-r--r--app/assets/javascripts/branches/init_new_branch_ref_selector.js25
-rw-r--r--app/assets/javascripts/ci/ci_lint/components/ci_lint.vue (renamed from app/assets/javascripts/ci_lint/components/ci_lint.vue)4
-rw-r--r--app/assets/javascripts/ci/ci_lint/index.js (renamed from app/assets/javascripts/ci_lint/index.js)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue (renamed from app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js (renamed from app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue (renamed from app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue (renamed from app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue (renamed from app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue (renamed from app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue)10
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_tree/container.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue (renamed from app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue (renamed from app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue (renamed from app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue (renamed from app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue (renamed from app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue)4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue)2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue (renamed from app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue (renamed from app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js (renamed from app/assets/javascripts/pipeline_editor/constants.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js (renamed from app/assets/javascripts/pipeline_editor/graphql/resolvers.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql (renamed from app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js (renamed from app/assets/javascripts/pipeline_editor/index.js)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue (renamed from app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue (renamed from app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue)0
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue311
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js24
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue (renamed from app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue)4
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/constants.js (renamed from app/assets/javascripts/reports/codequality_report/constants.js)34
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/actions.js (renamed from app/assets/javascripts/reports/codequality_report/store/actions.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/getters.js (renamed from app/assets/javascripts/reports/codequality_report/store/getters.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/index.js (renamed from app/assets/javascripts/reports/codequality_report/store/index.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js (renamed from app/assets/javascripts/reports/codequality_report/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutations.js (renamed from app/assets/javascripts/reports/codequality_report/store/mutations.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/state.js (renamed from app/assets/javascripts/reports/codequality_report/store/state.js)0
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js (renamed from app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js)0
-rw-r--r--app/assets/javascripts/ci/reports/components/grouped_issues_list.vue (renamed from app/assets/javascripts/reports/components/grouped_issues_list.vue)2
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_body.js (renamed from app/assets/javascripts/reports/components/issue_body.js)2
-rw-r--r--app/assets/javascripts/ci/reports/components/issue_status_icon.vue (renamed from app/assets/javascripts/reports/components/issue_status_icon.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/issues_list.vue (renamed from app/assets/javascripts/reports/components/issues_list.vue)4
-rw-r--r--app/assets/javascripts/ci/reports/components/report_item.vue (renamed from app/assets/javascripts/reports/components/report_item.vue)2
-rw-r--r--app/assets/javascripts/ci/reports/components/report_link.vue (renamed from app/assets/javascripts/reports/components/report_link.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue (renamed from app/assets/javascripts/reports/components/report_section.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/components/summary_row.vue (renamed from app/assets/javascripts/reports/components/summary_row.vue)0
-rw-r--r--app/assets/javascripts/ci/reports/constants.js (renamed from app/assets/javascripts/reports/constants.js)0
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue53
-rw-r--r--app/assets/javascripts/ci/runner/admin_runner_show/index.js2
-rw-r--r--app/assets/javascripts/ci/runner/admin_runners/admin_runners_app.vue17
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue6
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue (renamed from app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue)15
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_detail.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_groups.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue55
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue11
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_projects.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_type_tabs.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js4
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_count.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/stat/runner_stats.vue41
-rw-r--r--app/assets/javascripts/ci/runner/constants.js11
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue3
-rw-r--r--app/assets/javascripts/ci/runner/runner_search_utils.js1
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue4
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue1
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue43
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue13
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue10
-rw-r--r--app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue71
-rw-r--r--app/assets/javascripts/ci_variable_list/constants.js18
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql1
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql2
-rw-r--r--app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql1
-rw-r--r--app/assets/javascripts/clusters_list/clusters_util.js8
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_token.vue11
-rw-r--r--app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue103
-rw-r--r--app/assets/javascripts/clusters_list/constants.js6
-rw-r--r--app/assets/javascripts/constants.js3
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue (renamed from app/assets/javascripts/content_editor/components/top_toolbar.vue)4
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue4
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue3
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js4
-rw-r--r--app/assets/javascripts/content_editor/extensions/comment.js49
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js18
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference_label.js2
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js2
-rw-r--r--app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js5
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js3
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js27
-rw-r--r--app/assets/javascripts/crm/components/crm_form.vue (renamed from app/assets/javascripts/crm/components/form.vue)0
-rw-r--r--app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue6
-rw-r--r--app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue6
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue5
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue208
-rw-r--r--app/assets/javascripts/deploy_tokens/deploy_token_translations.js41
-rw-r--r--app/assets/javascripts/deprecated_jquery_dropdown/render.js2
-rw-r--r--app/assets/javascripts/deprecated_notes.js28
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue2
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note.vue12
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue2
-rw-r--r--app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue92
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue2
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_code_quality.vue39
-rw-r--r--app/assets/javascripts/diffs/components/diff_discussion_reply.vue17
-rw-r--r--app/assets/javascripts/diffs/components/diff_expansion_cell.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue9
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue16
-rw-r--r--app/assets/javascripts/diffs/i18n.js3
-rw-r--r--app/assets/javascripts/diffs/index.js3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js35
-rw-r--r--app/assets/javascripts/diffs/utils/merge_request.js18
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar.vue13
-rw-r--r--app/assets/javascripts/editor/components/source_editor_toolbar_button.vue17
-rw-r--r--app/assets/javascripts/editor/constants.js104
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js48
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js3
-rw-r--r--app/assets/javascripts/editor/schema/ci.json213
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue2
-rw-r--r--app/assets/javascripts/environments/environment_details/constants.js47
-rw-r--r--app/assets/javascripts/environments/environment_details/index.vue118
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql48
-rw-r--r--app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js62
-rw-r--r--app/assets/javascripts/environments/mount_show.js30
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue4
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue3
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue14
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/strategy_label.vue29
-rw-r--r--app/assets/javascripts/feature_flags/utils.js16
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_popover.vue9
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js31
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js34
-rw-r--r--app/assets/javascripts/filtered_search/constants.js15
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js3
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js12
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js7
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js51
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js3
-rw-r--r--app/assets/javascripts/flash.js8
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue5
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js19
-rw-r--r--app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue2
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue76
-rw-r--r--app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue160
-rw-r--r--app/assets/javascripts/gitlab_version_check/constants.js22
-rw-r--r--app/assets/javascripts/gitlab_version_check/index.js116
-rw-r--r--app/assets/javascripts/gitlab_version_check/utils.js18
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql1
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json7
-rw-r--r--app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql1
-rw-r--r--app/assets/javascripts/groups/components/app.vue75
-rw-r--r--app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue21
-rw-r--r--app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue21
-rw-r--r--app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue (renamed from app/assets/javascripts/groups/components/empty_state.vue)1
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue12
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue2
-rw-r--r--app/assets/javascripts/groups/components/groups.vue23
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue35
-rw-r--r--app/assets/javascripts/groups/components/transfer_group_form.vue1
-rw-r--r--app/assets/javascripts/header.js21
-rw-r--r--app/assets/javascripts/header_search/components/app.vue3
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue2
-rw-r--r--app/assets/javascripts/header_search/constants.js3
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue5
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/success_message.vue2
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue5
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail.vue5
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue4
-rw-r--r--app/assets/javascripts/ide/components/panes/right.vue17
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue26
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue42
-rw-r--r--app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue103
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue5
-rw-r--r--app/assets/javascripts/ide/constants.js3
-rw-r--r--app/assets/javascripts/ide/index.js5
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js98
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js2
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js11
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js12
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/index.js2
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js14
-rw-r--r--app/assets/javascripts/ide/remote/index.js40
-rw-r--r--app/assets/javascripts/ide/services/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js4
-rw-r--r--app/assets/javascripts/ide/stores/modules/terminal/messages.js4
-rw-r--r--app/assets/javascripts/ide/stores/state.js1
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue44
-rw-r--r--app/assets/javascripts/import_entities/components/import_status.vue2
-rw-r--r--app/assets/javascripts/import_entities/constants.js6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue40
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js10
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql6
-rw-r--r--app/assets/javascripts/import_entities/import_groups/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue18
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue52
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js16
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js86
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/getters.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutation_types.js8
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js30
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/state.js5
-rw-r--r--app/assets/javascripts/import_entities/import_projects/utils.js5
-rw-r--r--app/assets/javascripts/incidents/components/incidents_list.vue2
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js4
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue28
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue149
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form_actions.vue143
-rw-r--r--app/assets/javascripts/integrations/edit/index.js1
-rw-r--r--app/assets/javascripts/invite_members/components/import_project_members_modal.vue23
-rw-r--r--app/assets/javascripts/invite_members/components/invite_group_notification.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue37
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue161
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue107
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue17
-rw-r--r--app/assets/javascripts/invite_members/constants.js11
-rw-r--r--app/assets/javascripts/invite_members/init_import_project_members_modal.js4
-rw-r--r--app/assets/javascripts/invite_members/init_invite_groups_modal.js5
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js1
-rw-r--r--app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js23
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/constants.js23
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js75
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue10
-rw-r--r--app/assets/javascripts/issuable/index.js15
-rw-r--r--app/assets/javascripts/issuable/issuable_bulk_update_actions.js (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js)4
-rw-r--r--app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js)10
-rw-r--r--app/assets/javascripts/issuable/issuable_label_selector.js56
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js8
-rw-r--r--app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue271
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js26
-rw-r--r--app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql36
-rw-r--r--app/assets/javascripts/issues/index.js2
-rw-r--r--app/assets/javascripts/issues/issue.js6
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue53
-rw-r--r--app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue110
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_statistics.vue56
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue360
-rw-r--r--app/assets/javascripts/issues/list/components/new_issue_dropdown.vue4
-rw-r--r--app/assets/javascripts/issues/list/constants.js155
-rw-r--r--app/assets/javascripts/issues/list/index.js25
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql3
-rw-r--r--app/assets/javascripts/issues/list/utils.js64
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js4
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/store/actions.js4
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue8
-rw-r--r--app/assets/javascripts/issues/show/components/description.vue14
-rw-r--r--app/assets/javascripts/issues/show/components/fields/description.vue1
-rw-r--r--app/assets/javascripts/issues/show/components/form.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue15
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/constants.js1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql1
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql6
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue73
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue73
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue59
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue5
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue7
-rw-r--r--app/assets/javascripts/issues/show/components/locked_warning.vue27
-rw-r--r--app/assets/javascripts/issues/show/components/title.vue3
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue55
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue42
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue35
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue1
-rw-r--r--app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue18
-rw-r--r--app/assets/javascripts/jobs/components/job/empty_state.vue31
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql16
-rw-r--r--app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql17
-rw-r--r--app/assets/javascripts/jobs/components/job/job_app.vue25
-rw-r--r--app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue192
-rw-r--r--app/assets/javascripts/jobs/components/job/manual_variables_form.vue163
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue29
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue104
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue21
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue87
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue7
-rw-r--r--app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue6
-rw-r--r--app/assets/javascripts/jobs/constants.js15
-rw-r--r--app/assets/javascripts/jobs/index.js9
-rw-r--r--app/assets/javascripts/labels/labels_select.js2
-rw-r--r--app/assets/javascripts/language_switcher/components/app.vue49
-rw-r--r--app/assets/javascripts/language_switcher/constants.js1
-rw-r--r--app/assets/javascripts/language_switcher/index.js23
-rw-r--r--app/assets/javascripts/lib/dompurify.js2
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js27
-rw-r--r--app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue5
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/create_and_submit_form.js26
-rw-r--r--app/assets/javascripts/lib/utils/dom_utils.js21
-rw-r--r--app/assets/javascripts/lib/utils/http_status.js56
-rw-r--r--app/assets/javascripts/lib/utils/poll.js4
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/listbox/index.js4
-rw-r--r--app/assets/javascripts/listbox/redirect_behavior.js2
-rw-r--r--app/assets/javascripts/main.js17
-rw-r--r--app/assets/javascripts/members/components/avatars/user_avatar.vue8
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue6
-rw-r--r--app/assets/javascripts/members/constants.js8
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue2
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js63
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue11
-rw-r--r--app/assets/javascripts/merge_requests/components/target_project_dropdown.vue87
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/experiment.vue36
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue6
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue94
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue59
-rw-r--r--app/assets/javascripts/monitoring/components/charts/empty_chart.vue4
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue6
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue8
-rw-r--r--app/assets/javascripts/monitoring/components/group_empty_state.vue3
-rw-r--r--app/assets/javascripts/monitoring/components/refresh_button.vue16
-rw-r--r--app/assets/javascripts/monitoring/components/variables_section.vue7
-rw-r--r--app/assets/javascripts/monitoring/csv_export.js2
-rw-r--r--app/assets/javascripts/monitoring/requests/index.js9
-rw-r--r--app/assets/javascripts/monitoring/utils.js1
-rw-r--r--app/assets/javascripts/mr_notes/discussion_counter.js28
-rw-r--r--app/assets/javascripts/mr_notes/index.js36
-rw-r--r--app/assets/javascripts/mr_notes/init.js52
-rw-r--r--app/assets/javascripts/mr_notes/init_count.js13
-rw-r--r--app/assets/javascripts/mr_notes/init_notes.js33
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue71
-rw-r--r--app/assets/javascripts/new_branch_form.js8
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue2
-rw-r--r--app/assets/javascripts/notebook/cells/output/html.vue10
-rw-r--r--app/assets/javascripts/notebook/cells/output/latex.vue2
-rw-r--r--app/assets/javascripts/notebook/cells/output/markdown.vue4
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue6
-rw-r--r--app/assets/javascripts/notes/components/diff_discussion_header.vue3
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue3
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue10
-rw-r--r--app/assets/javascripts/notes/components/note_signed_out_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue6
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue13
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue89
-rw-r--r--app/assets/javascripts/notes/index.js53
-rw-r--r--app/assets/javascripts/notes/stores/actions.js67
-rw-r--r--app/assets/javascripts/notes/stores/getters.js20
-rw-r--r--app/assets/javascripts/notes/stores/modules/index.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js3
-rw-r--r--app/assets/javascripts/observability/components/observability_app.vue71
-rw-r--r--app/assets/javascripts/observability/components/skeleton/dashboards.vue29
-rw-r--r--app/assets/javascripts/observability/components/skeleton/explore.vue27
-rw-r--r--app/assets/javascripts/observability/components/skeleton/index.vue89
-rw-r--r--app/assets/javascripts/observability/components/skeleton/manage.vue25
-rw-r--r--app/assets/javascripts/observability/constants.js16
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue14
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js6
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue13
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue4
-rw-r--r--app/assets/javascripts/packages_and_registries/harbor_registry/utils.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue20
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql1
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue15
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/package_path.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue10
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js1
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index/index.js (renamed from app/assets/javascripts/pages/admin/broadcast_messages/index.js)2
-rw-r--r--app/assets/javascripts/pages/admin/dashboard/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue3
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js50
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js8
-rw-r--r--app/assets/javascripts/pages/help/index/index.js2
-rw-r--r--app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue8
-rw-r--r--app/assets/javascripts/pages/import/gitlab_projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/import/manifest/new/index.js3
-rw-r--r--app/assets/javascripts/pages/import/phabricator/new/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/branches/new/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/ci/lints/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/cycle_analytics/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/environments/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue87
-rw-r--r--app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue32
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js27
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/diffs/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js9
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js45
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js48
-rw-r--r--app/assets/javascripts/pages/projects/ml/candidates/show/index.js27
-rw-r--r--app/assets/javascripts/pages/projects/ml/experiments/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue13
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js82
-rw-r--r--app/assets/javascripts/pages/projects/project.js27
-rw-r--r--app/assets/javascripts/pages/projects/settings/merge_requests/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue206
-rw-r--r--app/assets/javascripts/pages/projects/shared/web_ide_link/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/tags/new/index.js4
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue9
-rw-r--r--app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js5
-rw-r--r--app/assets/javascripts/pages/web_ide/remote_ide/index.js3
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue23
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue4
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue5
-rw-r--r--app/assets/javascripts/performance_bar/constants.js10
-rw-r--r--app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue490
-rw-r--r--app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue4
-rw-r--r--app/assets/javascripts/pipeline_new/index.js50
-rw-r--r--app/assets/javascripts/pipeline_wizard/components/step_nav.vue3
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/graph/job_item.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue1
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue14
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/test_reports/test_reports.vue14
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines_mixin.js2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js65
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_dag.js42
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js36
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_graph.js35
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_jobs.js34
-rw-r--r--app/assets/javascripts/pipelines/pipeline_test_details.js40
-rw-r--r--app/assets/javascripts/popovers/components/popovers.vue5
-rw-r--r--app/assets/javascripts/profile/account/components/update_username.vue3
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue7
-rw-r--r--app/assets/javascripts/projects/commit/components/form_modal.vue12
-rw-r--r--app/assets/javascripts/projects/commit/init_revert_commit_modal.js1
-rw-r--r--app/assets/javascripts/projects/commits/index.js31
-rw-r--r--app/assets/javascripts/projects/compare/components/repo_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/compare/components/revision_card.vue2
-rw-r--r--app/assets/javascripts/projects/default_project_templates.js8
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue23
-rw-r--r--app/assets/javascripts/projects/new/constants.js2
-rw-r--r--app/assets/javascripts/projects/new/index.js2
-rw-r--r--app/assets/javascripts/projects/project_name_rules.js28
-rw-r--r--app/assets/javascripts/projects/project_new.js14
-rw-r--r--app/assets/javascripts/projects/settings/access_dropdown.js2
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js1
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue40
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js9
-rw-r--r--app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql1
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue11
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue57
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql24
-rw-r--r--app/assets/javascripts/projects/settings/utils.js17
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue5
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js2
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue1
-rw-r--r--app/assets/javascripts/releases/components/release_block.vue15
-rw-r--r--app/assets/javascripts/releases/components/release_block_footer.vue34
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/util.js3
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue12
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue8
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue10
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue34
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue2
-rw-r--r--app/assets/javascripts/repository/constants.js1
-rw-r--r--app/assets/javascripts/repository/index.js31
-rw-r--r--app/assets/javascripts/repository/queries/commit.query.graphql7
-rw-r--r--app/assets/javascripts/repository/utils/ref_switcher_utils.js30
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue12
-rw-r--r--app/assets/javascripts/search/sidebar/components/results_filters.vue27
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_navigation.vue47
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter.vue12
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js2
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue71
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue3
-rw-r--r--app/assets/javascripts/search/topbar/constants.js2
-rw-r--r--app/assets/javascripts/search/topbar/index.js20
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue4
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue13
-rw-r--r--app/assets/javascripts/self_monitor/store/actions.js8
-rw-r--r--app/assets/javascripts/sentry/constants.js1
-rw-r--r--app/assets/javascripts/sentry/index.js16
-rw-r--r--app/assets/javascripts/sentry/legacy_index.js34
-rw-r--r--app/assets/javascripts/sentry/legacy_sentry_config.js64
-rw-r--r--app/assets/javascripts/sentry/sentry_browser_wrapper.js27
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js39
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue4
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue (renamed from app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/copy/copyable_field.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue (renamed from app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue)4
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/constants.js25
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/escalation_status.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue9
-rw-r--r--app/assets/javascripts/sidebar/components/incidents/utils.js5
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue)7
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue73
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue)77
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue)0
-rw-r--r--app/assets/javascripts/sidebar/components/move/move_issues_button.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue)8
-rw-r--r--app/assets/javascripts/sidebar/components/participants/sidebar_participants_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewers.vue20
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/severity/constants.js41
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/status/status_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/constants.js1
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue227
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/help_state.vue5
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue40
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue)2
-rw-r--r--app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue (renamed from app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue)0
-rw-r--r--app/assets/javascripts/sidebar/constants.js187
-rw-r--r--app/assets/javascripts/sidebar/mount_milestone_sidebar.js1
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js100
-rw-r--r--app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql17
-rw-r--r--app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql)1
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql (renamed from app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql (renamed from app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql (renamed from app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql (renamed from app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql)0
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js15
-rw-r--r--app/assets/javascripts/sidebar/utils.js (renamed from app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js)5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_description_view.vue2
-rw-r--r--app/assets/javascripts/surveys/merge_request_experience/app.vue5
-rw-r--r--app/assets/javascripts/tags/init_new_tag_ref_selector.js23
-rw-r--r--app/assets/javascripts/terms/components/app.vue12
-rw-r--r--app/assets/javascripts/terraform/components/init_command_modal.vue8
-rw-r--r--app/assets/javascripts/tooltips/components/tooltips.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue21
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/child_content.vue12
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue18
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/state_container.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue64
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue52
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue134
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/app.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue44
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue35
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js31
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js88
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/i18n.js7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue38
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql7
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js8
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue12
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue2
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue112
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/rich_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/code_block_highlighted.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_modal.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js57
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue (renamed from app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue)50
-rw-r--r--app/assets/javascripts/vue_shared/components/group_select/group_select.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/help_popover.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue110
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue11
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field_view.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js (renamed from app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js)0
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue17
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/registry_search.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js21
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue13
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue161
-rw-r--r--app/assets/javascripts/vue_shared/constants.js4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue92
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue4
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_description.vue12
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue10
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue3
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue2
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue3
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue4
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/getters.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/store/utils.js8
-rw-r--r--app/assets/javascripts/webhooks/components/push_events.vue2
-rw-r--r--app/assets/javascripts/webhooks/constants.js4
-rw-r--r--app/assets/javascripts/whats_new/components/feature.vue5
-rw-r--r--app/assets/javascripts/work_items/components/notes/system_note.vue229
-rw-r--r--app/assets/javascripts/work_items/components/work_item_assignees.vue1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue89
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue16
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue241
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail_modal.vue7
-rw-r--r--app/assets/javascripts/work_items/components/work_item_information.vue53
-rw-r--r--app/assets/javascripts/work_items/components/work_item_labels.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/index.js2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue66
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue248
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue123
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue111
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue86
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue244
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue68
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue10
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue109
-rw-r--r--app/assets/javascripts/work_items/components/work_item_type_icon.vue5
-rw-r--r--app/assets/javascripts/work_items/constants.js51
-rw-r--r--app/assets/javascripts/work_items/graphql/discussion.fragment.graphql12
-rw-r--r--app/assets/javascripts/work_items/graphql/milestone.fragment.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_links.query.graphql3
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql29
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql27
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql32
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql53
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_widgets.fragment.graphql28
-rw-r--r--app/assets/javascripts/work_items/index.js12
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue6
-rw-r--r--app/assets/javascripts/work_items/utils.js6
-rw-r--r--app/assets/stylesheets/_page_specific_files.scss4
-rw-r--r--app/assets/stylesheets/components/content_editor.scss12
-rw-r--r--app/assets/stylesheets/components/ref_selector.scss2
-rw-r--r--app/assets/stylesheets/fonts.scss32
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss14
-rw-r--r--app/assets/stylesheets/framework/emojis.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss38
-rw-r--r--app/assets/stylesheets/framework/header.scss57
-rw-r--r--app/assets/stylesheets/framework/kbd.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss644
-rw-r--r--app/assets/stylesheets/framework/typography.scss15
-rw-r--r--app/assets/stylesheets/framework/variables.scss71
-rw-r--r--app/assets/stylesheets/page_bundles/_pipeline_mixins.scss23
-rw-r--r--app/assets/stylesheets/page_bundles/alert_management_details.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss21
-rw-r--r--app/assets/stylesheets/page_bundles/clusters.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/incidents.scss9
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss183
-rw-r--r--app/assets/stylesheets/page_bundles/issuable_list.scss96
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss69
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/oncall_schedules.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss39
-rw-r--r--app/assets/stylesheets/page_bundles/search.scss (renamed from app/assets/stylesheets/pages/search.scss)97
-rw-r--r--app/assets/stylesheets/page_bundles/settings.scss209
-rw-r--r--app/assets/stylesheets/page_bundles/todos.scss81
-rw-r--r--app/assets/stylesheets/page_bundles/tree.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/users.scss (renamed from app/assets/stylesheets/pages/users.scss)2
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss4
-rw-r--r--app/assets/stylesheets/pages/colors.scss8
-rw-r--r--app/assets/stylesheets/pages/commits.scss5
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss912
-rw-r--r--app/assets/stylesheets/pages/login.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss6
-rw-r--r--app/assets/stylesheets/pages/ml_experiment_tracking.scss6
-rw-r--r--app/assets/stylesheets/pages/monitor.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss38
-rw-r--r--app/assets/stylesheets/pages/projects.scss87
-rw-r--r--app/assets/stylesheets/pages/settings.scss228
-rw-r--r--app/assets/stylesheets/startup/startup-dark.scss394
-rw-r--r--app/assets/stylesheets/startup/startup-general.scss141
-rw-r--r--app/assets/stylesheets/startup/startup-signin.scss23
-rw-r--r--app/assets/stylesheets/themes/_dark.scss182
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss143
-rw-r--r--app/assets/stylesheets/utilities.scss12
-rw-r--r--app/channels/graphql_channel.rb4
-rw-r--r--app/components/diffs/stats_component.rb16
-rw-r--r--app/components/pajamas/button_component.rb2
-rw-r--r--app/controllers/abuse_reports_controller.rb2
-rw-r--r--app/controllers/admin/abuse_reports_controller.rb2
-rw-r--r--app/controllers/admin/application_settings/appearances_controller.rb1
-rw-r--r--app/controllers/admin/application_settings_controller.rb16
-rw-r--r--app/controllers/admin/background_jobs_controller.rb6
-rw-r--r--app/controllers/admin/background_migrations_controller.rb98
-rw-r--r--app/controllers/admin/batched_jobs_controller.rb34
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb154
-rw-r--r--app/controllers/admin/ci/variables_controller.rb80
-rw-r--r--app/controllers/admin/groups_controller.rb5
-rw-r--r--app/controllers/admin/plan_limits_controller.rb1
-rw-r--r--app/controllers/admin/projects_controller.rb8
-rw-r--r--app/controllers/admin/spam_logs_controller.rb2
-rw-r--r--app/controllers/admin/system_info_controller.rb10
-rw-r--r--app/controllers/admin/users_controller.rb14
-rw-r--r--app/controllers/application_controller.rb20
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb2
-rw-r--r--app/controllers/concerns/controller_with_cross_project_access_check.rb4
-rw-r--r--app/controllers/concerns/creates_commit.rb2
-rw-r--r--app/controllers/concerns/cycle_analytics_params.rb5
-rw-r--r--app/controllers/concerns/enforces_two_factor_authentication.rb18
-rw-r--r--app/controllers/concerns/impersonation.rb4
-rw-r--r--app/controllers/concerns/import/github_oauth.rb1
-rw-r--r--app/controllers/concerns/integrations/params.rb4
-rw-r--r--app/controllers/concerns/invisible_captcha_on_signup.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb18
-rw-r--r--app/controllers/concerns/issuable_collections.rb4
-rw-r--r--app/controllers/concerns/issues_calendar.rb4
-rw-r--r--app/controllers/concerns/labels_as_hash.rb4
-rw-r--r--app/controllers/concerns/lfs_request.rb22
-rw-r--r--app/controllers/concerns/membership_actions.rb16
-rw-r--r--app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb4
-rw-r--r--app/controllers/concerns/metrics_dashboard.rb4
-rw-r--r--app/controllers/concerns/milestone_actions.rb24
-rw-r--r--app/controllers/concerns/notes_actions.rb38
-rw-r--r--app/controllers/concerns/oauth_applications.rb6
-rw-r--r--app/controllers/concerns/observability/content_security_policy.rb25
-rw-r--r--app/controllers/concerns/page_limiter.rb9
-rw-r--r--app/controllers/concerns/paginated_collection.rb4
-rw-r--r--app/controllers/concerns/preferred_language_switcher.rb2
-rw-r--r--app/controllers/concerns/preview_markdown.rb8
-rw-r--r--app/controllers/concerns/product_analytics_tracking.rb68
-rw-r--r--app/controllers/concerns/record_user_last_activity.rb5
-rw-r--r--app/controllers/concerns/render_service_results.rb24
-rw-r--r--app/controllers/concerns/renders_ldap_servers.rb12
-rw-r--r--app/controllers/concerns/routable_actions.rb10
-rw-r--r--app/controllers/concerns/snippets/blobs_actions.rb17
-rw-r--r--app/controllers/concerns/sorting_preference.rb4
-rw-r--r--app/controllers/concerns/sourcegraph_decorator.rb4
-rw-r--r--app/controllers/concerns/uploads_actions.rb24
-rw-r--r--app/controllers/concerns/verifies_with_email.rb10
-rw-r--r--app/controllers/concerns/vscode_cdn_csp.rb17
-rw-r--r--app/controllers/concerns/web_hooks/hook_actions.rb19
-rw-r--r--app/controllers/dashboard/snippets_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb39
-rw-r--r--app/controllers/explore/snippets_controller.rb2
-rw-r--r--app/controllers/google_api/authorizations_controller.rb11
-rw-r--r--app/controllers/graphql_controller.rb6
-rw-r--r--app/controllers/groups/application_controller.rb22
-rw-r--r--app/controllers/groups/boards_controller.rb10
-rw-r--r--app/controllers/groups/dependency_proxy_for_containers_controller.rb2
-rw-r--r--app/controllers/groups/observability_controller.rb23
-rw-r--r--app/controllers/groups/settings/ci_cd_controller.rb2
-rw-r--r--app/controllers/groups/usage_quotas_controller.rb28
-rw-r--r--app/controllers/groups/variables_controller.rb2
-rw-r--r--app/controllers/groups_controller.rb2
-rw-r--r--app/controllers/ide_controller.rb1
-rw-r--r--app/controllers/import/bitbucket_controller.rb23
-rw-r--r--app/controllers/import/bulk_imports_controller.rb2
-rw-r--r--app/controllers/import/fogbugz_controller.rb4
-rw-r--r--app/controllers/import/gitea_controller.rb26
-rw-r--r--app/controllers/import/github_controller.rb48
-rw-r--r--app/controllers/jira_connect/app_descriptor_controller.rb6
-rw-r--r--app/controllers/jira_connect/application_controller.rb26
-rw-r--r--app/controllers/jira_connect/cors_preflight_checks_controller.rb16
-rw-r--r--app/controllers/jira_connect/events_controller.rb7
-rw-r--r--app/controllers/jira_connect/installations_controller.rb12
-rw-r--r--app/controllers/jira_connect/oauth_application_ids_controller.rb3
-rw-r--r--app/controllers/jira_connect/public_keys_controller.rb4
-rw-r--r--app/controllers/jira_connect/subscriptions_controller.rb10
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb2
-rw-r--r--app/controllers/passwords_controller.rb4
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb16
-rw-r--r--app/controllers/projects/autocomplete_sources_controller.rb2
-rw-r--r--app/controllers/projects/badges_controller.rb30
-rw-r--r--app/controllers/projects/blame_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb3
-rw-r--r--app/controllers/projects/branches_controller.rb2
-rw-r--r--app/controllers/projects/ci/daily_build_group_report_results_controller.rb2
-rw-r--r--app/controllers/projects/clusters_controller.rb1
-rw-r--r--app/controllers/projects/commit_controller.rb2
-rw-r--r--app/controllers/projects/commits_controller.rb14
-rw-r--r--app/controllers/projects/compare_controller.rb4
-rw-r--r--app/controllers/projects/environments_controller.rb6
-rw-r--r--app/controllers/projects/find_file_controller.rb2
-rw-r--r--app/controllers/projects/forks_controller.rb4
-rw-r--r--app/controllers/projects/graphs_controller.rb38
-rw-r--r--app/controllers/projects/incidents_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb15
-rw-r--r--app/controllers/projects/jobs_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/creations_controller.rb13
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests_controller.rb153
-rw-r--r--app/controllers/projects/metrics_dashboard_controller.rb4
-rw-r--r--app/controllers/projects/ml/candidates_controller.rb23
-rw-r--r--app/controllers/projects/ml/experiments_controller.rb1
-rw-r--r--app/controllers/projects/network_controller.rb10
-rw-r--r--app/controllers/projects/performance_monitoring/dashboards_controller.rb4
-rw-r--r--app/controllers/projects/pipelines_controller.rb5
-rw-r--r--app/controllers/projects/protected_branches_controller.rb6
-rw-r--r--app/controllers/projects/raw_controller.rb4
-rw-r--r--app/controllers/projects/refs_controller.rb20
-rw-r--r--app/controllers/projects/registry/repositories_controller.rb4
-rw-r--r--app/controllers/projects/runner_projects_controller.rb2
-rw-r--r--app/controllers/projects/service_desk_controller.rb2
-rw-r--r--app/controllers/projects/service_ping_controller.rb6
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb13
-rw-r--r--app/controllers/projects/settings/integrations_controller.rb4
-rw-r--r--app/controllers/projects/settings/repository_controller.rb8
-rw-r--r--app/controllers/projects/snippets/application_controller.rb2
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects/tree_controller.rb3
-rw-r--r--app/controllers/projects/variables_controller.rb2
-rw-r--r--app/controllers/projects/work_items_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb38
-rw-r--r--app/controllers/registrations/welcome_controller.rb8
-rw-r--r--app/controllers/registrations_controller.rb39
-rw-r--r--app/controllers/repositories/lfs_locks_api_controller.rb8
-rw-r--r--app/controllers/repositories/lfs_storage_controller.rb2
-rw-r--r--app/controllers/search_controller.rb25
-rw-r--r--app/controllers/snippets/application_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb2
-rw-r--r--app/controllers/users_controller.rb3
-rw-r--r--app/controllers/web_ide/remote_ide_controller.rb53
-rw-r--r--app/events/gitlab_subscriptions/renewed_event.rb17
-rw-r--r--app/experiments/concerns/project_commit_count.rb6
-rw-r--r--app/finders/autocomplete/routes_finder.rb6
-rw-r--r--app/finders/ci/freeze_periods_finder.rb16
-rw-r--r--app/finders/ci/jobs_finder.rb11
-rw-r--r--app/finders/ci/pipelines_finder.rb10
-rw-r--r--app/finders/ci/runners_finder.rb17
-rw-r--r--app/finders/clusters/agent_tokens_finder.rb22
-rw-r--r--app/finders/deployments_finder.rb1
-rw-r--r--app/finders/environments/environments_finder.rb10
-rw-r--r--app/finders/freeze_periods_finder.rb14
-rw-r--r--app/finders/git_refs_finder.rb53
-rw-r--r--app/finders/group_descendants_finder.rb2
-rw-r--r--app/finders/group_members_finder.rb4
-rw-r--r--app/finders/members_finder.rb4
-rw-r--r--app/finders/merge_request_target_project_finder.rb2
-rw-r--r--app/finders/notes_finder.rb6
-rw-r--r--app/finders/personal_access_tokens_finder.rb2
-rw-r--r--app/finders/projects_finder.rb17
-rw-r--r--app/finders/releases/group_releases_finder.rb4
-rw-r--r--app/finders/repositories/tree_finder.rb26
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/finders/users_finder.rb6
-rw-r--r--app/graphql/graphql_triggers.rb8
-rw-r--r--app/graphql/mutations/alert_management/alerts/set_assignees.rb2
-rw-r--r--app/graphql/mutations/alert_management/alerts/todo/create.rb2
-rw-r--r--app/graphql/mutations/alert_management/base.rb18
-rw-r--r--app/graphql/mutations/alert_management/create_alert_issue.rb2
-rw-r--r--app/graphql/mutations/alert_management/update_alert_status.rb2
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/create.rb72
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/play.rb32
-rw-r--r--app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb19
-rw-r--r--app/graphql/mutations/ci/runner/update.rb2
-rw-r--r--app/graphql/mutations/clusters/agent_tokens/create.rb6
-rw-r--r--app/graphql/mutations/container_repositories/destroy.rb4
-rw-r--r--app/graphql/mutations/incident_management/timeline_event/update.rb4
-rw-r--r--app/graphql/mutations/issues/link_alerts.rb26
-rw-r--r--app/graphql/mutations/issues/unlink_alert.rb33
-rw-r--r--app/graphql/mutations/notes/create/diff_note.rb8
-rw-r--r--app/graphql/mutations/notes/create/image_diff_note.rb6
-rw-r--r--app/graphql/mutations/notes/create/note.rb6
-rw-r--r--app/graphql/mutations/timelogs/create.rb8
-rw-r--r--app/graphql/mutations/todos/restore_many.rb6
-rw-r--r--app/graphql/mutations/work_items/create.rb2
-rw-r--r--app/graphql/resolvers/base_resolver.rb9
-rw-r--r--app/graphql/resolvers/ci/project_runners_resolver.rb15
-rw-r--r--app/graphql/resolvers/ci/runner_groups_resolver.rb45
-rw-r--r--app/graphql/resolvers/ci/runner_jobs_resolver.rb12
-rw-r--r--app/graphql/resolvers/ci/runner_owner_project_resolver.rb26
-rw-r--r--app/graphql/resolvers/ci/runner_projects_resolver.rb14
-rw-r--r--app/graphql/resolvers/clusters/agent_tokens_resolver.rb13
-rw-r--r--app/graphql/resolvers/concerns/looks_ahead.rb16
-rw-r--r--app/graphql/resolvers/concerns/resolves_groups.rb1
-rw-r--r--app/graphql/resolvers/environments/nested_environments_resolver.rb18
-rw-r--r--app/graphql/resolvers/environments_resolver.rb4
-rw-r--r--app/graphql/resolvers/group_packages_resolver.rb6
-rw-r--r--app/graphql/resolvers/issues_resolver.rb5
-rw-r--r--app/graphql/resolvers/package_details_resolver.rb8
-rw-r--r--app/graphql/resolvers/package_pipelines_resolver.rb2
-rw-r--r--app/graphql/resolvers/paginated_tree_resolver.rb5
-rw-r--r--app/graphql/resolvers/project_jobs_resolver.rb14
-rw-r--r--app/graphql/resolvers/projects/fork_details_resolver.rb21
-rw-r--r--app/graphql/resolvers/work_items/work_item_discussions_resolver.rb68
-rw-r--r--app/graphql/resolvers/work_items_resolver.rb1
-rw-r--r--app/graphql/types/alert_management/alert_type.rb10
-rw-r--r--app/graphql/types/base_field.rb7
-rw-r--r--app/graphql/types/ci/config_variable_type.rb1
-rw-r--r--app/graphql/types/ci/freeze_period_status_enum.rb13
-rw-r--r--app/graphql/types/ci/freeze_period_type.rb41
-rw-r--r--app/graphql/types/ci/pipeline_schedule_type.rb46
-rw-r--r--app/graphql/types/ci/pipeline_schedule_variable_type.rb13
-rw-r--r--app/graphql/types/ci/pipeline_type.rb2
-rw-r--r--app/graphql/types/ci/runner_job_execution_status_enum.rb19
-rw-r--r--app/graphql/types/ci/runner_type.rb93
-rw-r--r--app/graphql/types/commit_signature_interface.rb5
-rw-r--r--app/graphql/types/commit_signatures/gpg_signature_type.rb1
-rw-r--r--app/graphql/types/commit_signatures/ssh_signature_type.rb23
-rw-r--r--app/graphql/types/commit_signatures/x509_signature_type.rb1
-rw-r--r--app/graphql/types/container_repository_type.rb2
-rw-r--r--app/graphql/types/dependency_proxy/manifest_type.rb2
-rw-r--r--app/graphql/types/deployment_details_type.rb17
-rw-r--r--app/graphql/types/deployment_type.rb18
-rw-r--r--app/graphql/types/environment_type.rb11
-rw-r--r--app/graphql/types/global_id_type.rb4
-rw-r--r--app/graphql/types/group_connection.rb22
-rw-r--r--app/graphql/types/issue_type_enum.rb4
-rw-r--r--app/graphql/types/key_type.rb17
-rw-r--r--app/graphql/types/merge_request_type.rb4
-rw-r--r--app/graphql/types/mutation_type.rb4
-rw-r--r--app/graphql/types/nested_environment_type.rb28
-rw-r--r--app/graphql/types/notes/note_type.rb8
-rw-r--r--app/graphql/types/packages/package_links_type.rb2
-rw-r--r--app/graphql/types/permission_types/base_permission_type.rb8
-rw-r--r--app/graphql/types/permission_types/deployment.rb14
-rw-r--r--app/graphql/types/permission_types/environment.rb11
-rw-r--r--app/graphql/types/permission_types/project.rb3
-rw-r--r--app/graphql/types/project_statistics_type.rb8
-rw-r--r--app/graphql/types/project_type.rb31
-rw-r--r--app/graphql/types/projects/fork_details_type.rb20
-rw-r--r--app/graphql/types/query_type.rb2
-rw-r--r--app/graphql/types/release_type.rb10
-rw-r--r--app/graphql/types/root_storage_statistics_type.rb2
-rw-r--r--app/graphql/types/subscription_type.rb5
-rw-r--r--app/graphql/types/todo_action_enum.rb3
-rw-r--r--app/graphql/types/todo_type.rb6
-rw-r--r--app/graphql/types/user_interface.rb5
-rw-r--r--app/graphql/types/work_items/notes_filter_type_enum.rb20
-rw-r--r--app/graphql/types/work_items/widget_interface.rb5
-rw-r--r--app/graphql/types/work_items/widgets/hierarchy_type.rb23
-rw-r--r--app/graphql/types/work_items/widgets/notes_type.rb26
-rw-r--r--app/helpers/application_helper.rb36
-rw-r--r--app/helpers/application_settings_helper.rb7
-rw-r--r--app/helpers/auth_helper.rb8
-rw-r--r--app/helpers/avatars_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb4
-rw-r--r--app/helpers/ci/jobs_helper.rb2
-rw-r--r--app/helpers/ci/runners_helper.rb4
-rw-r--r--app/helpers/ci/secure_files_helper.rb2
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb12
-rw-r--r--app/helpers/dropdowns_helper.rb8
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/environment_helper.rb1
-rw-r--r--app/helpers/environments_helper.rb6
-rw-r--r--app/helpers/events_helper.rb2
-rw-r--r--app/helpers/groups/observability_helper.rb8
-rw-r--r--app/helpers/groups/settings_helper.rb2
-rw-r--r--app/helpers/hooks_helper.rb2
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/ide_helper.rb12
-rw-r--r--app/helpers/integrations_helper.rb23
-rw-r--r--app/helpers/invite_members_helper.rb7
-rw-r--r--app/helpers/issuables_helper.rb34
-rw-r--r--app/helpers/issues_helper.rb12
-rw-r--r--app/helpers/labels_helper.rb6
-rw-r--r--app/helpers/listbox_helper.rb4
-rw-r--r--app/helpers/markup_helper.rb21
-rw-r--r--app/helpers/members_helper.rb22
-rw-r--r--app/helpers/nav/top_nav_helper.rb13
-rw-r--r--app/helpers/nav_helper.rb6
-rw-r--r--app/helpers/numbers_helper.rb2
-rw-r--r--app/helpers/page_layout_helper.rb4
-rw-r--r--app/helpers/preferences_helper.rb14
-rw-r--r--app/helpers/preferred_language_switcher_helper.rb21
-rw-r--r--app/helpers/profiles_helper.rb8
-rw-r--r--app/helpers/programming_languages_helper.rb20
-rw-r--r--app/helpers/projects/ml/experiments_helper.rb40
-rw-r--r--app/helpers/projects/pipeline_helper.rb1
-rw-r--r--app/helpers/projects_helper.rb48
-rw-r--r--app/helpers/routing/pseudonymization_helper.rb1
-rw-r--r--app/helpers/search_helper.rb65
-rw-r--r--app/helpers/sidebars_helper.rb8
-rw-r--r--app/helpers/sorting_helper.rb56
-rw-r--r--app/helpers/ssh_keys_helper.rb4
-rw-r--r--app/helpers/submodule_helper.rb4
-rw-r--r--app/helpers/timeboxes_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb82
-rw-r--r--app/helpers/tooling/visual_review_helper.rb8
-rw-r--r--app/helpers/version_check_helper.rb15
-rw-r--r--app/helpers/web_hooks/web_hooks_helper.rb2
-rw-r--r--app/helpers/wiki_helper.rb2
-rw-r--r--app/helpers/x509_helper.rb4
-rw-r--r--app/mailers/emails/profile.rb3
-rw-r--r--app/models/abuse_report.rb2
-rw-r--r--app/models/achievements/achievement.rb18
-rw-r--r--app/models/alert_management/alert.rb20
-rw-r--r--app/models/alert_management/http_integration.rb2
-rw-r--r--app/models/analytics/cycle_analytics/aggregation.rb11
-rw-r--r--app/models/analytics/usage_trends/measurement.rb6
-rw-r--r--app/models/appearance.rb2
-rw-r--r--app/models/application_setting.rb25
-rw-r--r--app/models/application_setting_implementation.rb6
-rw-r--r--app/models/audit_event.rb10
-rw-r--r--app/models/award_emoji.rb6
-rw-r--r--app/models/badge.rb2
-rw-r--r--app/models/blob_viewer/metrics_dashboard_yml.rb15
-rw-r--r--app/models/board_group_recent_visit.rb2
-rw-r--r--app/models/board_project_recent_visit.rb2
-rw-r--r--app/models/bulk_import.rb2
-rw-r--r--app/models/bulk_imports/entity.rb2
-rw-r--r--app/models/bulk_imports/export_upload.rb1
-rw-r--r--app/models/bulk_imports/tracker.rb2
-rw-r--r--app/models/ci/bridge.rb19
-rw-r--r--app/models/ci/build.rb54
-rw-r--r--app/models/ci/build_metadata.rb15
-rw-r--r--app/models/ci/build_need.rb5
-rw-r--r--app/models/ci/build_pending_state.rb4
-rw-r--r--app/models/ci/build_report_result.rb4
-rw-r--r--app/models/ci/build_runner_session.rb4
-rw-r--r--app/models/ci/build_trace_chunk.rb7
-rw-r--r--app/models/ci/build_trace_metadata.rb12
-rw-r--r--app/models/ci/freeze_period.rb59
-rw-r--r--app/models/ci/freeze_period_status.rb31
-rw-r--r--app/models/ci/job_artifact.rb9
-rw-r--r--app/models/ci/job_token/allowlist.rb42
-rw-r--r--app/models/ci/job_token/project_scope_link.rb4
-rw-r--r--app/models/ci/job_token/scope.rb59
-rw-r--r--app/models/ci/job_variable.rb3
-rw-r--r--app/models/ci/pending_build.rb3
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/ci/pipeline_schedule_variable.rb2
-rw-r--r--app/models/ci/processable.rb4
-rw-r--r--app/models/ci/resource_group.rb11
-rw-r--r--app/models/ci/runner.rb3
-rw-r--r--app/models/ci/runner_namespace.rb2
-rw-r--r--app/models/ci/running_build.rb11
-rw-r--r--app/models/ci/secure_file.rb11
-rw-r--r--app/models/ci/sources/pipeline.rb15
-rw-r--r--app/models/ci/unit_test_failure.rb4
-rw-r--r--app/models/clusters/agent_token.rb4
-rw-r--r--app/models/commit.rb10
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/commit_signatures/gpg_signature.rb9
-rw-r--r--app/models/commit_signatures/ssh_signature.rb9
-rw-r--r--app/models/commit_signatures/x509_commit_signature.rb9
-rw-r--r--app/models/concerns/avatarable.rb1
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/cached_commit.rb4
-rw-r--r--app/models/concerns/ci/partitionable.rb44
-rw-r--r--app/models/concerns/ci/partitionable/partitioned_filter.rb41
-rw-r--r--app/models/concerns/commit_signature.rb4
-rw-r--r--app/models/concerns/counter_attribute.rb201
-rw-r--r--app/models/concerns/has_user_type.rb6
-rw-r--r--app/models/concerns/issuable.rb2
-rw-r--r--app/models/concerns/milestoneable.rb23
-rw-r--r--app/models/concerns/sensitive_serializable_hash.rb2
-rw-r--r--app/models/concerns/signature_type.rb13
-rw-r--r--app/models/concerns/sortable.rb2
-rw-r--r--app/models/concerns/taskable.rb15
-rw-r--r--app/models/concerns/time_trackable.rb10
-rw-r--r--app/models/container_repository.rb19
-rw-r--r--app/models/customer_relations/organization.rb4
-rw-r--r--app/models/dependency_proxy/group_setting.rb2
-rw-r--r--app/models/deploy_token.rb1
-rw-r--r--app/models/deployment.rb9
-rw-r--r--app/models/environment.rb51
-rw-r--r--app/models/event.rb14
-rw-r--r--app/models/generic_commit_status.rb8
-rw-r--r--app/models/gpg_key.rb2
-rw-r--r--app/models/group.rb28
-rw-r--r--app/models/group_deploy_key.rb5
-rw-r--r--app/models/hooks/active_hook_filter.rb4
-rw-r--r--app/models/hooks/service_hook.rb5
-rw-r--r--app/models/hooks/web_hook.rb27
-rw-r--r--app/models/import_export_upload.rb1
-rw-r--r--app/models/integration.rb4
-rw-r--r--app/models/integrations/asana.rb6
-rw-r--r--app/models/integrations/bamboo.rb2
-rw-r--r--app/models/integrations/base_chat_notification.rb33
-rw-r--r--app/models/integrations/base_slack_notification.rb9
-rw-r--r--app/models/integrations/base_slash_commands.rb2
-rw-r--r--app/models/integrations/confluence.rb2
-rw-r--r--app/models/integrations/datadog.rb10
-rw-r--r--app/models/integrations/flowdock.rb43
-rw-r--r--app/models/integrations/jira.rb9
-rw-r--r--app/models/integrations/mattermost.rb2
-rw-r--r--app/models/integrations/packagist.rb8
-rw-r--r--app/models/integrations/pushover.rb2
-rw-r--r--app/models/integrations/slack.rb7
-rw-r--r--app/models/issue.rb20
-rw-r--r--app/models/issue_collection.rb44
-rw-r--r--app/models/issue_email_participant.rb2
-rw-r--r--app/models/iteration.rb3
-rw-r--r--app/models/jira_connect_installation.rb12
-rw-r--r--app/models/key.rb12
-rw-r--r--app/models/lfs_object.rb1
-rw-r--r--app/models/member.rb17
-rw-r--r--app/models/members/group_member.rb6
-rw-r--r--app/models/members/member_role.rb14
-rw-r--r--app/models/members/project_member.rb4
-rw-r--r--app/models/merge_request.rb20
-rw-r--r--app/models/merge_request/predictions.rb7
-rw-r--r--app/models/merge_request_context_commit.rb2
-rw-r--r--app/models/merge_request_diff.rb26
-rw-r--r--app/models/merge_request_diff_commit.rb2
-rw-r--r--app/models/ml/candidate.rb17
-rw-r--r--app/models/ml/candidate_metadata.rb14
-rw-r--r--app/models/ml/experiment.rb1
-rw-r--r--app/models/ml/experiment_metadata.rb14
-rw-r--r--app/models/namespace.rb38
-rw-r--r--app/models/namespace_setting.rb10
-rw-r--r--app/models/namespace_statistics.rb2
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/operations/feature_flags_client.rb6
-rw-r--r--app/models/packages/package.rb1
-rw-r--r--app/models/packages/rpm/repository_file.rb10
-rw-r--r--app/models/pages/lookup_path.rb4
-rw-r--r--app/models/pages/virtual_domain.rb1
-rw-r--r--app/models/pages_domain.rb3
-rw-r--r--app/models/performance_monitoring/prometheus_dashboard.rb7
-rw-r--r--app/models/personal_access_token.rb3
-rw-r--r--app/models/postgresql/detached_partition.rb4
-rw-r--r--app/models/programming_language.rb18
-rw-r--r--app/models/project.rb156
-rw-r--r--app/models/project_export_job.rb29
-rw-r--r--app/models/project_statistics.rb52
-rw-r--r--app/models/projects/forks/divergence_counts.rb72
-rw-r--r--app/models/projects/import_export/relation_export_upload.rb1
-rw-r--r--app/models/prometheus_alert.rb2
-rw-r--r--app/models/protected_branch.rb6
-rw-r--r--app/models/remote_mirror.rb7
-rw-r--r--app/models/resource_label_event.rb2
-rw-r--r--app/models/service_desk_setting.rb2
-rw-r--r--app/models/snippet_statistics.rb2
-rw-r--r--app/models/synthetic_note.rb1
-rw-r--r--app/models/todo.rb20
-rw-r--r--app/models/upload.rb9
-rw-r--r--app/models/user.rb67
-rw-r--r--app/models/user_detail.rb2
-rw-r--r--app/models/user_preference.rb69
-rw-r--r--app/models/users/callout.rb4
-rw-r--r--app/models/users/group_callout.rb3
-rw-r--r--app/models/users/phone_number_validation.rb6
-rw-r--r--app/models/work_item.rb19
-rw-r--r--app/models/work_items/hierarchy_restriction.rb14
-rw-r--r--app/models/work_items/parent_link.rb62
-rw-r--r--app/models/work_items/type.rb88
-rw-r--r--app/models/work_items/widgets/notes.rb14
-rw-r--r--app/policies/base_policy.rb11
-rw-r--r--app/policies/ci/freeze_period_policy.rb2
-rw-r--r--app/policies/ci/pipeline_schedule_variable_policy.rb7
-rw-r--r--app/policies/commit_signatures/ssh_signature_policy.rb7
-rw-r--r--app/policies/concerns/archived_abilities.rb (renamed from app/policies/concerns/readonly_abilities.rb)16
-rw-r--r--app/policies/group_member_policy.rb2
-rw-r--r--app/policies/group_policy.rb5
-rw-r--r--app/policies/issue_policy.rb19
-rw-r--r--app/policies/merge_request_policy.rb14
-rw-r--r--app/policies/namespaces/user_namespace_policy.rb3
-rw-r--r--app/policies/note_policy.rb8
-rw-r--r--app/policies/project_member_policy.rb2
-rw-r--r--app/policies/project_policy.rb35
-rw-r--r--app/presenters/blob_presenter.rb2
-rw-r--r--app/presenters/ci/freeze_period_presenter.rb13
-rw-r--r--app/presenters/group_member_presenter.rb4
-rw-r--r--app/presenters/member_presenter.rb4
-rw-r--r--app/presenters/packages/pypi/simple_package_versions_presenter.rb5
-rw-r--r--app/presenters/project_member_presenter.rb6
-rw-r--r--app/presenters/project_presenter.rb16
-rw-r--r--app/presenters/search_service_presenter.rb8
-rw-r--r--app/serializers/analytics/cycle_analytics/configuration_entity.rb6
-rw-r--r--app/serializers/build_details_entity.rb2
-rw-r--r--app/serializers/ci/basic_variable_entity.rb1
-rw-r--r--app/serializers/issuable_sidebar_basic_entity.rb4
-rw-r--r--app/serializers/issue_entity.rb13
-rw-r--r--app/serializers/member_entity.rb2
-rw-r--r--app/serializers/merge_request_metrics_entity.rb8
-rw-r--r--app/serializers/merge_request_poll_widget_entity.rb2
-rw-r--r--app/serializers/project_entity.rb4
-rw-r--r--app/services/admin/set_feature_flag_service.rb145
-rw-r--r--app/services/bulk_imports/create_service.rb33
-rw-r--r--app/services/bulk_imports/file_download_service.rb18
-rw-r--r--app/services/chat_names/find_user_service.rb13
-rw-r--r--app/services/ci/append_build_trace_service.rb13
-rw-r--r--app/services/ci/create_downstream_pipeline_service.rb28
-rw-r--r--app/services/ci/create_pipeline_service.rb19
-rw-r--r--app/services/ci/enqueue_job_service.rb25
-rw-r--r--app/services/ci/generate_kubeconfig_service.rb11
-rw-r--r--app/services/ci/job_artifacts/create_service.rb4
-rw-r--r--app/services/ci/pipeline_schedule_service.rb2
-rw-r--r--app/services/ci/pipeline_schedules/calculate_next_run_service.rb6
-rw-r--r--app/services/ci/play_bridge_service.rb7
-rw-r--r--app/services/ci/play_build_service.rb12
-rw-r--r--app/services/ci/process_build_service.rb2
-rw-r--r--app/services/ci/register_job_service.rb16
-rw-r--r--app/services/ci/reset_skipped_jobs_service.rb (renamed from app/services/ci/after_requeue_job_service.rb)4
-rw-r--r--app/services/ci/retry_job_service.rb13
-rw-r--r--app/services/ci/test_failure_history_service.rb3
-rw-r--r--app/services/ci/track_failed_build_service.rb5
-rw-r--r--app/services/ci/unlock_artifacts_service.rb50
-rw-r--r--app/services/clusters/agents/filter_authorizations_service.rb50
-rw-r--r--app/services/clusters/agents/refresh_authorization_service.rb6
-rw-r--r--app/services/clusters/applications/base_service.rb96
-rw-r--r--app/services/clusters/applications/check_progress_service.rb50
-rw-r--r--app/services/clusters/applications/install_service.rb32
-rw-r--r--app/services/clusters/applications/prometheus_config_service.rb155
-rw-r--r--app/services/clusters/applications/upgrade_service.rb34
-rw-r--r--app/services/clusters/kubernetes/create_or_update_service_account_service.rb2
-rw-r--r--app/services/concerns/incident_management/usage_data.rb18
-rw-r--r--app/services/concerns/rate_limited_service.rb4
-rw-r--r--app/services/deployments/create_for_build_service.rb2
-rw-r--r--app/services/design_management/generate_image_versions_service.rb20
-rw-r--r--app/services/environments/create_for_build_service.rb8
-rw-r--r--app/services/environments/schedule_to_delete_review_apps_service.rb2
-rw-r--r--app/services/error_tracking/list_projects_service.rb21
-rw-r--r--app/services/event_create_service.rb86
-rw-r--r--app/services/git/branch_hooks_service.rb21
-rw-r--r--app/services/groups/group_links/create_service.rb2
-rw-r--r--app/services/groups/group_links/destroy_service.rb2
-rw-r--r--app/services/groups/group_links/update_service.rb2
-rw-r--r--app/services/groups/import_export/import_service.rb18
-rw-r--r--app/services/import/base_service.rb25
-rw-r--r--app/services/import/bitbucket_server_service.rb2
-rw-r--r--app/services/import/github/gists_import_service.rb34
-rw-r--r--app/services/import/github_service.rb1
-rw-r--r--app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.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/incident_management/incidents/create_service.rb2
-rw-r--r--app/services/incident_management/link_alerts/base_service.rb27
-rw-r--r--app/services/incident_management/link_alerts/create_service.rb42
-rw-r--r--app/services/incident_management/link_alerts/destroy_service.rb30
-rw-r--r--app/services/incident_management/pager_duty/process_webhook_service.rb22
-rw-r--r--app/services/incident_management/timeline_events/base_service.rb29
-rw-r--r--app/services/incident_management/timeline_events/create_service.rb13
-rw-r--r--app/services/incident_management/timeline_events/destroy_service.rb2
-rw-r--r--app/services/incident_management/timeline_events/update_service.rb43
-rw-r--r--app/services/issuable/discussions_list_service.rb16
-rw-r--r--app/services/issue_links/create_service.rb4
-rw-r--r--app/services/issues/base_service.rb5
-rw-r--r--app/services/issues/close_service.rb13
-rw-r--r--app/services/issues/create_service.rb2
-rw-r--r--app/services/issues/move_service.rb13
-rw-r--r--app/services/issues/update_service.rb2
-rw-r--r--app/services/jira_connect/create_asymmetric_jwt_service.rb11
-rw-r--r--app/services/jira_connect_installations/proxy_lifecycle_event_service.rb91
-rw-r--r--app/services/jira_connect_installations/update_service.rb61
-rw-r--r--app/services/jira_import/start_import_service.rb2
-rw-r--r--app/services/markup/rendering_service.rb36
-rw-r--r--app/services/members/destroy_service.rb4
-rw-r--r--app/services/merge_requests/after_create_service.rb2
-rw-r--r--app/services/merge_requests/approval_service.rb1
-rw-r--r--app/services/merge_requests/assign_issues_service.rb16
-rw-r--r--app/services/merge_requests/base_service.rb15
-rw-r--r--app/services/merge_requests/build_service.rb10
-rw-r--r--app/services/merge_requests/create_service.rb2
-rw-r--r--app/services/merge_requests/push_options_handler_service.rb2
-rw-r--r--app/services/merge_requests/remove_approval_service.rb1
-rw-r--r--app/services/metrics/dashboard/grafana_metric_embed_service.rb3
-rw-r--r--app/services/ml/experiment_tracking/candidate_repository.rb59
-rw-r--r--app/services/ml/experiment_tracking/experiment_repository.rb41
-rw-r--r--app/services/notification_service.rb18
-rw-r--r--app/services/packages/debian/process_package_file_service.rb101
-rw-r--r--app/services/packages/rpm/parse_package_service.rb4
-rw-r--r--app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb2
-rw-r--r--app/services/pages_domains/retry_acme_order_service.rb21
-rw-r--r--app/services/personal_access_tokens/revoke_service.rb24
-rw-r--r--app/services/projects/batch_forks_count_service.rb4
-rw-r--r--app/services/projects/batch_open_issues_count_service.rb4
-rw-r--r--app/services/projects/container_repository/cleanup_tags_base_service.rb6
-rw-r--r--app/services/projects/container_repository/destroy_service.rb40
-rw-r--r--app/services/projects/container_repository/gitlab/cleanup_tags_service.rb6
-rw-r--r--app/services/projects/create_service.rb3
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/services/projects/import_export/parallel_export_service.rb98
-rw-r--r--app/services/projects/import_service.rb3
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_link_list_service.rb25
-rw-r--r--app/services/projects/lfs_pointers/lfs_download_service.rb6
-rw-r--r--app/services/projects/lfs_pointers/lfs_import_service.rb4
-rw-r--r--app/services/projects/lfs_pointers/lfs_object_download_list_service.rb43
-rw-r--r--app/services/projects/refresh_build_artifacts_size_statistics_service.rb2
-rw-r--r--app/services/projects/update_pages_service.rb7
-rw-r--r--app/services/projects/update_remote_mirror_service.rb2
-rw-r--r--app/services/projects/update_service.rb38
-rw-r--r--app/services/protected_branches/api_service.rb6
-rw-r--r--app/services/protected_branches/base_service.rb8
-rw-r--r--app/services/protected_branches/cache_service.rb11
-rw-r--r--app/services/protected_branches/create_service.rb4
-rw-r--r--app/services/protected_branches/destroy_service.rb2
-rw-r--r--app/services/protected_branches/legacy_api_create_service.rb2
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb2
-rw-r--r--app/services/protected_branches/update_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb6
-rw-r--r--app/services/repositories/housekeeping_service.rb6
-rw-r--r--app/services/search_service.rb6
-rw-r--r--app/services/snippets/create_service.rb2
-rw-r--r--app/services/system_notes/commit_service.rb8
-rw-r--r--app/services/task_list_toggle_service.rb4
-rw-r--r--app/services/timelogs/base_service.rb4
-rw-r--r--app/services/timelogs/create_service.rb3
-rw-r--r--app/services/todo_service.rb82
-rw-r--r--app/services/users/approve_service.rb2
-rw-r--r--app/services/users/assigned_issues_count_service.rb63
-rw-r--r--app/services/users/banned_user_base_service.rb2
-rw-r--r--app/services/users/build_service.rb2
-rw-r--r--app/services/users/keys_count_service.rb2
-rw-r--r--app/services/users/migrate_records_to_ghost_user_service.rb5
-rw-r--r--app/services/users/reject_service.rb2
-rw-r--r--app/services/users/update_highest_member_role_service.rb4
-rw-r--r--app/services/web_hooks/log_execution_service.rb16
-rw-r--r--app/services/wiki_pages/update_service.rb2
-rw-r--r--app/services/work_items/create_and_link_service.rb2
-rw-r--r--app/services/work_items/create_from_task_service.rb2
-rw-r--r--app/services/work_items/create_service.rb2
-rw-r--r--app/services/work_items/delete_task_service.rb2
-rw-r--r--app/uploaders/ci/secure_file_uploader.rb4
-rw-r--r--app/uploaders/file_mover.rb1
-rw-r--r--app/uploaders/file_uploader.rb4
-rw-r--r--app/uploaders/gitlab_uploader.rb13
-rw-r--r--app/uploaders/object_storage.rb63
-rw-r--r--app/uploaders/packages/composer/cache_uploader.rb2
-rw-r--r--app/uploaders/packages/debian/component_file_uploader.rb2
-rw-r--r--app/uploaders/packages/debian/distribution_release_file_uploader.rb2
-rw-r--r--app/uploaders/packages/package_file_uploader.rb2
-rw-r--r--app/uploaders/packages/rpm/repository_file_uploader.rb2
-rw-r--r--app/uploaders/pages/deployment_uploader.rb7
-rw-r--r--app/uploaders/terraform/state_uploader.rb4
-rw-r--r--app/validators/iso8601_date_validator.rb9
-rw-r--r--app/validators/json_schemas/build_metadata_id_tokens.json29
-rw-r--r--app/validators/json_schemas/build_report_result_data.json11
-rw-r--r--app/validators/json_schemas/build_report_result_data_tests.json26
-rw-r--r--app/validators/json_schemas/ci_secure_file_metadata.json4
-rw-r--r--app/validators/json_schemas/daily_build_group_report_result_data.json7
-rw-r--r--app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json10
-rw-r--r--app/validators/json_schemas/web_hooks_url_variables.json2
-rw-r--r--app/views/abuse_reports/new.html.haml6
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml12
-rw-r--r--app/views/admin/application_settings/_account_and_limit.html.haml2
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml8
-rw-r--r--app/views/admin/application_settings/_default_branch.html.haml2
-rw-r--r--app/views/admin/application_settings/_error_tracking.html.haml2
-rw-r--r--app/views/admin/application_settings/_git_lfs_limits.html.haml2
-rw-r--r--app/views/admin/application_settings/_grafana.html.haml2
-rw-r--r--app/views/admin/application_settings/_kroki.html.haml2
-rw-r--r--app/views/admin/application_settings/_localization.html.haml7
-rw-r--r--app/views/admin/application_settings/_mailgun.html.haml2
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml2
-rw-r--r--app/views/admin/application_settings/_performance_bar.html.haml2
-rw-r--r--app/views/admin/application_settings/_plantuml.html.haml2
-rw-r--r--app/views/admin/application_settings/_repository_check.html.haml41
-rw-r--r--app/views/admin/application_settings/_repository_static_objects.html.haml4
-rw-r--r--app/views/admin/application_settings/_repository_storage.html.haml2
-rw-r--r--app/views/admin/application_settings/_runner_registrars_form.html.haml4
-rw-r--r--app/views/admin/application_settings/_search_limits.html.haml4
-rw-r--r--app/views/admin/application_settings/_spam.html.haml5
-rw-r--r--app/views/admin/application_settings/_terminal.html.haml4
-rw-r--r--app/views/admin/application_settings/_terraform_limits.html.haml11
-rw-r--r--app/views/admin/application_settings/_visibility_and_access.html.haml8
-rw-r--r--app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml2
-rw-r--r--app/views/admin/application_settings/appearances/show.html.haml1
-rw-r--r--app/views/admin/application_settings/ci/_header.html.haml3
-rw-r--r--app/views/admin/application_settings/ci_cd.html.haml1
-rw-r--r--app/views/admin/application_settings/general.html.haml3
-rw-r--r--app/views/admin/application_settings/integrations.html.haml1
-rw-r--r--app/views/admin/application_settings/metrics_and_profiling.html.haml5
-rw-r--r--app/views/admin/application_settings/network.html.haml1
-rw-r--r--app/views/admin/application_settings/preferences.html.haml17
-rw-r--r--app/views/admin/application_settings/reporting.html.haml1
-rw-r--r--app/views/admin/application_settings/repository.html.haml7
-rw-r--r--app/views/admin/application_settings/service_usage_data.html.haml5
-rw-r--r--app/views/admin/applications/index.html.haml6
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml8
-rw-r--r--app/views/admin/broadcast_messages/edit.html.haml17
-rw-r--r--app/views/admin/broadcast_messages/index.html.haml1
-rw-r--r--app/views/admin/dashboard/_security_newsletter_callout.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml11
-rw-r--r--app/views/admin/deploy_keys/edit.html.haml3
-rw-r--r--app/views/admin/groups/_form.html.haml6
-rw-r--r--app/views/admin/groups/_group.html.haml6
-rw-r--r--app/views/admin/groups/index.html.haml3
-rw-r--r--app/views/admin/hook_logs/show.html.haml7
-rw-r--r--app/views/admin/identities/_form.html.haml4
-rw-r--r--app/views/admin/identities/_identity.html.haml4
-rw-r--r--app/views/admin/labels/index.html.haml5
-rw-r--r--app/views/admin/projects/_projects.html.haml4
-rw-r--r--app/views/admin/projects/index.html.haml4
-rw-r--r--app/views/admin/projects/show.html.haml5
-rw-r--r--app/views/admin/topics/_form.html.haml11
-rw-r--r--app/views/admin/topics/index.html.haml2
-rw-r--r--app/views/admin/users/_form.html.haml6
-rw-r--r--app/views/admin/users/_head.html.haml3
-rw-r--r--app/views/admin/users/_users.html.haml2
-rw-r--r--app/views/ci/runner/_how_to_setup_runner.html.haml5
-rw-r--r--app/views/ci/variables/_content.html.haml10
-rw-r--r--app/views/ci/variables/_header.html.haml2
-rw-r--r--app/views/ci/variables/_index.html.haml2
-rw-r--r--app/views/ci/variables/_variable_row.html.haml3
-rw-r--r--app/views/clusters/clusters/_details.html.haml2
-rw-r--r--app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml2
-rw-r--r--app/views/dashboard/_activities.html.haml3
-rw-r--r--app/views/dashboard/_groups_head.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml17
-rw-r--r--app/views/dashboard/_projects_nav.html.haml8
-rw-r--r--app/views/dashboard/_snippets_head.html.haml3
-rw-r--r--app/views/dashboard/issues.html.haml6
-rw-r--r--app/views/dashboard/merge_requests.html.haml1
-rw-r--r--app/views/dashboard/projects/_nav.html.haml23
-rw-r--r--app/views/dashboard/projects/index.html.haml2
-rw-r--r--app/views/dashboard/todos/_todo.html.haml98
-rw-r--r--app/views/dashboard/todos/index.html.haml1
-rw-r--r--app/views/devise/shared/_footer.html.haml3
-rw-r--r--app/views/devise/shared/_language_switcher.html.haml3
-rw-r--r--app/views/devise/shared/_signup_box.html.haml1
-rw-r--r--app/views/devise/unlocks/new.html.haml4
-rw-r--r--app/views/explore/projects/_filter.html.haml4
-rw-r--r--app/views/explore/projects/_nav.html.haml1
-rw-r--r--app/views/explore/projects/index.html.haml2
-rw-r--r--app/views/explore/projects/page_out_of_bounds.html.haml5
-rw-r--r--app/views/explore/projects/starred.html.haml2
-rw-r--r--app/views/explore/projects/topic.html.haml1
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/groups/_activities.html.haml3
-rw-r--r--app/views/groups/_group_admin_settings.html.haml10
-rw-r--r--app/views/groups/_home_panel.html.haml50
-rw-r--r--app/views/groups/_import_group_from_another_instance_panel.html.haml40
-rw-r--r--app/views/groups/_invite_groups_modal.html.haml2
-rw-r--r--app/views/groups/_invite_members_modal.html.haml1
-rw-r--r--app/views/groups/_new_group_fields.html.haml6
-rw-r--r--app/views/groups/edit.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml4
-rw-r--r--app/views/groups/issues.html.haml3
-rw-r--r--app/views/groups/labels/index.html.haml2
-rw-r--r--app/views/groups/merge_requests.html.haml1
-rw-r--r--app/views/groups/milestones/_form.html.haml6
-rw-r--r--app/views/groups/milestones/index.html.haml10
-rw-r--r--app/views/groups/projects.html.haml16
-rw-r--r--app/views/groups/registry/repositories/index.html.haml3
-rw-r--r--app/views/groups/runners/_settings.html.haml7
-rw-r--r--app/views/groups/runners/index.html.haml2
-rw-r--r--app/views/groups/settings/_export.html.haml12
-rw-r--r--app/views/groups/settings/_general.html.haml2
-rw-r--r--app/views/groups/settings/_git_access_protocols.html.haml2
-rw-r--r--app/views/groups/settings/_permissions.html.haml2
-rw-r--r--app/views/groups/settings/_remove_button.html.haml4
-rw-r--r--app/views/groups/settings/_transfer.html.haml4
-rw-r--r--app/views/groups/settings/applications/index.html.haml1
-rw-r--r--app/views/groups/settings/ci_cd/_auto_devops_form.html.haml2
-rw-r--r--app/views/groups/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/groups/settings/repository/_default_branch.html.haml2
-rw-r--r--app/views/groups/usage_quotas/index.html.haml7
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/ide/_show.html.haml15
-rw-r--r--app/views/import/_githubish_status.html.haml3
-rw-r--r--app/views/import/bulk_imports/status.html.haml1
-rw-r--r--app/views/import/github/status.html.haml1
-rw-r--r--app/views/import/gitlab_projects/new.html.haml7
-rw-r--r--app/views/import/manifest/_form.html.haml20
-rw-r--r--app/views/import/shared/_new_project_form.html.haml29
-rw-r--r--app/views/invites/show.html.haml6
-rw-r--r--app/views/jira_connect/users/show.html.haml5
-rw-r--r--app/views/layouts/_google_tag_manager_head.html.haml11
-rw-r--r--app/views/layouts/_head.html.haml18
-rw-r--r--app/views/layouts/_loading_hints.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/_search.html.haml2
-rw-r--r--app/views/layouts/group_settings.html.haml1
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml8
-rw-r--r--app/views/layouts/header/_gitlab_version.html.haml2
-rw-r--r--app/views/layouts/header/_marketing_links.html.haml16
-rw-r--r--app/views/layouts/header/_registration_enabled_callout.html.haml6
-rw-r--r--app/views/layouts/header/_sign_in_register_button.html.haml3
-rw-r--r--app/views/layouts/jira_connect.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml24
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml26
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml2
-rw-r--r--app/views/layouts/project_settings.html.haml1
-rw-r--r--app/views/layouts/search.html.haml1
-rw-r--r--app/views/notify/_reassigned_issuable_email.html.haml2
-rw-r--r--app/views/notify/access_token_revoked_email.html.haml2
-rw-r--r--app/views/notify/access_token_revoked_email.text.erb4
-rw-r--r--app/views/notify/autodevops_disabled_email.html.haml2
-rw-r--r--app/views/notify/issue_moved_email.html.haml2
-rw-r--r--app/views/notify/repository_push_email.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml6
-rw-r--r--app/views/profiles/keys/_form.html.haml6
-rw-r--r--app/views/profiles/keys/_key.html.haml5
-rw-r--r--app/views/profiles/keys/_key_details.html.haml5
-rw-r--r--app/views/profiles/preferences/show.html.haml23
-rw-r--r--app/views/projects/_files.html.haml11
-rw-r--r--app/views/projects/_flash_messages.html.haml4
-rw-r--r--app/views/projects/_fork_info.html.haml14
-rw-r--r--app/views/projects/_home_panel.html.haml37
-rw-r--r--app/views/projects/_invite_groups_modal.html.haml2
-rw-r--r--app/views/projects/_invite_members_modal.html.haml1
-rw-r--r--app/views/projects/_merge_request_merge_checks_settings.html.haml13
-rw-r--r--app/views/projects/_merge_request_pipelines_and_threads_options.html.haml13
-rw-r--r--app/views/projects/_new_project_fields.html.haml3
-rw-r--r--app/views/projects/blob/_editor.html.haml3
-rw-r--r--app/views/projects/blob/_template_selectors.html.haml10
-rw-r--r--app/views/projects/branches/new.html.haml12
-rw-r--r--app/views/projects/buttons/_clone.html.haml12
-rw-r--r--app/views/projects/buttons/_download.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml8
-rw-r--r--app/views/projects/buttons/_star.html.haml12
-rw-r--r--app/views/projects/commit/_commit_box.html.haml4
-rw-r--r--app/views/projects/commit/_signature.html.haml2
-rw-r--r--app/views/projects/commit/_signature_badge.html.haml15
-rw-r--r--app/views/projects/commit/_signature_badge_user.html.haml22
-rw-r--r--app/views/projects/commit/x509/_signature_badge_user.html.haml2
-rw-r--r--app/views/projects/commits/_commits.html.haml4
-rw-r--r--app/views/projects/commits/show.html.haml5
-rw-r--r--app/views/projects/diffs/_diffs.html.haml2
-rw-r--r--app/views/projects/environments/show.html.haml49
-rw-r--r--app/views/projects/graphs/show.html.haml10
-rw-r--r--app/views/projects/issuable/_show.html.haml1
-rw-r--r--app/views/projects/issues/index.html.haml5
-rw-r--r--app/views/projects/issues/service_desk.html.haml2
-rw-r--r--app/views/projects/jobs/_table.html.haml18
-rw-r--r--app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml42
-rw-r--r--app/views/projects/merge_requests/_code_dropdown.html.haml34
-rw-r--r--app/views/projects/merge_requests/_page.html.haml114
-rw-r--r--app/views/projects/merge_requests/creations/_new_compare.html.haml27
-rw-r--r--app/views/projects/merge_requests/diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/index.html.haml1
-rw-r--r--app/views/projects/merge_requests/show.html.haml114
-rw-r--r--app/views/projects/ml/candidates/show.html.haml7
-rw-r--r--app/views/projects/network/show.html.haml3
-rw-r--r--app/views/projects/notes/_more_actions_dropdown.html.haml2
-rw-r--r--app/views/projects/pages/_list.html.haml2
-rw-r--r--app/views/projects/pages_domains/new.html.haml9
-rw-r--r--app/views/projects/pages_domains/show.html.haml6
-rw-r--r--app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml61
-rw-r--r--app/views/projects/pipeline_schedules/_table.html.haml20
-rw-r--r--app/views/projects/pipeline_schedules/index.html.haml5
-rw-r--r--app/views/projects/pipeline_schedules/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml7
-rw-r--r--app/views/projects/pipelines/_with_tabs.html.haml48
-rw-r--r--app/views/projects/pipelines/show.html.haml12
-rw-r--r--app/views/projects/project_members/index.html.haml6
-rw-r--r--app/views/projects/protected_branches/_branches_list.html.haml4
-rw-r--r--app/views/projects/protected_branches/_index.html.haml7
-rw-r--r--app/views/projects/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/projects/protected_branches/_update_protected_branch.html.haml1
-rw-r--r--app/views/projects/protected_tags/shared/_create_protected_tag.html.haml4
-rw-r--r--app/views/projects/registry/repositories/index.html.haml3
-rw-r--r--app/views/projects/runners/_group_runners.html.haml10
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml9
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml2
-rw-r--r--app/views/projects/settings/_general.html.haml4
-rw-r--r--app/views/projects/settings/branch_rules/index.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_autodevops_form.html.haml4
-rw-r--r--app/views/projects/settings/operations/_alert_management.html.haml2
-rw-r--r--app/views/projects/settings/repository/_protected_branches.html.haml2
-rw-r--r--app/views/projects/show.html.haml5
-rw-r--r--app/views/projects/starrers/index.html.haml1
-rw-r--r--app/views/projects/tags/new.html.haml14
-rw-r--r--app/views/projects/tree/_tree_header.html.haml2
-rw-r--r--app/views/projects/triggers/_form.html.haml4
-rw-r--r--app/views/protected_branches/_branches_list.html.haml4
-rw-r--r--app/views/protected_branches/_create_protected_branch.html.haml (renamed from app/views/projects/protected_branches/_create_protected_branch.html.haml)2
-rw-r--r--app/views/protected_branches/_index.html.haml7
-rw-r--r--app/views/protected_branches/_protected_branch.html.haml2
-rw-r--r--app/views/protected_branches/_update_protected_branch.html.haml1
-rw-r--r--app/views/protected_branches/shared/_branches_list.html.haml (renamed from app/views/projects/protected_branches/shared/_branches_list.html.haml)2
-rw-r--r--app/views/protected_branches/shared/_create_protected_branch.html.haml (renamed from app/views/projects/protected_branches/shared/_create_protected_branch.html.haml)4
-rw-r--r--app/views/protected_branches/shared/_dropdown.html.haml (renamed from app/views/projects/protected_branches/shared/_dropdown.html.haml)0
-rw-r--r--app/views/protected_branches/shared/_index.html.haml (renamed from app/views/projects/protected_branches/shared/_index.html.haml)0
-rw-r--r--app/views/protected_branches/shared/_matching_branch.html.haml (renamed from app/views/projects/protected_branches/shared/_matching_branch.html.haml)0
-rw-r--r--app/views/protected_branches/shared/_protected_branch.html.haml (renamed from app/views/projects/protected_branches/shared/_protected_branch.html.haml)2
-rw-r--r--app/views/protected_branches/shared/_update_protected_branch.html.haml (renamed from app/views/shared/projects/protected_branches/_update_protected_branch.html.haml)4
-rw-r--r--app/views/protected_branches/show.html.haml (renamed from app/views/projects/protected_branches/show.html.haml)2
-rw-r--r--app/views/pwa/manifest.json.erb6
-rw-r--r--app/views/registrations/welcome/show.html.haml36
-rw-r--r--app/views/search/_category.html.haml2
-rw-r--r--app/views/search/results/_issuable.html.haml2
-rw-r--r--app/views/search/show.html.haml4
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml25
-rw-r--r--app/views/shared/_file_highlight.html.haml28
-rw-r--r--app/views/shared/_ide_root.html.haml11
-rw-r--r--app/views/shared/_issuable_meta_data.html.haml10
-rw-r--r--app/views/shared/_label.html.haml12
-rw-r--r--app/views/shared/_milestones_filter.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml2
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/_web_ide_button.html.haml2
-rw-r--r--app/views/shared/builds/_tabs.html.haml2
-rw-r--r--app/views/shared/empty_states/_milestones.html.haml2
-rw-r--r--app/views/shared/empty_states/_milestones_tab.html.haml2
-rw-r--r--app/views/shared/file_hooks/_index.html.haml21
-rw-r--r--app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml4
-rw-r--r--app/views/shared/integrations/prometheus/_custom_metrics.html.haml4
-rw-r--r--app/views/shared/integrations/prometheus/_metrics.html.haml4
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml4
-rw-r--r--app/views/shared/issuable/form/_title.html.haml4
-rw-r--r--app/views/shared/issue_type/_details_content.html.haml2
-rw-r--r--app/views/shared/nav/_sidebar_submenu.html.haml2
-rw-r--r--app/views/shared/projects/_dropdown.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/projects/_search_bar.html.haml26
-rw-r--r--app/views/shared/projects/_search_form.html.haml21
-rw-r--r--app/views/shared/projects/_sort_dropdown.html.haml39
-rw-r--r--app/views/shared/runners/_form.html.haml2
-rw-r--r--app/views/shared/ssh_keys/_key_delete.html.haml14
-rw-r--r--app/views/shared/topics/_search_form.html.haml2
-rw-r--r--app/views/shared/web_hooks/_form.html.haml44
-rw-r--r--app/views/shared/web_hooks/_hook.html.haml2
-rw-r--r--app/views/shared/web_hooks/_test_button.html.haml6
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/views/web_ide/remote_ide/index.html.haml5
-rw-r--r--app/workers/all_queues.yml112
-rw-r--r--app/workers/bulk_imports/entity_worker.rb2
-rw-r--r--app/workers/bulk_imports/export_request_worker.rb62
-rw-r--r--app/workers/bulk_imports/pipeline_worker.rb127
-rw-r--r--app/workers/ci/create_downstream_pipeline_worker.rb8
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb9
-rw-r--r--app/workers/concerns/waitable_worker.rb25
-rw-r--r--app/workers/container_registry/cleanup_worker.rb2
-rw-r--r--app/workers/container_registry/delete_container_repository_worker.rb1
-rw-r--r--app/workers/container_registry/migration/enqueuer_worker.rb4
-rw-r--r--app/workers/database/batched_background_migration/ci_database_worker.rb4
-rw-r--r--app/workers/database/batched_background_migration/ci_execution_worker.rb9
-rw-r--r--app/workers/database/batched_background_migration/execution_worker.rb37
-rw-r--r--app/workers/database/batched_background_migration/main_execution_worker.rb9
-rw-r--r--app/workers/database/batched_background_migration/single_database_worker.rb41
-rw-r--r--app/workers/database/batched_background_migration_worker.rb4
-rw-r--r--app/workers/delete_container_repository_worker.rb61
-rw-r--r--app/workers/flush_counter_increments_worker.rb2
-rw-r--r--app/workers/gitlab/export/prune_project_export_jobs_worker.rb23
-rw-r--r--app/workers/gitlab/github_gists_import/finish_import_worker.rb46
-rw-r--r--app/workers/gitlab/github_gists_import/import_gist_worker.rb75
-rw-r--r--app/workers/gitlab/github_gists_import/start_import_worker.rb64
-rw-r--r--app/workers/gitlab_shell_worker.rb2
-rw-r--r--app/workers/issuable_export_csv_worker.rb4
-rw-r--r--app/workers/jira_connect/send_uninstalled_hook_worker.rb22
-rw-r--r--app/workers/mail_scheduler/notification_service_worker.rb4
-rw-r--r--app/workers/merge_requests/delete_branch_worker.rb27
-rw-r--r--app/workers/merge_requests/delete_source_branch_worker.rb9
-rw-r--r--app/workers/namespaces/root_statistics_worker.rb2
-rw-r--r--app/workers/object_storage/background_move_worker.rb35
-rw-r--r--app/workers/packages/debian/process_package_file_worker.rb52
-rw-r--r--app/workers/post_receive.rb8
-rw-r--r--app/workers/projects/delete_branch_worker.rb30
-rw-r--r--app/workers/projects/import_export/parallel_project_export_worker.rb61
-rw-r--r--app/workers/projects/inactive_projects_deletion_cron_worker.rb10
-rw-r--r--app/workers/run_pipeline_schedule_worker.rb2
-rw-r--r--app/workers/update_highest_role_worker.rb2
1853 files changed, 21619 insertions, 12290 deletions
diff --git a/app/assets/images/web-ide-promo-popover.svg b/app/assets/images/web-ide-promo-popover.svg
new file mode 100644
index 00000000000..3ced89860da
--- /dev/null
+++ b/app/assets/images/web-ide-promo-popover.svg
@@ -0,0 +1,101 @@
+<svg width="280" height="140" viewBox="0 0 280 140" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g clip-path="url(#clip0_187_122567)">
+<g clip-path="url(#clip1_187_122567)">
+<circle cx="189.5" cy="-42.5" r="131.5" fill="url(#paint0_radial_187_122567)"/>
+<circle cx="-41.5" cy="-97.5" r="198.5" fill="url(#paint1_radial_187_122567)"/>
+<circle cx="309.5" cy="-7.5" r="121.5" fill="url(#paint2_radial_187_122567)"/>
+<g filter="url(#filter0_b_187_122567)">
+<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V130H0V4Z" fill="white" fill-opacity="0.01"/>
+</g>
+</g>
+<path d="M183.948 47.9647H100.897V100.482H183.948V47.9647Z" fill="white"/>
+<path d="M100.64 47.9647H184.06V98.9817C184.06 104.1 179.908 108.256 174.793 108.256H100.638V47.9647H100.64Z" fill="white"/>
+<path d="M184.314 34.7452H100.64V47.9676H184.314V34.7452Z" fill="#AEA5D6"/>
+<path d="M109.594 43.2574C110.644 43.2574 111.495 42.4056 111.495 41.3549C111.495 40.3043 110.644 39.4525 109.594 39.4525C108.544 39.4525 107.693 40.3043 107.693 41.3549C107.693 42.4056 108.544 43.2574 109.594 43.2574Z" fill="#10B1B1"/>
+<path d="M116.482 43.2574C117.532 43.2574 118.383 42.4056 118.383 41.3549C118.383 40.3043 117.532 39.4525 116.482 39.4525C115.432 39.4525 114.581 40.3043 114.581 41.3549C114.581 42.4056 115.432 43.2574 116.482 43.2574Z" fill="#A888F4"/>
+<path d="M123.368 43.2574C124.418 43.2574 125.269 42.4056 125.269 41.3549C125.269 40.3043 124.418 39.4525 123.368 39.4525C122.318 39.4525 121.467 40.3043 121.467 41.3549C121.467 42.4056 122.318 43.2574 123.368 43.2574Z" fill="#FF9D73"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M100.556 34.4038H165.377V35.0858H101.238V61.7159H100.556V34.4038Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M101.238 66.3383V83.4486H100.556V66.3383H101.238Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 99.0231V89.5341H184.403V99.0231C184.403 104.311 180.118 108.599 174.834 108.599H120.244V107.917H174.834C179.741 107.917 183.721 103.935 183.721 99.0231Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M183.721 83.1296V62.2422H184.403V83.1296H183.721Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M109.596 39.7936C108.734 39.7936 108.036 40.4924 108.036 41.355C108.036 42.2176 108.734 42.9164 109.596 42.9164C110.457 42.9164 111.155 42.2176 111.155 41.355C111.155 40.4924 110.457 39.7936 109.596 39.7936ZM107.354 41.355C107.354 40.1162 108.357 39.1116 109.596 39.1116C110.834 39.1116 111.837 40.1162 111.837 41.355C111.837 42.5938 110.834 43.5985 109.596 43.5985C108.357 43.5985 107.354 42.5938 107.354 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M116.48 39.7936C115.619 39.7936 114.92 40.4924 114.92 41.355C114.92 42.2176 115.619 42.9164 116.48 42.9164C117.342 42.9164 118.04 42.2176 118.04 41.355C118.04 40.4924 117.342 39.7936 116.48 39.7936ZM114.238 41.355C114.238 40.1162 115.242 39.1116 116.48 39.1116C117.719 39.1116 118.722 40.1162 118.722 41.355C118.722 42.5938 117.719 43.5985 116.48 43.5985C115.242 43.5985 114.238 42.5938 114.238 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M123.367 39.7936C122.506 39.7936 121.808 40.4924 121.808 41.355C121.808 42.2176 122.506 42.9164 123.367 42.9164C124.229 42.9164 124.927 42.2176 124.927 41.355C124.927 40.4924 124.229 39.7936 123.367 39.7936ZM121.125 41.355C121.125 40.1162 122.129 39.1116 123.367 39.1116C124.606 39.1116 125.609 40.1162 125.609 41.355C125.609 42.5938 124.606 43.5985 123.367 43.5985C122.129 43.5985 121.125 42.5938 121.125 41.355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M160.466 48.3058H100.897V47.6238H160.466V48.3058Z" fill="#171321"/>
+<path d="M173.511 80.7124H152.485V83.4598H173.511V80.7124Z" fill="#D0C5E2"/>
+<path d="M149.08 80.7009H111.703V83.4484H149.08V80.7009Z" fill="#E7E4F2"/>
+<path d="M111.702 68.5293H132.728V65.7819H111.702V68.5293Z" fill="#E7E4F2"/>
+<path d="M136.131 68.5336H173.508V65.7861H136.131V68.5336Z" fill="#AEA5D6"/>
+<path d="M111.703 75.9889H120.838V73.2415H111.703V75.9889Z" fill="#AEA5D6"/>
+<path d="M155.891 75.9978H173.512V73.2504H155.891V75.9978Z" fill="#E7E4F2"/>
+<path d="M124.244 75.9978H152.485V73.2504H124.244V75.9978Z" fill="#D0C5E2"/>
+<path d="M141.099 58.3217H124.332V61.0691H141.099V58.3217Z" fill="#D0C5E2"/>
+<path d="M173.512 58.3217H144.31V61.0691H173.512V58.3217Z" fill="#E7E4F2"/>
+<path d="M120.926 58.3198H111.703V61.0673H120.926V58.3198Z" fill="#AEA5D6"/>
+<path d="M144.115 90.9242H160.882V88.1768H144.115V90.9242Z" fill="#AEA5D6"/>
+<path d="M111.703 90.908H140.905V88.1605H111.703V90.908Z" fill="#D0C5E2"/>
+<path d="M164.288 90.9242H173.512V88.1768H164.288V90.9242Z" fill="#D0C5E2"/>
+<path d="M173.508 95.6178H158.224V98.3652H173.508V95.6178Z" fill="#D0C5E2"/>
+<path d="M154.82 95.6199H111.703V98.3673H154.82V95.6199Z" fill="#E7E4F2"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M167.801 34.8965L168.463 35.0598L162.398 59.6461L189.091 57.5112L189.145 58.1911L161.509 60.4014L167.801 34.8965Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M134.795 81.6335L109.428 116.595L108.876 116.195L133.312 82.5167L92.5055 87.8873L92.4165 87.2111L134.795 81.6335Z" fill="#171321"/>
+<path d="M187.019 57.9366C197.646 57.9366 206.262 49.3147 206.262 38.6791C206.262 28.0435 197.646 19.4216 187.019 19.4216C176.392 19.4216 167.777 28.0435 167.777 38.6791C167.777 49.3147 176.392 57.9366 187.019 57.9366Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M187.019 19.6948C176.543 19.6948 168.05 28.1943 168.05 38.6794C168.05 49.1646 176.543 57.6641 187.019 57.6641C197.495 57.6641 205.989 49.1646 205.989 38.6794C205.989 28.1943 197.495 19.6948 187.019 19.6948ZM167.504 38.6794C167.504 27.8934 176.241 19.1492 187.019 19.1492C197.797 19.1492 206.534 27.8934 206.534 38.6794C206.534 49.4655 197.797 58.2097 187.019 58.2097C176.241 58.2097 167.504 49.4655 167.504 38.6794Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M212.255 38.6791C212.255 24.7294 200.956 13.4218 187.018 13.4218V12.677C201.368 12.677 213 24.3186 213 38.6791H212.255Z" fill="#AEA5D6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M67.0944 108.321C67.0944 122.27 78.3936 133.578 92.3316 133.578V134.323C77.9817 134.323 66.3496 122.681 66.3496 108.321H67.0944Z" fill="#AEA5D6"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M181.794 28.4035C183.212 28.0221 184.673 28.8643 185.054 30.2837L184.395 30.4605C184.112 29.4049 183.026 28.7787 181.971 29.0622L181.794 28.4035Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M192.066 29.0622C191.011 28.7787 189.925 29.4049 189.642 30.4605L188.983 30.2837C189.364 28.8643 190.824 28.0221 192.243 28.4035L192.066 29.0622Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M195.892 33.5398V36.1862L192.04 37.4786L191.823 36.832L195.21 35.6956V33.5398H195.892Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M195.213 44.8355L192.045 43.7774L192.262 43.1305L195.895 44.3442V46.991H195.213V44.8355Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M178.827 35.6957V33.5398H178.145V36.1861L181.995 37.4786L182.212 36.832L178.827 35.6957Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M181.776 43.1305L181.992 43.7773L178.827 44.8354V46.991H178.145V44.3443L181.776 43.1305Z" fill="#171321"/>
+<path d="M182.675 34.3942L181.129 37.5941C179.03 41.9415 182.192 46.9908 187.019 46.9908C191.845 46.9908 195.008 41.9415 192.908 37.5941L191.363 34.3942" fill="#A888F4"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M186.678 46.9908V34.3942H187.36V46.9908H186.678Z" fill="#171321"/>
+<path d="M191.25 34.3939H182.788V33.2534C182.788 28.2918 191.25 28.2987 191.25 33.2534V34.3939Z" fill="#7759C1"/>
+<path d="M92.4612 126.064C103.088 126.064 111.704 117.442 111.704 106.806C111.704 96.1706 103.088 87.5487 92.4612 87.5487C81.8339 87.5487 73.2188 96.1706 73.2188 106.806C73.2188 117.442 81.8339 126.064 92.4612 126.064Z" fill="white"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M92.4604 87.89C82.0217 87.89 73.559 96.359 73.559 106.806C73.559 117.254 82.0217 125.723 92.4604 125.723C102.899 125.723 111.362 117.254 111.362 106.806C111.362 96.359 102.899 87.89 92.4604 87.89ZM72.877 106.806C72.877 95.9828 81.6445 87.208 92.4604 87.208C103.276 87.208 112.044 95.9828 112.044 106.806C112.044 117.63 103.276 126.405 92.4604 126.405C81.6445 126.405 72.877 117.63 72.877 106.806Z" fill="#171321"/>
+<path d="M98.2885 105.28H95.9653V101.385C95.9653 99.3302 94.2951 97.6587 92.2419 97.6587C90.1887 97.6587 88.5184 99.3302 88.5184 101.385V105.28H86.1953V101.385C86.1953 98.0489 88.9083 95.3337 92.2419 95.3337C95.5754 95.3337 98.2885 98.0489 98.2885 101.385V105.28Z" fill="#7759C1"/>
+<path d="M100.894 104.186H83.3677V116.836H100.894V104.186Z" fill="#A888F4"/>
+<path d="M92.1343 111.015C92.8235 111.015 93.3823 110.456 93.3823 109.766C93.3823 109.076 92.8235 108.517 92.1343 108.517C91.445 108.517 90.8862 109.076 90.8862 109.766C90.8862 110.456 91.445 111.015 92.1343 111.015Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M91.792 112.209V110.722H92.474V112.209H91.792Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M87.2401 53.2567V60.9103C87.2401 61.9966 88.1212 62.878 89.2061 62.878H121.061C122.523 62.878 123.709 64.0653 123.709 65.5278V67.1601H123.027V65.5278C123.027 64.4415 122.146 63.5601 121.061 63.5601H89.2061C87.7441 63.5601 86.5581 62.3728 86.5581 60.9103V53.2567H87.2401Z" fill="#171321"/>
+<path d="M123.368 67.735C123.685 67.735 123.942 67.4776 123.942 67.1601C123.942 66.8426 123.685 66.5852 123.368 66.5852C123.051 66.5852 122.793 66.8426 122.793 67.1601C122.793 67.4776 123.051 67.735 123.368 67.735Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M123.368 66.9262C123.239 66.9262 123.134 67.0306 123.134 67.16C123.134 67.2895 123.239 67.3939 123.368 67.3939C123.496 67.3939 123.601 67.2895 123.601 67.16C123.601 67.0306 123.496 66.9262 123.368 66.9262ZM122.452 67.16C122.452 66.6545 122.862 66.2441 123.368 66.2441C123.873 66.2441 124.283 66.6545 124.283 67.16C124.283 67.6656 123.873 68.0759 123.368 68.0759C122.862 68.0759 122.452 67.6656 122.452 67.16Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M167.208 82.5551V84.88C167.208 85.5854 167.778 86.1551 168.482 86.1551H193.305V86.8371H168.482C167.4 86.8371 166.526 85.9616 166.526 84.88V82.5551H167.208Z" fill="#171321"/>
+<path d="M166.867 83.13C167.184 83.13 167.441 82.8726 167.441 82.5551C167.441 82.2376 167.184 81.9802 166.867 81.9802C166.55 81.9802 166.292 82.2376 166.292 82.5551C166.292 82.8726 166.55 83.13 166.867 83.13Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M166.866 82.3209C166.737 82.3209 166.633 82.4254 166.633 82.5548C166.633 82.6842 166.737 82.7887 166.866 82.7887C166.995 82.7887 167.1 82.6842 167.1 82.5548C167.1 82.4255 166.995 82.3209 166.866 82.3209ZM165.951 82.5548C165.951 82.0492 166.36 81.6389 166.866 81.6389C167.372 81.6389 167.782 82.0492 167.782 82.5548C167.782 83.0605 167.372 83.4707 166.866 83.4707C166.36 83.4707 165.951 83.0604 165.951 82.5548Z" fill="#171321"/>
+<path d="M86.9023 37.1553C82.8536 41.5743 77.6099 40.6 77.6099 40.6V48.5768C77.6099 49.962 77.8867 51.3404 78.4796 52.5917C80.8973 57.6918 86.9023 59.5873 86.9023 59.5873C86.9023 59.5873 92.9051 57.6918 95.3251 52.5917C95.918 51.3404 96.1948 49.962 96.1948 48.5768V40.6C96.1948 40.6 90.9511 41.5743 86.9023 37.1553V37.1553Z" fill="#10B1B1"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M90.6411 45.6638L85.4674 50.8415L83.1592 48.5289L83.6419 48.0471L85.4677 49.8764L90.1586 45.1818L90.6411 45.6638Z" fill="#171321"/>
+<path d="M197.955 76.3168C193.902 80.7427 188.651 79.7684 188.651 79.7684V87.7567C188.651 89.1443 188.928 90.5249 189.521 91.7786C191.943 96.8856 197.955 98.7834 197.955 98.7834C197.955 98.7834 203.967 96.8856 206.39 91.7786C206.985 90.5249 207.259 89.1443 207.259 87.7567V79.7684C207.259 79.7684 202.009 80.7427 197.955 76.3168Z" fill="#FC6D26"/>
+<path d="M197.19 83.1044L197.566 88.4584H198.343L198.719 83.1044H197.19Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M196.806 82.7468H199.102L198.676 88.8156H197.232L196.806 82.7468ZM197.572 83.4616L197.898 88.1009H198.009L198.335 83.4616H197.572Z" fill="#171321"/>
+<path d="M198.343 90.2083H197.566V91.1248H198.343V90.2083Z" fill="#171321"/>
+<path fill-rule="evenodd" clip-rule="evenodd" d="M197.208 89.851H198.7V91.4823H197.208V89.851ZM197.922 90.5657V90.7675H197.985V90.5657H197.922Z" fill="#171321"/>
+</g>
+<defs>
+<filter id="filter0_b_187_122567" x="-50" y="-50" width="380" height="230" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feGaussianBlur in="BackgroundImageFix" stdDeviation="25"/>
+<feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_187_122567"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_backgroundBlur_187_122567" result="shape"/>
+</filter>
+<radialGradient id="paint0_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(189.5 -42.5) rotate(89.5818) scale(125.986)">
+<stop stop-color="#7759C2"/>
+<stop offset="1" stop-color="#7759C2" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint1_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(-41.5 -97.5) rotate(89.5818) scale(190.176)">
+<stop stop-color="#D64028"/>
+<stop offset="1" stop-color="#D64028" stop-opacity="0"/>
+</radialGradient>
+<radialGradient id="paint2_radial_187_122567" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(309.5 -7.5) rotate(89.5818) scale(116.405)">
+<stop stop-color="#EF76F1"/>
+<stop offset="1" stop-color="#EF76F1" stop-opacity="0"/>
+</radialGradient>
+<clipPath id="clip0_187_122567">
+<rect width="280" height="140" fill="white"/>
+</clipPath>
+<clipPath id="clip1_187_122567">
+<path d="M0 4C0 1.79086 1.79086 0 4 0H276C278.209 0 280 1.79086 280 4V140H0V4Z" fill="white"/>
+</clipPath>
+</defs>
+</svg>
diff --git a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
index 80c216024a0..8e814cd55ef 100644
--- a/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
+++ b/app/assets/javascripts/admin/background_migrations/components/database_listbox.vue
@@ -1,5 +1,5 @@
<script>
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { s__ } from '~/locale';
import { setUrlParams, visitUrl } from '~/lib/utils/url_utility';
@@ -9,7 +9,7 @@ export default {
database: s__('BackgroundMigrations|Database'),
},
components: {
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
databases: {
@@ -39,7 +39,7 @@ export default {
<label id="label" class="gl-font-weight-bold gl-mr-4 gl-mb-0">{{
$options.i18n.database
}}</label>
- <gl-listbox
+ <gl-collapsible-listbox
v-model="selected"
:items="databases"
right
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/base.vue b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
index b7bafe46327..f869d21d55f 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/base.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/base.vue
@@ -5,14 +5,18 @@ import { buildUrlWithCurrentLocation } from '~/lib/utils/common_utils';
import { createAlert, VARIANT_DANGER } from '~/flash';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { NEW_BROADCAST_MESSAGE } from '../constants';
+import MessageForm from './message_form.vue';
import MessagesTable from './messages_table.vue';
const PER_PAGE = 20;
export default {
name: 'BroadcastMessagesBase',
+ NEW_BROADCAST_MESSAGE,
components: {
GlPagination,
+ MessageForm,
MessagesTable,
},
@@ -97,6 +101,7 @@ export default {
<template>
<div>
+ <message-form :broadcast-message="$options.NEW_BROADCAST_MESSAGE" />
<messages-table
v-if="hasVisibleMessages"
:messages="visibleMessages"
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
new file mode 100644
index 00000000000..07814ef2511
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/datetime_picker.vue
@@ -0,0 +1,47 @@
+<script>
+import { GlDatepicker, GlFormInput } from '@gitlab/ui';
+import { dateToTimeInputValue, timeToHoursMinutes } from '~/lib/utils/datetime/date_format_utility';
+
+export default {
+ name: 'DatetimePicker',
+ components: {
+ GlDatepicker,
+ GlFormInput,
+ },
+ props: {
+ value: {
+ type: Date,
+ required: true,
+ },
+ },
+ computed: {
+ date: {
+ get() {
+ return this.value;
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ dup.setFullYear(val.getFullYear(), val.getMonth(), val.getDate());
+ this.$emit('input', dup);
+ },
+ },
+ time: {
+ get() {
+ return dateToTimeInputValue(this.value);
+ },
+ set(val) {
+ const dup = new Date(this.value.getTime());
+ const { hours, minutes } = timeToHoursMinutes(val);
+ dup.setHours(hours, minutes);
+ this.$emit('input', dup);
+ },
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-gap-3 gl-align-items-center">
+ <gl-datepicker v-model="date" />
+ <gl-form-input v-model="time" size="sm" type="time" data-testid="time-picker" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
new file mode 100644
index 00000000000..36796708e78
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form.vue
@@ -0,0 +1,225 @@
+<script>
+import {
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+} from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+import { createAlert, VARIANT_DANGER } from '~/flash';
+import { redirectTo } from '~/lib/utils/url_utility';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { BROADCAST_MESSAGES_PATH, THEMES, TYPES, TYPE_BANNER } from '../constants';
+import MessageFormGroup from './message_form_group.vue';
+import DatetimePicker from './datetime_picker.vue';
+
+const FORM_HEADERS = { headers: { 'Content-Type': 'application/json; charset=utf-8' } };
+
+export default {
+ name: 'MessageForm',
+ components: {
+ DatetimePicker,
+ GlButton,
+ GlBroadcastMessage,
+ GlForm,
+ GlFormCheckbox,
+ GlFormCheckboxGroup,
+ GlFormInput,
+ GlFormSelect,
+ GlFormText,
+ GlFormTextarea,
+ MessageFormGroup,
+ },
+ mixins: [glFeatureFlagsMixin()],
+ inject: ['targetAccessLevelOptions'],
+ i18n: {
+ message: s__('BroadcastMessages|Message'),
+ messagePlaceholder: s__('BroadcastMessages|Your message here'),
+ type: s__('BroadcastMessages|Type'),
+ theme: s__('BroadcastMessages|Theme'),
+ dismissable: s__('BroadcastMessages|Dismissable'),
+ dismissableDescription: s__('BroadcastMessages|Allow users to dismiss the broadcast message'),
+ targetRoles: s__('BroadcastMessages|Target roles'),
+ targetRolesDescription: s__(
+ 'BroadcastMessages|The broadcast message displays only to users in projects and groups who have these roles.',
+ ),
+ targetPath: s__('BroadcastMessages|Target Path'),
+ targetPathDescription: s__('BroadcastMessages|Paths can contain wildcards, like */welcome'),
+ startsAt: s__('BroadcastMessages|Starts at'),
+ endsAt: s__('BroadcastMessages|Ends at'),
+ add: s__('BroadcastMessages|Add broadcast message'),
+ addError: s__('BroadcastMessages|There was an error adding broadcast message.'),
+ update: s__('BroadcastMessages|Update broadcast message'),
+ updateError: s__('BroadcastMessages|There was an error updating broadcast message.'),
+ },
+ messageThemes: THEMES,
+ messageTypes: TYPES,
+ props: {
+ broadcastMessage: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ message: this.broadcastMessage.message,
+ type: this.broadcastMessage.broadcastType,
+ theme: this.broadcastMessage.theme,
+ dismissable: this.broadcastMessage.dismissable || false,
+ targetPath: this.broadcastMessage.targetPath,
+ targetAccessLevels: this.broadcastMessage.targetAccessLevels,
+ targetAccessLevelOptions: this.targetAccessLevelOptions.map(([text, value]) => ({
+ text,
+ value,
+ })),
+ startsAt: new Date(this.broadcastMessage.startsAt.getTime()),
+ endsAt: new Date(this.broadcastMessage.endsAt.getTime()),
+ };
+ },
+ computed: {
+ isBanner() {
+ return this.type === TYPE_BANNER;
+ },
+ messageBlank() {
+ return this.message.trim() === '';
+ },
+ messagePreview() {
+ return this.messageBlank ? this.$options.i18n.messagePlaceholder : this.message;
+ },
+ isAddForm() {
+ return !this.broadcastMessage.id;
+ },
+ formPath() {
+ return this.isAddForm
+ ? BROADCAST_MESSAGES_PATH
+ : `${BROADCAST_MESSAGES_PATH}/${this.broadcastMessage.id}`;
+ },
+ formPayload() {
+ return JSON.stringify({
+ message: this.message,
+ broadcast_type: this.type,
+ theme: this.theme,
+ dismissable: this.dismissable,
+ target_path: this.targetPath,
+ target_access_levels: this.targetAccessLevels,
+ starts_at: this.startsAt.toISOString(),
+ ends_at: this.endsAt.toISOString(),
+ });
+ },
+ },
+ methods: {
+ async onSubmit() {
+ this.loading = true;
+
+ const success = await this.submitForm();
+ if (success) {
+ redirectTo(BROADCAST_MESSAGES_PATH);
+ } else {
+ this.loading = false;
+ }
+ },
+
+ async submitForm() {
+ const requestMethod = this.isAddForm ? 'post' : 'patch';
+
+ try {
+ await axios[requestMethod](this.formPath, this.formPayload, FORM_HEADERS);
+ } catch (e) {
+ const message = this.isAddForm
+ ? this.$options.i18n.addError
+ : this.$options.i18n.updateError;
+ createAlert({ message, variant: VARIANT_DANGER });
+ return false;
+ }
+ return true;
+ },
+ },
+};
+</script>
+<template>
+ <gl-form @submit.prevent="onSubmit">
+ <gl-broadcast-message class="gl-my-6" :type="type" :theme="theme" :dismissible="dismissable">
+ {{ messagePreview }}
+ </gl-broadcast-message>
+
+ <message-form-group :label="$options.i18n.message" label-for="message-textarea">
+ <gl-form-textarea
+ id="message-textarea"
+ v-model="message"
+ size="sm"
+ :placeholder="$options.i18n.messagePlaceholder"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.type" label-for="type-select">
+ <gl-form-select id="type-select" v-model="type" :options="$options.messageTypes" />
+ </message-form-group>
+
+ <template v-if="isBanner">
+ <message-form-group :label="$options.i18n.theme" label-for="theme-select">
+ <gl-form-select
+ id="theme-select"
+ v-model="theme"
+ :options="$options.messageThemes"
+ data-testid="theme-select"
+ />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.dismissable" label-for="dismissable-checkbox">
+ <gl-form-checkbox
+ id="dismissable-checkbox"
+ v-model="dismissable"
+ class="gl-mt-3"
+ data-testid="dismissable-checkbox"
+ >
+ <span>{{ $options.i18n.dismissableDescription }}</span>
+ </gl-form-checkbox>
+ </message-form-group>
+ </template>
+
+ <message-form-group
+ v-if="glFeatures.roleTargetedBroadcastMessages"
+ :label="$options.i18n.targetRoles"
+ data-testid="target-roles-checkboxes"
+ >
+ <gl-form-checkbox-group v-model="targetAccessLevels" :options="targetAccessLevelOptions" />
+ <gl-form-text>
+ {{ $options.i18n.targetRolesDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.targetPath" label-for="target-path-input">
+ <gl-form-input id="target-path-input" v-model="targetPath" />
+ <gl-form-text>
+ {{ $options.i18n.targetPathDescription }}
+ </gl-form-text>
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.startsAt">
+ <datetime-picker v-model="startsAt" />
+ </message-form-group>
+
+ <message-form-group :label="$options.i18n.endsAt">
+ <datetime-picker v-model="endsAt" />
+ </message-form-group>
+
+ <div class="form-actions gl-mb-3">
+ <gl-button
+ type="submit"
+ variant="confirm"
+ :loading="loading"
+ :disabled="messageBlank"
+ data-testid="submit-button"
+ >
+ {{ isAddForm ? $options.i18n.add : $options.i18n.update }}
+ </gl-button>
+ </div>
+ </gl-form>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
new file mode 100644
index 00000000000..eec51c0c28b
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/components/message_form_group.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlFormGroup } from '@gitlab/ui';
+
+export default {
+ name: 'MessageFormGroup',
+ components: {
+ GlFormGroup,
+ },
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ labelFor: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-form-group
+ :label="label"
+ :label-for="labelFor"
+ label-cols-sm="2"
+ label-class="gl-mt-3"
+ label-align-sm="right"
+ >
+ <slot></slot>
+ </gl-form-group>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
index 1408312d3e4..a523dd3b391 100644
--- a/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
+++ b/app/assets/javascripts/admin/broadcast_messages/components/messages_table.vue
@@ -1,6 +1,8 @@
<script>
-import { GlButton, GlTableLite, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlTableLite } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
const DEFAULT_TD_CLASSES = 'gl-vertical-align-middle!';
@@ -12,7 +14,7 @@ export default {
GlTableLite,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
i18n: {
@@ -77,6 +79,11 @@ export default {
safeHtmlConfig: {
ADD_TAGS: ['use'],
},
+ methods: {
+ formatDate(dateString) {
+ return formatDate(new Date(dateString));
+ },
+ },
};
</script>
<template>
@@ -90,6 +97,14 @@ export default {
<div v-safe-html:[$options.safeHtmlConfig]="preview"></div>
</template>
+ <template #cell(starts_at)="{ item: { starts_at } }">
+ {{ formatDate(starts_at) }}
+ </template>
+
+ <template #cell(ends_at)="{ item: { ends_at } }">
+ {{ formatDate(ends_at) }}
+ </template>
+
<template #cell(buttons)="{ item: { id, edit_path, disable_delete } }">
<gl-button
icon="pencil"
diff --git a/app/assets/javascripts/admin/broadcast_messages/constants.js b/app/assets/javascripts/admin/broadcast_messages/constants.js
new file mode 100644
index 00000000000..6250d5a943d
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/constants.js
@@ -0,0 +1,35 @@
+import { s__ } from '~/locale';
+
+export const BROADCAST_MESSAGES_PATH = '/admin/broadcast_messages';
+
+export const TYPE_BANNER = 'banner';
+export const TYPE_NOTIFICATION = 'notification';
+
+export const TYPES = [
+ { value: TYPE_BANNER, text: s__('BroadcastMessages|Banner') },
+ { value: TYPE_NOTIFICATION, text: s__('BroadcastMessages|Notification') },
+];
+
+export const THEMES = [
+ { value: 'indigo', text: s__('BroadcastMessages|Indigo') },
+ { value: 'light-indigo', text: s__('BroadcastMessages|Light Indigo') },
+ { value: 'blue', text: s__('BroadcastMessages|Blue') },
+ { value: 'light-blue', text: s__('BroadcastMessages|Light Blue') },
+ { value: 'green', text: s__('BroadcastMessages|Green') },
+ { value: 'light-green', text: s__('BroadcastMessages|Light Green') },
+ { value: 'red', text: s__('BroadcastMessages|Red') },
+ { value: 'light-red', text: s__('BroadcastMessages|Light Red') },
+ { value: 'dark', text: s__('BroadcastMessages|Dark') },
+ { value: 'light', text: s__('BroadcastMessages|Light') },
+];
+
+export const NEW_BROADCAST_MESSAGE = {
+ message: '',
+ broadcastType: TYPES[0].value,
+ theme: THEMES[0].value,
+ dismissable: false,
+ targetPath: '',
+ targetAccessLevels: [],
+ startsAt: new Date(),
+ endsAt: new Date(),
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/edit.js b/app/assets/javascripts/admin/broadcast_messages/edit.js
new file mode 100644
index 00000000000..70a270f7a56
--- /dev/null
+++ b/app/assets/javascripts/admin/broadcast_messages/edit.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import MessageForm from './components/message_form.vue';
+
+export default () => {
+ const el = document.querySelector('#js-broadcast-message');
+ const {
+ id,
+ message,
+ broadcastType,
+ theme,
+ dismissable,
+ targetAccessLevels,
+ targetAccessLevelOptions,
+ targetPath,
+ startsAt,
+ endsAt,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'EditBroadcastMessage',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
+ render(createElement) {
+ return createElement(MessageForm, {
+ props: {
+ broadcastMessage: {
+ id: parseInt(id, 10),
+ message,
+ broadcastType,
+ theme,
+ dismissable: dismissable === 'true',
+ targetAccessLevels: JSON.parse(targetAccessLevels),
+ targetPath,
+ startsAt: new Date(startsAt),
+ endsAt: new Date(endsAt),
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/admin/broadcast_messages/index.js b/app/assets/javascripts/admin/broadcast_messages/index.js
index 81952d2033e..fd8b2aad4ec 100644
--- a/app/assets/javascripts/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/admin/broadcast_messages/index.js
@@ -3,11 +3,14 @@ import BroadcastMessagesBase from './components/base.vue';
export default () => {
const el = document.querySelector('#js-broadcast-messages');
- const { page, messagesCount, messages } = el.dataset;
+ const { page, targetAccessLevelOptions, messagesCount, messages } = el.dataset;
return new Vue({
el,
name: 'BroadcastMessages',
+ provide: {
+ targetAccessLevelOptions: JSON.parse(targetAccessLevelOptions),
+ },
render(createElement) {
return createElement(BroadcastMessagesBase, {
props: {
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 c0cac958a42..5229d4c9ae2 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_table.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_table.vue
@@ -17,6 +17,7 @@ import { fetchPolicies } from '~/lib/graphql';
import { joinPaths, visitUrl } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
import AlertStatus from '~/vue_shared/alert_details/components/alert_status.vue';
+import { TOKEN_TYPE_ASSIGNEE } from '~/vue_shared/components/filtered_search_bar/constants';
import {
tdClass,
thClass,
@@ -96,6 +97,7 @@ export default {
sortable: true,
},
],
+ filterSearchTokens: [TOKEN_TYPE_ASSIGNEE],
severityLabels: SEVERITY_LEVELS,
statusTabs: ALERTS_STATUS_TABS,
components: {
@@ -294,9 +296,7 @@ export default {
:status-tabs="$options.statusTabs"
:track-views-options="$options.trackAlertListViewsOptions"
:server-error-message="serverErrorMessage"
- :filter-search-tokens="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
- 'assignee_username',
- ] /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :filter-search-tokens="$options.filterSearchTokens"
filter-search-key="alerts"
@page-changed="pageChanged"
@tabs-changed="statusChanged"
@@ -312,6 +312,7 @@ export default {
<template #table>
<gl-table
class="alert-management-table"
+ data-qa-selector="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 388d925196b..a0d5cb7f4c3 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_form.vue
@@ -83,7 +83,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_issue_checkbox">
+ <gl-form-checkbox v-model="createIssueEnabled" data-qa-selector="create_incident_checkbox">
<span>{{ $options.i18n.createIncident.label }}</span>
</gl-form-checkbox>
</gl-form-group>
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 03bc4b825ae..65c3bc732ed 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_form.vue
@@ -430,6 +430,7 @@ export default {
v-model="integrationForm.type"
:disabled="isSelectDisabled"
class="gl-max-w-full"
+ data-qa-selector="integration_type_dropdown"
:options="integrationTypesOptions"
/>
@@ -461,6 +462,7 @@ export default {
v-model="integrationForm.name"
type="text"
:placeholder="$options.i18n.integrationFormSteps.nameIntegration.placeholder"
+ data-qa-selector="integration_name_field"
@input="validateName"
/>
</gl-form-group>
@@ -483,6 +485,7 @@ export default {
v-model="integrationForm.active"
:is-loading="loading"
:label="$options.i18n.integrationFormSteps.nameIntegration.activeToggle"
+ data-qa-selector="active_toggle_container"
class="gl-mt-4 gl-font-weight-normal"
/>
</gl-form-group>
@@ -594,6 +597,7 @@ export default {
category="secondary"
class="gl-ml-3 js-no-auto-disable"
data-testid="integration-form-test-and-submit"
+ data-qa-selector="save_and_create_alert_button"
@click="submit(true)"
>
{{ $options.i18n.saveAndTestIntegration }}
@@ -695,6 +699,7 @@ export default {
:debounce="$options.JSON_VALIDATE_DELAY"
rows="6"
max-rows="10"
+ data-qa-selector="test_payload_field"
@input="validateJson(false)"
/>
</gl-form-group>
@@ -706,6 +711,7 @@ 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 bf456b6adaa..010cb5721a1 100644
--- a/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
+++ b/app/assets/javascripts/alerts_settings/components/alerts_settings_wrapper.vue
@@ -375,6 +375,7 @@ export default {
category="secondary"
variant="confirm"
data-testid="add-integration-btn"
+ data-qa-selector="add_integration_button"
class="gl-mt-3"
@click="setFormVisibility(true)"
>
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
index f06544f50c6..a688e2f497b 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/base.vue
@@ -5,9 +5,9 @@ import { getCookie, setCookie } from '~/lib/utils/common_utils';
import ValueStreamMetrics from '~/analytics/shared/components/value_stream_metrics.vue';
import { VSA_METRICS_GROUPS } from '~/analytics/shared/constants';
import { toYmd } from '~/analytics/shared/utils';
-import PathNavigation from '~/cycle_analytics/components/path_navigation.vue';
-import StageTable from '~/cycle_analytics/components/stage_table.vue';
-import ValueStreamFilters from '~/cycle_analytics/components/value_stream_filters.vue';
+import PathNavigation from '~/analytics/cycle_analytics/components/path_navigation.vue';
+import StageTable from '~/analytics/cycle_analytics/components/stage_table.vue';
+import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
import { __ } from '~/locale';
import { SUMMARY_METRICS_REQUEST, METRICS_REQUESTS } from '../constants';
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
index 0ad325a8523..54b632968e2 100644
--- a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/filter_bar.vue
@@ -1,12 +1,16 @@
<script>
import { mapActions, mapState } from 'vuex';
import {
- OPERATOR_IS_ONLY,
- DEFAULT_NONE_ANY,
+ OPERATORS_IS,
+ OPTIONS_NONE_ANY,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_LABEL,
TOKEN_TITLE_MILESTONE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import {
@@ -14,7 +18,7 @@ import {
processFilters,
filterToQueryObject,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
@@ -47,45 +51,45 @@ export default {
{
icon: 'clock',
title: TOKEN_TITLE_MILESTONE,
- type: 'milestone',
+ type: TOKEN_TYPE_MILESTONE,
token: MilestoneToken,
initialMilestones: this.milestonesData,
unique: true,
symbol: '%',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchMilestones: this.fetchMilestones,
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
- type: 'labels',
+ type: TOKEN_TYPE_LABEL,
token: LabelToken,
- defaultLabels: DEFAULT_NONE_ANY,
+ defaultLabels: OPTIONS_NONE_ANY,
initialLabels: this.labelsData,
unique: false,
symbol: '~',
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
fetchLabels: this.fetchLabels,
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
- type: 'author',
- token: AuthorToken,
- initialAuthors: this.authorsData,
+ type: TOKEN_TYPE_AUTHOR,
+ token: UserToken,
+ initialUsers: this.authorsData,
unique: true,
- operators: OPERATOR_IS_ONLY,
- fetchAuthors: this.fetchAuthors,
+ operators: OPERATORS_IS,
+ fetchUsers: this.fetchAuthors,
},
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
- type: 'assignees',
- token: AuthorToken,
- initialAuthors: this.assigneesData,
+ type: TOKEN_TYPE_ASSIGNEE,
+ token: UserToken,
+ initialUsers: this.assigneesData,
unique: false,
- operators: OPERATOR_IS_ONLY,
- fetchAuthors: this.fetchAssignees,
+ operators: OPERATORS_IS,
+ fetchUsers: this.fetchAssignees,
},
];
},
@@ -108,14 +112,19 @@ export default {
]),
initialFilterValue() {
return prepareTokens({
- milestone: this.selectedMilestone,
- author: this.selectedAuthor,
- assignees: this.selectedAssigneeList,
- labels: this.selectedLabelList,
+ [TOKEN_TYPE_MILESTONE]: this.selectedMilestone,
+ [TOKEN_TYPE_AUTHOR]: this.selectedAuthor,
+ [TOKEN_TYPE_ASSIGNEE]: this.selectedAssigneeList,
+ [TOKEN_TYPE_LABEL]: this.selectedLabelList,
});
},
handleFilter(filters) {
- const { labels, milestone, author, assignees } = processFilters(filters);
+ const {
+ [TOKEN_TYPE_LABEL]: labels,
+ [TOKEN_TYPE_MILESTONE]: milestone,
+ [TOKEN_TYPE_AUTHOR]: author,
+ [TOKEN_TYPE_ASSIGNEE]: assignees,
+ } = processFilters(filters);
this.setFilters({
selectedAuthor: author ? author[0] : null,
diff --git a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
index b622b0441e2..b622b0441e2 100644
--- a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/formatted_stage_count.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/metric_tile.vue b/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
index a5c20b237b3..a5c20b237b3 100644
--- a/app/assets/javascripts/cycle_analytics/components/metric_tile.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/metric_tile.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue
index 72a7659aac0..ac41bc4917c 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/path_navigation.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPath, GlPopover, GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlPath, GlPopover, GlSkeletonLoader } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { OVERVIEW_STAGE_ID } from '../constants';
import FormattedStageCount from './formatted_stage_count.vue';
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
index f1fdffd4b72..78ac29426d9 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/stage_table.vue
@@ -8,7 +8,7 @@ import {
GlTable,
GlBadge,
} from '@gitlab/ui';
-import FormattedStageCount from '~/cycle_analytics/components/formatted_stage_count.vue';
+import FormattedStageCount from '~/analytics/cycle_analytics/components/formatted_stage_count.vue';
import { __ } from '~/locale';
import Tracking from '~/tracking';
import {
diff --git a/app/assets/javascripts/cycle_analytics/components/total_time.vue b/app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue
index 725952c3518..725952c3518 100644
--- a/app/assets/javascripts/cycle_analytics/components/total_time.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/total_time.vue
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
index 17decb6b448..17decb6b448 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/analytics/cycle_analytics/components/value_stream_filters.vue
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/analytics/cycle_analytics/constants.js
index 2758d686fb1..2758d686fb1 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/constants.js
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/analytics/cycle_analytics/index.js
index 3da8696edeb..df161f7e563 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/index.js
@@ -3,7 +3,7 @@ import {
extractFilterQueryParameters,
extractPaginationQueryParameters,
} from '~/analytics/shared/utils';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
import CycleAnalytics from './components/base.vue';
import createStore from './store';
import { buildCycleAnalyticsInitialData } from './utils';
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
index 4a201e00582..4a201e00582 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/actions.js
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
index 83068cabf0f..83068cabf0f 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/getters.js
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/analytics/cycle_analytics/store/index.js
index 76e3e835016..76e3e835016 100644
--- a/app/assets/javascripts/cycle_analytics/store/index.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/index.js
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
index 9376d81f317..9376d81f317 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutation_types.js
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
index 8567529caf2..8567529caf2 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/mutations.js
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
index 8d662333afa..00dd2e53883 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/store/state.js
@@ -1,7 +1,7 @@
import {
PAGINATION_SORT_FIELD_END_EVENT,
PAGINATION_SORT_DIRECTION_DESC,
-} from '~/cycle_analytics/constants';
+} from '~/analytics/cycle_analytics/constants';
export default () => ({
id: null,
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/analytics/cycle_analytics/utils.js
index 428bb11b950..428bb11b950 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/analytics/cycle_analytics/utils.js
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js
index 15457f28eff..66ed30130bb 100644
--- a/app/assets/javascripts/api/analytics_api.js
+++ b/app/assets/javascripts/api/analytics_api.js
@@ -7,6 +7,11 @@ const PROJECT_VSA_PATH_BASE = '/:request_path/-/analytics/value_stream_analytics
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
const PROJECT_VSA_STAGE_DATA_PATH = `${PROJECT_VSA_STAGES_PATH}/:stage_id`;
+export const LEAD_TIME_METRIC_TYPE = 'lead_time';
+export const CYCLE_TIME_METRIC_TYPE = 'cycle_time';
+export const ISSUES_METRIC_TYPE = 'issues';
+export const DEPLOYS_METRIC_TYPE = 'deploys';
+
export const METRIC_TYPE_SUMMARY = 'summary';
export const METRIC_TYPE_TIME_SUMMARY = 'time_summary';
diff --git a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
index 89a24d7891e..9777153999e 100644
--- a/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
+++ b/app/assets/javascripts/artifacts/graphql/queries/get_job_artifacts.query.graphql
@@ -10,6 +10,7 @@ query getJobArtifacts(
project(fullPath: $projectPath) {
id
jobs(
+ withArtifacts: true
statuses: [SUCCESS, FAILED]
first: $firstPageSize
last: $lastPageSize
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9ab1d6bfd80..1855fb9ed8c 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,7 +2,7 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { uniq } from 'lodash';
+import { uniq, escape } from 'lodash';
import { getEmojiScoreWithIntent } from '~/emoji/utils';
import { getCookie, setCookie, scrollToElement } from '~/lib/utils/common_utils';
import * as Emoji from '~/emoji';
@@ -149,7 +149,7 @@ export class AwardsHandler {
let frequentlyUsedCatgegory = '';
if (frequentlyUsedEmojis.length > 0) {
frequentlyUsedCatgegory = this.renderCategory('Frequently used', frequentlyUsedEmojis, {
- menuListClass: 'frequent-emojis',
+ frequentEmojis: true,
});
}
@@ -228,9 +228,9 @@ export class AwardsHandler {
renderCategory(name, emojiList, opts = {}) {
return `
<h5 class="emoji-menu-title">
- ${name}
+ ${escape(name)}
</h5>
- <ul class="clearfix emoji-menu-list ${opts.menuListClass || ''}">
+ <ul class="clearfix emoji-menu-list ${opts.frequentEmojis ? 'frequent-emojis' : ''}">
${emojiList
.map(
(emojiName) => `
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index f68666f8a0c..c95c90d5daf 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -1,10 +1,12 @@
<script>
-import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import { escape, debounce } from 'lodash';
import { mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert, VARIANT_INFO } from '~/flash';
import { s__, sprintf } from '~/locale';
import createEmptyBadge from '../empty_badge';
+import { PLACEHOLDERS } from '../constants';
import Badge from './badge.vue';
const badgePreviewDelayInMilliseconds = 1500;
@@ -19,7 +21,7 @@ export default {
GlFormGroup,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
isEditing: {
@@ -49,9 +51,9 @@ export default {
return this.badgeInAddForm;
},
helpText() {
- const placeholders = ['project_path', 'project_id', 'default_branch', 'commit_sha']
- .map((placeholder) => `<code>%{${placeholder}}</code>`)
- .join(', ');
+ const placeholders = PLACEHOLDERS.map((placeholder) => `<code>%{${placeholder}}</code>`).join(
+ ', ',
+ );
return sprintf(
s__('Badges|Supported %{docsLinkStart}variables%{docsLinkEnd}: %{placeholders}'),
{
diff --git a/app/assets/javascripts/badges/constants.js b/app/assets/javascripts/badges/constants.js
index 8fbe3db5ef1..709436abca6 100644
--- a/app/assets/javascripts/badges/constants.js
+++ b/app/assets/javascripts/badges/constants.js
@@ -1,2 +1,10 @@
export const GROUP_BADGE = 'group';
export const PROJECT_BADGE = 'project';
+export const PLACEHOLDERS = [
+ 'project_path',
+ 'project_title',
+ 'project_name',
+ 'project_id',
+ 'default_branch',
+ 'commit_sha',
+];
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index e5408d0734a..5bb310afac7 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -1,6 +1,7 @@
<script>
-import { GlButton, GlSafeHtmlDirective, GlBadge } from '@gitlab/ui';
+import { GlButton, GlBadge } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteableNote from '~/notes/components/noteable_note.vue';
import PublishButton from './publish_button.vue';
@@ -13,7 +14,7 @@ export default {
GlBadge,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -84,32 +85,25 @@ export default {
};
</script>
<template>
- <article
- class="draft-note-component note-wrapper"
- @mouseenter="handleMouseEnter(draft)"
- @mouseleave="handleMouseLeave(draft)"
+ <noteable-note
+ :note="draft"
+ :line="line"
+ :discussion-root="true"
+ :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }"
+ class="draft-note-component draft-note"
+ @handleEdit="handleEditing"
+ @cancelForm="handleNotEditing"
+ @updateSuccess="handleNotEditing"
+ @handleDeleteNote="deleteDraft"
+ @handleUpdateNote="update"
+ @toggleResolveStatus="toggleResolveDiscussion(draft.id)"
+ @mouseenter.native="handleMouseEnter(draft)"
+ @mouseleave.native="handleMouseLeave(draft)"
>
- <ul class="notes draft-notes">
- <noteable-note
- :note="draft"
- :line="line"
- :discussion-root="true"
- :class="{ 'gl-mb-0!': glFeatures.mrReviewSubmitComment }"
- class="draft-note"
- @handleEdit="handleEditing"
- @cancelForm="handleNotEditing"
- @updateSuccess="handleNotEditing"
- @handleDeleteNote="deleteDraft"
- @handleUpdateNote="update"
- @toggleResolveStatus="toggleResolveDiscussion(draft.id)"
- >
- <template #note-header-info>
- <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge>
- </template>
- </noteable-note>
- </ul>
-
- <template v-if="!isEditingDraft">
+ <template #note-header-info>
+ <gl-badge variant="warning" class="gl-mr-2">{{ __('Pending') }}</gl-badge>
+ </template>
+ <template v-if="!isEditingDraft" #after-note-body>
<div
v-if="draftCommands"
v-safe-html:[$options.safeHtmlConfig]="draftCommands"
@@ -133,5 +127,5 @@ export default {
</gl-button>
</p>
</template>
- </article>
+ </noteable-note>
</template>
diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js
index ae186aba32d..0c81ae63f21 100644
--- a/app/assets/javascripts/behaviors/copy_code.js
+++ b/app/assets/javascripts/behaviors/copy_code.js
@@ -7,7 +7,10 @@ class CopyCodeButton extends HTMLElement {
connectedCallback() {
this.for = uniqueId('code-');
- this.parentNode.querySelector('pre').setAttribute('id', this.for);
+ const target = this.parentNode.querySelector('pre');
+ if (!target) return;
+
+ target.setAttribute('id', this.for);
this.appendChild(this.createButton());
}
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 30160248a77..220064e6673 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -1,6 +1,5 @@
import $ from 'jquery';
import './autosize';
-import './markdown/render_gfm';
import initCollapseSidebarOnWindowResize from './collapse_sidebar_on_window_resize';
import initCopyToClipboard from './copy_to_clipboard';
import installGlEmojiElement from './gl_emoji';
diff --git a/app/assets/javascripts/behaviors/markdown/init_gfm.js b/app/assets/javascripts/behaviors/markdown/init_gfm.js
new file mode 100644
index 00000000000..d9c7cee50da
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/init_gfm.js
@@ -0,0 +1,13 @@
+import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+$.fn.renderGFM = function plugin() {
+ this.get().forEach(renderGFM);
+ return this;
+};
+requestIdleCallback(
+ () => {
+ renderGFM(document.body);
+ },
+ { timeout: 500 },
+);
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index a08cf48c327..2eab5b84e3e 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -1,45 +1,52 @@
-import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import highlightCurrentUser from './highlight_current_user';
import { renderKroki } from './render_kroki';
import renderMath from './render_math';
import renderSandboxedMermaid from './render_sandboxed_mermaid';
import renderMetrics from './render_metrics';
+import renderObservability from './render_observability';
import { renderJSONTable } from './render_json_table';
-// Render GitLab flavoured Markdown
-//
-// Delegates to syntax highlight and render math & mermaid diagrams.
-//
-$.fn.renderGFM = function renderGFM() {
- syntaxHighlight(this.find('.js-syntax-highlight').get());
- renderKroki(this.find('.js-render-kroki[hidden]').get());
- renderMath(this.find('.js-render-math'));
- renderSandboxedMermaid(this.find('.js-render-mermaid').get());
- renderJSONTable(
- Array.from(this.find('[lang="json"][data-lang-params="table"]').get()).map((e) => e.parentNode),
- );
-
- highlightCurrentUser(this.find('.gfm-project_member').get());
+function initPopovers(elements) {
+ if (!elements.length) return;
+ import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
+ .then(({ default: initIssuablePopovers }) => {
+ initIssuablePopovers(elements);
+ })
+ .catch(() => {});
+}
- const issuablePopoverElements = this.find('.gfm-issue, .gfm-merge_request').get();
- if (issuablePopoverElements.length) {
- import(/* webpackChunkName: 'IssuablePopoverBundle' */ '~/issuable/popover')
- .then(({ default: initIssuablePopovers }) => {
- initIssuablePopovers(issuablePopoverElements);
- })
- .catch(() => {});
- }
-
- renderMetrics(this.find('.js-render-metrics').get());
- return this;
-};
+// Render GitLab flavoured Markdown
+export function renderGFM(element) {
+ const [
+ highlightEls,
+ krokiEls,
+ mathEls,
+ mermaidEls,
+ tableEls,
+ userEls,
+ popoverEls,
+ metricsEls,
+ observabilityEls,
+ ] = [
+ '.js-syntax-highlight',
+ '.js-render-kroki[hidden]',
+ '.js-render-math',
+ '.js-render-mermaid',
+ '[lang="json"][data-lang-params="table"]',
+ '.gfm-project_member',
+ '.gfm-issue, .gfm-merge_request',
+ '.js-render-metrics',
+ '.js-render-observability',
+ ].map((selector) => Array.from(element.querySelectorAll(selector)));
-$(() => {
- window.requestIdleCallback(
- () => {
- $('body').renderGFM();
- },
- { timeout: 500 },
- );
-});
+ syntaxHighlight(highlightEls);
+ renderKroki(krokiEls);
+ renderMath(mathEls);
+ renderSandboxedMermaid(mermaidEls);
+ renderJSONTable(tableEls.map((e) => e.parentNode));
+ highlightCurrentUser(userEls);
+ renderMetrics(metricsEls);
+ renderObservability(observabilityEls);
+ initPopovers(popoverEls);
+}
diff --git a/app/assets/javascripts/behaviors/markdown/render_math.js b/app/assets/javascripts/behaviors/markdown/render_math.js
index ac41af4df7a..7852a909160 100644
--- a/app/assets/javascripts/behaviors/markdown/render_math.js
+++ b/app/assets/javascripts/behaviors/markdown/render_math.js
@@ -175,14 +175,14 @@ class SafeMathRenderer {
}
}
-export default function renderMath($els) {
- if (!$els.length) return;
+export default function renderMath(elements) {
+ if (!elements.length) return;
Promise.all([
import(/* webpackChunkName: 'katex' */ 'katex'),
import(/* webpackChunkName: 'katex' */ 'katex/dist/katex.min.css'),
])
.then(([katex]) => {
- const renderer = new SafeMathRenderer($els.get(), katex);
+ const renderer = new SafeMathRenderer(elements, katex);
renderer.render();
renderer.attachEvents();
})
diff --git a/app/assets/javascripts/behaviors/markdown/render_observability.js b/app/assets/javascripts/behaviors/markdown/render_observability.js
new file mode 100644
index 00000000000..704d85cf22e
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/render_observability.js
@@ -0,0 +1,33 @@
+import Vue from 'vue';
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+
+export function getFrameSrc(url) {
+ return `${setUrlParams({ theme: darkModeEnabled() ? 'dark' : 'light' }, url)}&kiosk`;
+}
+
+const mountVueComponent = (element) => {
+ const url = [element.dataset.frameUrl];
+
+ return new Vue({
+ el: element,
+ render(h) {
+ return h('iframe', {
+ style: {
+ height: '366px',
+ width: '768px',
+ },
+ attrs: {
+ src: getFrameSrc(url),
+ frameBorder: '0',
+ },
+ });
+ },
+ });
+};
+
+export default function renderObservability(elements) {
+ elements.forEach((element) => {
+ mountVueComponent(element);
+ });
+}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 68f5180cc03..86a05f24dfc 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import '~/behaviors/markdown/init_gfm';
// MarkdownPreview
//
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index 97ba9e15c0f..64297da39cd 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -3,7 +3,7 @@ import ClipboardJS from 'clipboard';
import Mousetrap from 'mousetrap';
import { getSelectedFragment } from '~/lib/utils/common_utils';
import { isElementVisible } from '~/lib/utils/dom_utils';
-import { DEBOUNCE_DROPDOWN_DELAY } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { DEBOUNCE_DROPDOWN_DELAY } from '~/sidebar/components/labels/labels_select_widget/constants';
import toast from '~/vue_shared/plugins/global_toast';
import { s__ } from '~/locale';
import Sidebar from '~/right_sidebar';
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 716321430d2..361d736f740 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -1,5 +1,5 @@
<script>
-import DefaultActions from './blob_header_default_actions.vue';
+import DefaultActions from 'jh_else_ce/blob/components/blob_header_default_actions.vue';
import BlobFilepath from './blob_header_filepath.vue';
import ViewerSwitcher from './blob_header_viewer_switcher.vue';
import { SIMPLE_BLOB_VIEWER } from './constants';
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 24a54358de5..8cfdc00bb40 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -5,7 +5,7 @@ const createSandbox = () => {
const iframeEl = document.createElement('iframe');
setAttributes(iframeEl, {
src: '/-/sandbox/swagger',
- sandbox: 'allow-scripts allow-popups',
+ sandbox: 'allow-scripts allow-popups allow-forms',
frameBorder: 0,
width: '100%',
// The height will be adjusted dynamically.
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 8d323c335d3..439c4258805 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import '~/behaviors/markdown/init_gfm';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import {
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index 4741dd53708..509d399273d 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -66,7 +66,7 @@ export default () => {
})
.catch((e) =>
createAlert({
- message: e,
+ message: e.message,
}),
);
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 97d8b206307..46b3f16df77 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -9,6 +9,7 @@ import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
import { insertFinalNewline } from '~/lib/utils/text_utility';
import TemplateSelectorMediator from '../blob/file_template_mediator';
import { BLOB_EDITOR_ERROR, BLOB_PREVIEW_ERROR } from './constants';
+import '~/behaviors/markdown/init_gfm';
export default class EditBlob {
// The options object has:
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 150378f7a7d..ca86894ca40 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -1,8 +1,10 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import { breakpoints } from '@gitlab/ui/dist/utils';
import { sortBy, throttle } from 'lodash';
import Draggable from 'vuedraggable';
import { mapState, mapGetters, mapActions } from 'vuex';
+import { contentTop } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import { formatBoardLists } from 'ee_else_ce/boards/boards_util';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
@@ -114,6 +116,8 @@ export default {
group: 'boards-list',
tag: 'div',
value: this.boardListsToUse,
+ delay: 100,
+ delayOnTouchOnly: true,
};
return this.canDragColumns ? options : {};
@@ -142,7 +146,11 @@ export default {
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
},
setBoardHeight() {
- this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
+ if (window.innerWidth < breakpoints.md) {
+ this.boardHeight = `${window.innerHeight - contentTop()}px`;
+ } else {
+ this.boardHeight = `${window.innerHeight - this.$el.getBoundingClientRect().top}px`;
+ }
},
},
};
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 00b4e6c96a9..392a73b5859 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -14,8 +14,8 @@ import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
-import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import { LabelType } from '~/sidebar/components/labels/labels_select_widget/constants';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
@@ -32,10 +32,12 @@ export default {
SidebarTodoWidget,
SidebarSeverity,
MountingPortal,
+ SidebarHealthStatusWidget: () =>
+ import('ee_component/sidebar/components/health_status/sidebar_health_status_widget.vue'),
+ SidebarIterationWidget: () =>
+ import('ee_component/sidebar/components/iteration/sidebar_iteration_widget.vue'),
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
- IterationSidebarDropdownWidget: () =>
- import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
mixins: [glFeatureFlagMixin()],
inject: {
@@ -51,6 +53,9 @@ export default {
weightFeatureAvailable: {
default: false,
},
+ healthStatusFeatureAvailable: {
+ default: false,
+ },
allowLabelEdit: {
default: false,
},
@@ -115,6 +120,7 @@ export default {
'setActiveItemConfidential',
'setActiveBoardItemLabels',
'setActiveItemWeight',
+ 'setActiveItemHealthStatus',
]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
@@ -143,7 +149,7 @@ export default {
<gl-drawer
v-bind="$attrs"
:open="showSidebar"
- class="boards-sidebar gl-absolute"
+ class="boards-sidebar"
variant="sidebar"
@close="handleClose"
>
@@ -187,7 +193,7 @@ export default {
:issuable-type="issuableType"
data-testid="sidebar-milestones"
/>
- <iteration-sidebar-dropdown-widget
+ <sidebar-iteration-widget
v-if="iterationFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
@@ -236,6 +242,13 @@ export default {
:issuable-type="issuableType"
@weightUpdated="setActiveItemWeight($event)"
/>
+ <sidebar-health-status-widget
+ v-if="healthStatusFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @statusUpdated="setActiveItemHealthStatus($event)"
+ />
<sidebar-confidentiality-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 816b22e4dc6..215691c7ba2 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -133,6 +133,8 @@ export default {
'ghost-class': 'board-card-drag-active',
'data-list-id': this.list.id,
value: this.boardItems,
+ delay: 100,
+ delayOnTouchOnly: true,
};
return this.canMoveIssue ? options : {};
@@ -317,7 +319,7 @@ export default {
>
<!-- TODO: remove the condition when https://gitlab.com/gitlab-org/gitlab/-/issues/377862 is resolved -->
<board-card-move-to-position
- v-if="!isEpicBoard"
+ v-if="!isEpicBoard && !disabled"
:item="item"
:index="index"
:list="list"
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index eaf3facb450..4f90d77c0be 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -237,7 +237,7 @@ export default {
:text="board.name"
@show="loadBoards"
>
- <p class="gl-new-dropdown-header-top" @mousedown.prevent>
+ <p class="gl-dropdown-header-top" @mousedown.prevent>
{{ s__('IssueBoards|Switch board') }}
</p>
<gl-search-box-by-type ref="searchBox" v-model="filterTerm" class="m-2" />
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 605e11d1590..bc68c2e0e99 100644
--- a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -12,8 +12,8 @@ import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import {
- OPERATOR_IS_AND_IS_NOT,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS_NOT,
+ OPERATORS_IS,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -31,7 +31,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import EmojiToken from '~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue';
import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
@@ -60,7 +60,7 @@ export default {
tokensCE() {
const { issue, incident } = this.$options.i18n;
const { types } = this.$options;
- const { fetchAuthors, fetchLabels } = issueBoardFilters(
+ const { fetchUsers, fetchLabels } = issueBoardFilters(
this.$apollo,
this.fullPath,
this.boardType,
@@ -71,28 +71,28 @@ export default {
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
type: TOKEN_TYPE_ASSIGNEE,
- operators: OPERATOR_IS_AND_IS_NOT,
- token: AuthorToken,
+ operators: OPERATORS_IS_NOT,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: this.preloadedAuthors(),
+ fetchUsers,
+ preloadedUsers: this.preloadedUsers(),
},
{
icon: 'pencil',
title: TOKEN_TITLE_AUTHOR,
type: TOKEN_TYPE_AUTHOR,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
symbol: '@',
- token: AuthorToken,
+ token: UserToken,
unique: true,
- fetchAuthors,
- preloadedAuthors: this.preloadedAuthors(),
+ fetchUsers,
+ preloadedUsers: this.preloadedUsers(),
},
{
icon: 'labels',
title: TOKEN_TITLE_LABEL,
type: TOKEN_TYPE_LABEL,
- operators: OPERATOR_IS_AND_IS_NOT,
+ operators: OPERATORS_IS_NOT,
token: LabelToken,
unique: false,
symbol: '~',
@@ -128,7 +128,7 @@ export default {
title: TOKEN_TITLE_CONFIDENTIAL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: __('Yes') },
{ icon: 'eye', value: 'no', title: __('No') },
@@ -186,7 +186,7 @@ export default {
},
methods: {
...mapActions(['fetchMilestones']),
- preloadedAuthors() {
+ preloadedUsers() {
return gon?.current_user_id
? [
{
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
index a35b3f14be4..b70294c9db3 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_time_tracker.vue
@@ -6,7 +6,7 @@ export default {
components: {
IssuableTimeTracker,
},
- inject: ['timeTrackingLimitToHours'],
+ inject: ['timeTrackingLimitToHours', 'canUpdate'],
computed: {
...mapGetters(['activeBoardItem']),
initialTimeTracking() {
@@ -34,5 +34,6 @@ export default {
:limit-to-hours="timeTrackingLimitToHours"
:initial-time-tracking="initialTimeTracking"
:show-collapsed="false"
+ :can-add-time-entries="canUpdate"
/>
</template>
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
index 699d7e12de4..4bfd92fb748 100644
--- a/app/assets/javascripts/boards/issue_board_filters.js
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -14,13 +14,13 @@ export default function issueBoardFilters(apollo, fullPath, boardType) {
return isGroupBoard ? groupBoardMembers : projectBoardMembers;
};
- const fetchAuthors = (authorsSearchTerm) => {
+ const fetchUsers = (usersSearchTerm) => {
return apollo
.query({
query: boardAssigneesQuery(),
variables: {
fullPath,
- search: authorsSearchTerm,
+ search: usersSearchTerm,
},
})
.then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
@@ -42,6 +42,6 @@ export default function issueBoardFilters(apollo, fullPath, boardType) {
return {
fetchLabels,
- fetchAuthors,
+ fetchUsers,
};
}
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index e5437690fd4..07b127d86e2 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -928,4 +928,5 @@ export default {
// EE action needs CE empty equivalent
setActiveItemWeight: () => {},
+ setActiveItemHealthStatus: () => {},
};
diff --git a/app/assets/javascripts/branches/components/sort_dropdown.vue b/app/assets/javascripts/branches/components/sort_dropdown.vue
index 5f782b5e652..263efcaa788 100644
--- a/app/assets/javascripts/branches/components/sort_dropdown.vue
+++ b/app/assets/javascripts/branches/components/sort_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByClick } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlSearchBoxByClick } from '@gitlab/ui';
import { mergeUrlParams, visitUrl, getParameterValues } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -10,8 +10,7 @@ export default {
searchPlaceholder: s__('Branches|Filter by branch name'),
},
components: {
- GlDropdown,
- GlDropdownItem,
+ GlCollapsibleListbox,
GlSearchBoxByClick,
},
inject: ['projectBranchesFilteredPath', 'sortOptions', 'mode'],
@@ -28,6 +27,9 @@ export default {
selectedSortMethodName() {
return this.sortOptions[this.selectedKey];
},
+ listboxItems() {
+ return Object.entries(this.sortOptions).map(([value, text]) => ({ value, text }));
+ },
},
created() {
const sortValue = getParameterValues('sort');
@@ -42,9 +44,6 @@ export default {
}
},
methods: {
- isSortMethodSelected(sortKey) {
- return sortKey === this.selectedKey;
- },
visitUrlFromOption(sortKey) {
this.selectedKey = sortKey;
const urlParams = {};
@@ -70,20 +69,15 @@ export default {
data-testid="branch-search"
@submit="visitUrlFromOption(selectedKey)"
/>
- <gl-dropdown
+
+ <gl-collapsible-listbox
v-if="shouldShowDropdown"
- :text="selectedSortMethodName"
+ v-model="selectedKey"
+ :items="listboxItems"
+ :toggle-text="selectedSortMethodName"
class="gl-mr-3"
data-testid="branches-dropdown"
- >
- <gl-dropdown-item
- v-for="(value, key) in sortOptions"
- :key="key"
- :is-checked="isSortMethodSelected(key)"
- is-check-item
- @click="visitUrlFromOption(key)"
- >{{ value }}</gl-dropdown-item
- >
- </gl-dropdown>
+ @select="visitUrlFromOption(selectedKey)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/branches/init_new_branch_ref_selector.js b/app/assets/javascripts/branches/init_new_branch_ref_selector.js
new file mode 100644
index 00000000000..aad3fbb9982
--- /dev/null
+++ b/app/assets/javascripts/branches/init_new_branch_ref_selector.js
@@ -0,0 +1,25 @@
+import Vue from 'vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+export default function initNewBranchRefSelector() {
+ const el = document.querySelector('.js-new-branch-ref-selector');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectId, defaultBranchName, hiddenInputName } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ value: defaultBranchName,
+ name: hiddenInputName,
+ projectId,
+ },
+ });
+ },
+ });
+}
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue
index 8db4cba529f..49a314e067c 100644
--- a/app/assets/javascripts/ci_lint/components/ci_lint.vue
+++ b/app/assets/javascripts/ci/ci_lint/components/ci_lint.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlFormCheckbox, GlIcon, GlLink, GlAlert } from '@gitlab/ui';
-import CiLintResults from '~/pipeline_editor/components/lint/ci_lint_results.vue';
-import lintCiMutation from '~/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
+import CiLintResults from '~/ci/pipeline_editor/components/lint/ci_lint_results.vue';
+import lintCiMutation from '~/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
diff --git a/app/assets/javascripts/ci_lint/index.js b/app/assets/javascripts/ci/ci_lint/index.js
index 274aab45deb..382059eb17e 100644
--- a/app/assets/javascripts/ci_lint/index.js
+++ b/app/assets/javascripts/ci/ci_lint/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
+import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import CiLint from './components/ci_lint.vue';
diff --git a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
index 7b33d98bca0..7b33d98bca0 100644
--- a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/code_snippet_alert.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js
index e4fd423249b..e4fd423249b 100644
--- a/app/assets/javascripts/pipeline_editor/components/code_snippet_alert/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/components/code_snippet_alert/constants.js
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
index 4775836fcc6..4775836fcc6 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_form.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
index 9cbf60b1c8f..9cbf60b1c8f 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_section.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/commit/commit_section.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
index 0b57433e894..0b57433e894 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/first_pipeline_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue
index d2682cf6326..d2682cf6326 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/getting_started_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/getting_started_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
index bc9203b9c5b..bc9203b9c5b 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/pipeline_config_reference_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
index aeeb52319d2..aeeb52319d2 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/cards/visualize_and_lint_card.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
index 375db7f3054..375db7f3054 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue b/app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue
index 049504181c4..049504181c4 100644
--- a/app/assets/javascripts/pipeline_editor/components/drawer/ui/demo_job_pill.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/drawer/ui/demo_job_pill.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 42e2d34fa3a..42e2d34fa3a 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_config_merged_preview.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 189690ce2c3..201fba837e2 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -43,9 +43,7 @@ export default {
</script>
<template>
- <div
- class="gl-bg-gray-10 gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1"
- >
+ <div class="gl-display-flex gl-p-3 gl-gap-3 gl-border-solid gl-border-gray-100 gl-border-1">
<gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
index 255e3cb31f1..255e3cb31f1 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/text_editor.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 1f8ddae3696..ef9acc1f8f1 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -16,11 +16,11 @@ import {
BRANCH_PAGINATION_LIMIT,
BRANCH_SEARCH_DEBOUNCE,
DEFAULT_FAILURE,
-} from '~/pipeline_editor/constants';
-import updateCurrentBranchMutation from '~/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql';
-import getAvailableBranchesQuery from '~/pipeline_editor/graphql/queries/available_branches.query.graphql';
-import getCurrentBranch from '~/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import getLastCommitBranch from '~/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
+} from '~/ci/pipeline_editor/constants';
+import updateCurrentBranchMutation from '~/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql';
+import getAvailableBranchesQuery from '~/ci/pipeline_editor/graphql/queries/available_branches.query.graphql';
+import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
+import getLastCommitBranch from '~/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql';
export default {
i18n: {
diff --git a/app/assets/javascripts/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 8e95fad1e48..84c29e48114 100644
--- a/app/assets/javascripts/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
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import { EDITOR_APP_STATUS_EMPTY, EDITOR_APP_STATUS_LOADING } from '../../constants';
import FileTreePopover from '../popovers/file_tree_popover.vue';
import BranchSwitcher from './branch_switcher.vue';
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue
index 280cd729a43..280cd729a43 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_tree/container.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/container.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue
index 786d483b5b9..786d483b5b9 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_tree/file_item.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_tree/file_item.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
index ec6ee52b6b2..ec6ee52b6b2 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_header.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index feadc60a22a..feadc60a22a 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 137dfca68d6..372f04075ab 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -3,8 +3,8 @@ import { GlButton, GlIcon, GlLink, GlLoadingIcon, GlSprintf, GlTooltipDirective
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__ } from '~/locale';
-import getPipelineQuery from '~/pipeline_editor/graphql/queries/pipeline.query.graphql';
-import getPipelineEtag from '~/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
+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,
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
index 610a570c4ce..84c0eef441f 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/validation_segment.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
-import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.query.graphql';
+import getAppStatus from '~/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue
index 0f19b9386e6..0f19b9386e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue
index 49225a7cac7..49225a7cac7 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_param.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_param.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue
index ef2be2a5fba..ef2be2a5fba 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_results_value.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_results_value.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue
index ac0332cb0bd..ac0332cb0bd 100644
--- a/app/assets/javascripts/pipeline_editor/components/lint/ci_lint_warnings.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/lint/ci_lint_warnings.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index ed5466ff99c..ed5466ff99c 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
index efa6a54c638..efa6a54c638 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/file_tree_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/file_tree_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue
index 4730a521227..4730a521227 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/validate_pipeline_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/validate_pipeline_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue b/app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue
index c636d8b8e34..c636d8b8e34 100644
--- a/app/assets/javascripts/pipeline_editor/components/popovers/walkthrough_popover.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/popovers/walkthrough_popover.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
index bc076fbe349..bc076fbe349 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue
index 65f399d1912..22b82f2e96f 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/editor_tab.vue
@@ -41,7 +41,9 @@ import { __, s__ } from '~/locale';
export default {
i18n: {
- invalid: __('Your CI/CD configuration syntax is invalid. View Lint tab for more details.'),
+ invalid: __(
+ 'Your CI/CD configuration syntax is invalid. Select the Validate tab for more details.',
+ ),
unavailable: __(
"We're experiencing difficulties and this tab content is currently unavailable.",
),
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
index 7d2b9cd3d42..d7b8e7151d9 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_empty_state.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton, GlSprintf } from '@gitlab/ui';
import { __ } from '~/locale';
-import PipelineEditorFileNav from '~/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
+import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
export default {
components: {
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue
index c72cff4c6f8..c72cff4c6f8 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_messages.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/ui/pipeline_editor_messages.vue
diff --git a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 83fcab4b343..83fcab4b343 100644
--- a/app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index dd25c4d433b..dd25c4d433b 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
index 2d42ebb6ac3..2d42ebb6ac3 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/lint_ci.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
index 7487e328668..7487e328668 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_app_status.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
index b722c147f5f..b722c147f5f 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_current_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
index 9561312f2b6..9561312f2b6 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_last_commit_branch.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
index 9025f00b343..9025f00b343 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
index 3495ca51283..3495ca51283 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql
index 359b4a846c7..359b4a846c7 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/available_branches.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/available_branches.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql
index 5928d90f7c4..5928d90f7c4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/blob_content.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
index 5354ed7c2d5..5354ed7c2d5 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/ci_config.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql
index 0df8cafa3cb..0df8cafa3cb 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/app_status.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/app_status.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql
index 1f4f9d26f24..1f4f9d26f24 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/current_branch.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
index a83129759de..a83129759de 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/last_commit_branch.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
index 8df6e74a5d9..8df6e74a5d9 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql
index a34c8f365f4..a34c8f365f4 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/get_starter_template.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/get_starter_template.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
index d62fda40237..d62fda40237 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql
index 021b858d72e..021b858d72e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/pipeline.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/queries/pipeline.query.graphql
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js
index fa1c70c1994..fa1c70c1994 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/resolvers.js
diff --git a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql b/app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql
index 508ff22c46e..508ff22c46e 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/typedefs.graphql
+++ b/app/assets/javascripts/ci/pipeline_editor/graphql/typedefs.graphql
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index 6d91c339833..6d91c339833 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
index ff848a973e3..ff848a973e3 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_app.vue
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 1972125ed56..1972125ed56 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
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 6e24ac6b8d4..a4ef7827f73 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,18 +1,321 @@
<script>
-import { GlForm } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormCheckbox,
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+import Vue from 'vue';
+import { __, s__ } from '~/locale';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
+import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import { VARIABLE_TYPE, FILE_TYPE } from '../constants';
export default {
components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
GlForm,
+ GlFormCheckbox,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlLink,
+ GlSprintf,
+ RefSelector,
+ TimezoneDropdown,
+ IntervalPatternInput,
},
- inject: {
- fullPath: {
+ inject: [
+ 'fullPath',
+ 'projectId',
+ 'defaultBranch',
+ 'cron',
+ 'cronTimezone',
+ 'dailyLimit',
+ 'settingsLink',
+ ],
+ props: {
+ timezoneData: {
+ type: Array,
+ required: true,
+ },
+ refParam: {
+ type: String,
+ required: false,
default: '',
},
},
+ data() {
+ return {
+ refValue: {
+ shortName: this.refParam,
+ // this is needed until we add support for ref type in url query strings
+ // ensure default branch is called with full ref on load
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
+ fullName: this.refParam === this.defaultBranch ? `refs/heads/${this.refParam}` : undefined,
+ },
+ description: '',
+ scheduleRef: this.defaultBranch,
+ activated: true,
+ timezone: this.cronTimezone,
+ formCiVariables: {},
+ // TODO: Add the GraphQL query to help populate the predefined variables
+ // app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue#131
+ predefinedValueOptions: {},
+ };
+ },
+ i18n: {
+ activated: __('Activated'),
+ cronTimezone: s__('PipelineSchedules|Cron timezone'),
+ description: s__('PipelineSchedules|Description'),
+ shortDescriptionPipeline: s__(
+ 'PipelineSchedules|Provide a short description for this pipeline',
+ ),
+ savePipelineSchedule: s__('PipelineSchedules|Save pipeline schedule'),
+ cancel: __('Cancel'),
+ targetBranchTag: __('Select target branch or tag'),
+ intervalPattern: s__('PipelineSchedules|Interval Pattern'),
+ variablesDescription: s__(
+ 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
+ ),
+ removeVariableLabel: s__('CiVariables|Remove variable'),
+ variables: s__('Pipeline|Variables'),
+ },
+ 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() {
+ return {
+ dropdownHeader: this.$options.i18n.targetBranchTag,
+ };
+ },
+ refFullName() {
+ return this.refValue.fullName;
+ },
+ variables() {
+ return this.formCiVariables[this.refFullName]?.variables ?? [];
+ },
+ descriptions() {
+ return this.formCiVariables[this.refFullName]?.descriptions ?? {};
+ },
+ typeOptionsListbox() {
+ return [
+ {
+ text: __('Variable'),
+ value: VARIABLE_TYPE,
+ },
+ {
+ text: __('File'),
+ value: FILE_TYPE,
+ },
+ ];
+ },
+ getEnabledRefTypes() {
+ return [REF_TYPE_BRANCHES, REF_TYPE_TAGS];
+ },
+ },
+ created() {
+ Vue.set(this.formCiVariables, this.refFullName, {
+ variables: [],
+ descriptions: {},
+ });
+
+ this.addEmptyVariable(this.refFullName);
+ },
+ methods: {
+ addEmptyVariable(refValue) {
+ const { variables } = this.formCiVariables[refValue];
+
+ const lastVar = variables[variables.length - 1];
+ if (lastVar?.key === '' && lastVar?.value === '') {
+ return;
+ }
+
+ variables.push({
+ uniqueId: uniqueId(`var-${refValue}`),
+ variable_type: VARIABLE_TYPE,
+ key: '',
+ value: '',
+ });
+ },
+ setVariableAttribute(key, attribute, value) {
+ const { variables } = this.formCiVariables[this.refFullName];
+ const variable = variables.find((v) => v.key === key);
+ variable[attribute] = value;
+ },
+ shouldShowValuesDropdown(key) {
+ return this.predefinedValueOptions[key]?.length > 1;
+ },
+ removeVariable(index) {
+ this.variables.splice(index, 1);
+ },
+ canRemove(index) {
+ return index < this.variables.length - 1;
+ },
+ },
};
</script>
<template>
- <gl-form />
+ <div class="col-lg-8">
+ <gl-form>
+ <!--Description-->
+ <gl-form-group :label="$options.i18n.description" label-for="schedule-description">
+ <gl-form-input
+ id="schedule-description"
+ v-model="description"
+ type="text"
+ :placeholder="$options.i18n.shortDescriptionPipeline"
+ data-testid="schedule-description"
+ />
+ </gl-form-group>
+ <!--Interval Pattern-->
+ <gl-form-group :label="$options.i18n.intervalPattern" label-for="schedule-interval">
+ <interval-pattern-input
+ id="schedule-interval"
+ :initial-cron-interval="cron"
+ :daily-limit="dailyLimit"
+ :send-native-errors="false"
+ />
+ </gl-form-group>
+ <!--Timezone-->
+ <gl-form-group :label="$options.i18n.cronTimezone" label-for="schedule-timezone">
+ <timezone-dropdown
+ id="schedule-timezone"
+ :value="timezone"
+ :timezone-data="timezoneData"
+ name="schedule-timezone"
+ />
+ </gl-form-group>
+ <!--Branch/Tag Selector-->
+ <gl-form-group :label="$options.i18n.targetBranchTag" label-for="schedule-target-branch-tag">
+ <ref-selector
+ id="schedule-target-branch-tag"
+ :enabled-ref-types="getEnabledRefTypes"
+ :project-id="projectId"
+ :value="scheduleRef"
+ :use-symbolic-ref-names="true"
+ :translations="dropdownTranslations"
+ class="gl-w-full"
+ />
+ </gl-form-group>
+ <!--Variable List-->
+ <gl-form-group :label="$options.i18n.variables">
+ <div
+ v-for="(variable, index) in variables"
+ :key="variable.uniqueId"
+ class="gl-mb-3 gl-pb-2"
+ data-testid="ci-variable-row"
+ data-qa-selector="ci_variable_row_container"
+ >
+ <div
+ class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
+ >
+ <gl-dropdown
+ :text="$options.typeOptions[variable.variable_type]"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-type"
+ >
+ <gl-dropdown-item
+ v-for="type in Object.keys($options.typeOptions)"
+ :key="type"
+ @click="setVariableAttribute(variable.key, 'variable_type', type)"
+ >
+ {{ $options.typeOptions[type] }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-input
+ v-model="variable.key"
+ :placeholder="s__('CiVariables|Input variable key')"
+ :class="$options.formElementClasses"
+ data-testid="pipeline-form-ci-variable-key"
+ data-qa-selector="ci_variable_key_field"
+ @change="addEmptyVariable(refFullName)"
+ />
+ <gl-dropdown
+ v-if="shouldShowValuesDropdown(variable.key)"
+ :text="variable.value"
+ :class="$options.formElementClasses"
+ class="gl-flex-grow-1 gl-mr-0!"
+ data-testid="pipeline-form-ci-variable-value-dropdown"
+ >
+ <gl-dropdown-item
+ v-for="value in predefinedValueOptions[variable.key]"
+ :key="value"
+ data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ @click="setVariableAttribute(variable.key, 'value', value)"
+ >
+ {{ value }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-form-textarea
+ v-else
+ 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"
+ />
+
+ <template v-if="variables.length > 1">
+ <gl-button
+ v-if="canRemove(index)"
+ class="gl-md-ml-3 gl-mb-3"
+ data-testid="remove-ci-variable-row"
+ variant="danger"
+ category="secondary"
+ icon="clear"
+ :aria-label="$options.i18n.removeVariableLabel"
+ @click="removeVariable(index)"
+ />
+ <gl-button
+ v-else
+ class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
+ icon="clear"
+ :aria-label="$options.i18n.removeVariableLabel"
+ />
+ </template>
+ </div>
+ <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
+ {{ descriptions[variable.key] }}
+ </div>
+ </div>
+
+ <template #description
+ ><gl-sprintf :message="$options.i18n.variablesDescription">
+ <template #link="{ content }">
+ <gl-link :href="settingsLink">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf></template
+ >
+ </gl-form-group>
+ <!--Activated-->
+ <gl-form-checkbox id="schedule-active" v-model="activated" class="gl-mb-3">{{
+ $options.i18n.activated
+ }}</gl-form-checkbox>
+
+ <gl-button type="submit" variant="confirm" data-testid="schedule-submit-button">{{
+ $options.i18n.savePipelineSchedule
+ }}</gl-button>
+ <gl-button type="reset" data-testid="schedule-cancel-button">{{
+ $options.i18n.cancel
+ }}</gl-button>
+ </gl-form>
+ </div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/constants.js b/app/assets/javascripts/ci/pipeline_schedules/constants.js
new file mode 100644
index 00000000000..b4ab1143f60
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_schedules/constants.js
@@ -0,0 +1,2 @@
+export const VARIABLE_TYPE = 'env_var';
+export const FILE_TYPE = 'file';
diff --git a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
index d83417ab84a..445161f99cb 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
+++ b/app/assets/javascripts/ci/pipeline_schedules/mount_pipeline_schedules_form_app.js
@@ -16,7 +16,16 @@ export default (selector) => {
return false;
}
- const { fullPath } = containerEl.dataset;
+ const {
+ fullPath,
+ cron,
+ dailyLimit,
+ timezoneData,
+ cronTimezone,
+ projectId,
+ defaultBranch,
+ settingsLink,
+ } = containerEl.dataset;
return new Vue({
el: containerEl,
@@ -24,9 +33,20 @@ export default (selector) => {
apolloProvider,
provide: {
fullPath,
+ projectId,
+ defaultBranch,
+ dailyLimit: dailyLimit ?? '',
+ cronTimezone: cronTimezone ?? '',
+ cron: cron ?? '',
+ settingsLink,
},
render(createElement) {
- return createElement(PipelineSchedulesForm);
+ return createElement(PipelineSchedulesForm, {
+ props: {
+ timezoneData: JSON.parse(timezoneData),
+ refParam: defaultBranch,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue b/app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue
index fb2ef850e4f..5a7ee9c9b28 100644
--- a/app/assets/javascripts/reports/codequality_report/components/codequality_issue_body.vue
+++ b/app/assets/javascripts/ci/reports/codequality_report/components/codequality_issue_body.vue
@@ -5,8 +5,8 @@
*/
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
-import ReportLink from '~/reports/components/report_link.vue';
-import { STATUS_SUCCESS, STATUS_NEUTRAL } from '~/reports/constants';
+import ReportLink from '~/ci/reports/components/report_link.vue';
+import { STATUS_SUCCESS, STATUS_NEUTRAL } from '~/ci/reports/constants';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '../constants';
export default {
diff --git a/app/assets/javascripts/reports/codequality_report/constants.js b/app/assets/javascripts/ci/reports/codequality_report/constants.js
index 0c472b24471..5e81245037f 100644
--- a/app/assets/javascripts/reports/codequality_report/constants.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/constants.js
@@ -16,12 +16,7 @@ export const SEVERITY_ICONS = {
unknown: 'severity-unknown',
};
-// This is the icons mapping for the code Quality Merge-Request Widget Extension
-// once the refactor_mr_widgets_extensions flag is activated the above SEVERITY_ICONS
-// need be removed and this variable needs to be rename to SEVERITY_ICONS
-// Rollout Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341759
-
-export const SEVERITY_ICONS_EXTENSION = {
+export const SEVERITY_ICONS_MR_WIDGET = {
info: 'severityInfo',
minor: 'severityLow',
major: 'severityMedium',
@@ -29,3 +24,30 @@ export const SEVERITY_ICONS_EXTENSION = {
blocker: 'severityCritical',
unknown: 'severityUnknown',
};
+
+export const SEVERITIES = {
+ info: {
+ class: SEVERITY_CLASSES.info,
+ name: SEVERITY_ICONS.info,
+ },
+ minor: {
+ class: SEVERITY_CLASSES.minor,
+ name: SEVERITY_ICONS.minor,
+ },
+ major: {
+ class: SEVERITY_CLASSES.major,
+ name: SEVERITY_ICONS.major,
+ },
+ critical: {
+ class: SEVERITY_CLASSES.critical,
+ name: SEVERITY_ICONS.critical,
+ },
+ blocker: {
+ class: SEVERITY_CLASSES.blocker,
+ name: SEVERITY_ICONS.blocker,
+ },
+ unknown: {
+ class: SEVERITY_CLASSES.unknown,
+ name: SEVERITY_ICONS.unknown,
+ },
+};
diff --git a/app/assets/javascripts/reports/codequality_report/store/actions.js b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
index 04aca11b945..04aca11b945 100644
--- a/app/assets/javascripts/reports/codequality_report/store/actions.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/getters.js b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
index 70d11e96a54..70d11e96a54 100644
--- a/app/assets/javascripts/reports/codequality_report/store/getters.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/index.js b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
index 5bfcd69edec..5bfcd69edec 100644
--- a/app/assets/javascripts/reports/codequality_report/store/index.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
index c362c973ae1..c362c973ae1 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutation_types.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/mutations.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
index 249c2f35c0b..249c2f35c0b 100644
--- a/app/assets/javascripts/reports/codequality_report/store/mutations.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/state.js b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
index f68dbc2a5fa..f68dbc2a5fa 100644
--- a/app/assets/javascripts/reports/codequality_report/store/state.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
diff --git a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js b/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
index 417297df43c..417297df43c 100644
--- a/app/assets/javascripts/reports/codequality_report/store/utils/codequality_parser.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
diff --git a/app/assets/javascripts/reports/components/grouped_issues_list.vue b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
index ca369022938..b21a486e259 100644
--- a/app/assets/javascripts/reports/components/grouped_issues_list.vue
+++ b/app/assets/javascripts/ci/reports/components/grouped_issues_list.vue
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import ReportItem from '~/reports/components/report_item.vue';
+import ReportItem from '~/ci/reports/components/report_item.vue';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
export default {
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/ci/reports/components/issue_body.js
index 4f418216024..daff1be30ff 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/ci/reports/components/issue_body.js
@@ -1,4 +1,4 @@
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
+import IssueStatusIcon from '~/ci/reports/components/issue_status_icon.vue';
export const components = {
CodequalityIssueBody: () => import('../codequality_report/components/codequality_issue_body.vue'),
diff --git a/app/assets/javascripts/reports/components/issue_status_icon.vue b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
index bd41b8d23f1..bd41b8d23f1 100644
--- a/app/assets/javascripts/reports/components/issue_status_icon.vue
+++ b/app/assets/javascripts/ci/reports/components/issue_status_icon.vue
diff --git a/app/assets/javascripts/reports/components/issues_list.vue b/app/assets/javascripts/ci/reports/components/issues_list.vue
index 9df0a1953b6..ababd4b5e49 100644
--- a/app/assets/javascripts/reports/components/issues_list.vue
+++ b/app/assets/javascripts/ci/reports/components/issues_list.vue
@@ -1,6 +1,6 @@
<script>
-import ReportItem from '~/reports/components/report_item.vue';
-import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+import ReportItem from '~/ci/reports/components/report_item.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/ci/reports/constants';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
const wrapIssueWithState = (status, isNew = false) => (issue) => ({
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/ci/reports/components/report_item.vue
index 918263bfb5c..97d4ac7bf6f 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/ci/reports/components/report_item.vue
@@ -4,7 +4,7 @@ import {
componentNames,
iconComponents,
iconComponentNames,
-} from 'ee_else_ce/reports/components/issue_body';
+} from 'ee_else_ce/ci/reports/components/issue_body';
export default {
name: 'ReportItem',
diff --git a/app/assets/javascripts/reports/components/report_link.vue b/app/assets/javascripts/ci/reports/components/report_link.vue
index 1f68f79e487..1f68f79e487 100644
--- a/app/assets/javascripts/reports/components/report_link.vue
+++ b/app/assets/javascripts/ci/reports/components/report_link.vue
diff --git a/app/assets/javascripts/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index 468c8916b8d..468c8916b8d 100644
--- a/app/assets/javascripts/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
diff --git a/app/assets/javascripts/reports/components/summary_row.vue b/app/assets/javascripts/ci/reports/components/summary_row.vue
index ee55368c829..ee55368c829 100644
--- a/app/assets/javascripts/reports/components/summary_row.vue
+++ b/app/assets/javascripts/ci/reports/components/summary_row.vue
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/ci/reports/constants.js
index bad6fa1e7b9..bad6fa1e7b9 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/ci/reports/constants.js
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
index 9fa4b521ebc..66d790acb00 100644
--- a/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/admin_runner_show_app.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBadge, GlTabs, GlTab, GlTooltipDirective } from '@gitlab/ui';
+import { GlBadge, GlTabs, GlTab } from '@gitlab/ui';
+import VueRouter from 'vue-router';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -11,11 +12,28 @@ import RunnerPauseButton from '../components/runner_pause_button.vue';
import RunnerHeader from '../components/runner_header.vue';
import RunnerDetails from '../components/runner_details.vue';
import RunnerJobs from '../components/runner_jobs.vue';
-import { I18N_DETAILS, I18N_FETCH_ERROR } from '../constants';
+import { I18N_DETAILS, I18N_JOBS, I18N_FETCH_ERROR } from '../constants';
import runnerQuery from '../graphql/show/runner.query.graphql';
import { captureException } from '../sentry_utils';
import { saveAlertToLocalStorage } from '../local_storage_alert/save_alert_to_local_storage';
+const ROUTE_DETAILS = 'details';
+const ROUTE_JOBS = 'jobs';
+
+const routes = [
+ {
+ path: '/',
+ name: ROUTE_DETAILS,
+ component: RunnerDetails,
+ },
+ {
+ path: '/jobs',
+ name: ROUTE_JOBS,
+ component: RunnerJobs,
+ },
+ { path: '*', redirect: { name: ROUTE_DETAILS } },
+];
+
export default {
name: 'AdminRunnerShowApp',
components: {
@@ -26,12 +44,10 @@ export default {
RunnerEditButton,
RunnerPauseButton,
RunnerHeader,
- RunnerDetails,
- RunnerJobs,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
},
+ router: new VueRouter({
+ routes,
+ }),
props: {
runnerId: {
type: String,
@@ -72,11 +88,17 @@ export default {
jobCount() {
return formatJobCount(this.runner?.jobCount);
},
+ tabIndex() {
+ return routes.findIndex(({ name }) => name === this.$route.name);
+ },
},
errorCaptured(error) {
this.reportToSentry(error);
},
methods: {
+ goTo(name) {
+ this.$router.push({ name });
+ },
reportToSentry(error) {
captureException({ error, component: this.$options.name });
},
@@ -85,7 +107,10 @@ export default {
redirectTo(this.runnersPath);
},
},
+ ROUTE_DETAILS,
+ ROUTE_JOBS,
I18N_DETAILS,
+ I18N_JOBS,
};
</script>
<template>
@@ -98,15 +123,13 @@ export default {
</template>
</runner-header>
- <gl-tabs>
- <gl-tab>
+ <gl-tabs :value="tabIndex">
+ <gl-tab @click="goTo($options.ROUTE_DETAILS)">
<template #title>{{ $options.I18N_DETAILS }}</template>
-
- <runner-details v-if="runner" :runner="runner" />
</gl-tab>
- <gl-tab>
+ <gl-tab @click="goTo($options.ROUTE_JOBS)">
<template #title>
- {{ s__('Runners|Jobs') }}
+ {{ $options.I18N_JOBS }}
<gl-badge
v-if="jobCount"
data-testid="job-count-badge"
@@ -116,9 +139,9 @@ export default {
{{ jobCount }}
</gl-badge>
</template>
-
- <runner-jobs v-if="runner" :runner="runner" />
</gl-tab>
+
+ <router-view v-if="runner" :runner="runner" />
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/admin_runner_show/index.js b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
index ea455416648..cbd25819303 100644
--- a/app/assets/javascripts/ci/runner/admin_runner_show/index.js
+++ b/app/assets/javascripts/ci/runner/admin_runner_show/index.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import { showAlertFromLocalStorage } from '../local_storage_alert/show_alert_from_local_storage';
import AdminRunnerShowApp from './admin_runner_show_app.vue';
Vue.use(VueApollo);
+Vue.use(VueRouter);
export const initAdminRunnerShow = (selector = '#js-admin-runner-show') => {
showAlertFromLocalStorage();
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 2915e460085..3bd20dff9cc 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
@@ -23,6 +23,7 @@ import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
import RunnerActionsCell from '../components/cells/runner_actions_cell.vue';
+import RunnerJobStatusBadge from '../components/runner_job_status_badge.vue';
import { pausedTokenConfig } from '../components/search_tokens/paused_token_config';
import { statusTokenConfig } from '../components/search_tokens/status_token_config';
@@ -48,6 +49,7 @@ export default {
RunnerPagination,
RunnerTypeTabs,
RunnerActionsCell,
+ RunnerJobStatusBadge,
},
mixins: [glFeatureFlagMixin()],
inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'],
@@ -69,6 +71,9 @@ export default {
apollo: {
runners: {
query: allRunnersQuery,
+ context: {
+ isSingleRequest: true,
+ },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
@@ -134,6 +139,12 @@ export default {
this.reportToSentry(error);
},
methods: {
+ jobsUrl(runner) {
+ const url = new URL(runner.adminUrl);
+ url.hash = '#/jobs';
+
+ return url.href;
+ },
onToggledPaused() {
// When a runner becomes Paused, the tab count can
// become stale, refetch outdated counts.
@@ -208,6 +219,12 @@ export default {
<runner-name :runner="runner" />
</gl-link>
</template>
+ <template #runner-job-status-badge="{ runner }">
+ <runner-job-status-badge
+ :href="jobsUrl(runner)"
+ :job-status="runner.jobExecutionStatus"
+ />
+ </template>
<template #runner-actions-cell="{ runner }">
<runner-actions-cell
:runner="runner"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
index 67b9b0a266f..cfbe37f5ba2 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
@@ -7,8 +7,6 @@ import RunnerPausedBadge from '../runner_paused_badge.vue';
export default {
components: {
RunnerStatusBadge,
- RunnerUpgradeStatusBadge: () =>
- import('ee_component/ci/runner/components/runner_upgrade_status_badge.vue'),
RunnerPausedBadge,
},
directives: {
@@ -34,10 +32,6 @@ export default {
:runner="runner"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
/>
- <runner-upgrade-status-badge
- :runner="runner"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- />
<runner-paused-badge
v-if="paused"
class="gl-display-inline-block gl-max-w-full gl-text-truncate"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index 1e44d5fccc2..4a72023b6a0 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_stacked_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -6,9 +6,11 @@ import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
+import RunnerJobStatusBadge from '../runner_job_status_badge.vue';
import { formatJobCount } from '../../utils';
import {
+ I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -25,6 +27,7 @@ export default {
RunnerName,
RunnerTags,
RunnerTypeBadge,
+ RunnerJobStatusBadge,
RunnerUpgradeStatusIcon: () =>
import('ee_component/ci/runner/components/runner_upgrade_status_icon.vue'),
TooltipOnTruncate,
@@ -44,6 +47,7 @@ export default {
},
},
i18n: {
+ I18N_NO_DESCRIPTION,
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
I18N_LAST_CONTACT_LABEL,
@@ -75,12 +79,21 @@ export default {
</gl-sprintf>
</div>
<div class="gl-text-secondary gl-mx-2" aria-hidden="true">·</div>
- <tooltip-on-truncate class="gl-text-truncate gl-display-block" :title="runner.description">
+ <tooltip-on-truncate
+ v-if="runner.description"
+ class="gl-text-truncate gl-display-block"
+ :title="runner.description"
+ >
{{ runner.description }}
</tooltip-on-truncate>
+ <span v-else class="gl-text-secondary">{{ $options.i18n.I18N_NO_DESCRIPTION }}</span>
</div>
<div>
+ <slot :runner="runner" name="runner-job-status-badge">
+ <runner-job-status-badge :job-status="runner.jobExecutionStatus" />
+ </slot>
+
<runner-summary-field icon="clock">
<gl-sprintf :message="$options.i18n.I18N_LAST_CONTACT_LABEL">
<template #timeAgo>
diff --git a/app/assets/javascripts/ci/runner/components/runner_detail.vue b/app/assets/javascripts/ci/runner/components/runner_detail.vue
index c260670b517..9e8055a8432 100644
--- a/app/assets/javascripts/ci/runner/components/runner_detail.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_detail.vue
@@ -49,7 +49,7 @@ export default {
<template v-if="value || $scopedSlots.value">
<slot name="value">{{ value }}</slot>
</template>
- <span v-else class="gl-text-gray-500">{{ emptyValue }}</span>
+ <span v-else class="gl-text-secondary">{{ emptyValue }}</span>
</dd>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_groups.vue b/app/assets/javascripts/ci/runner/components/runner_groups.vue
index c3b35bd52a9..8501d165157 100644
--- a/app/assets/javascripts/ci/runner/components/runner_groups.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_groups.vue
@@ -32,6 +32,6 @@ export default {
:avatar-url="group.avatarUrl"
/>
</template>
- <span v-else class="gl-text-gray-500">{{ __('None') }}</span>
+ <span v-else class="gl-text-secondary">{{ __('None') }}</span>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
new file mode 100644
index 00000000000..1e52acecfb8
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlBadge, GlTooltipDirective } from '@gitlab/ui';
+import {
+ I18N_JOB_STATUS_RUNNING,
+ I18N_JOB_STATUS_IDLE,
+ JOB_STATUS_RUNNING,
+ JOB_STATUS_IDLE,
+} from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ jobStatus: {
+ required: false,
+ default: null,
+ type: String,
+ },
+ },
+ computed: {
+ badge() {
+ switch (this.jobStatus) {
+ case JOB_STATUS_RUNNING:
+ return {
+ classes: 'gl-text-blue-600! gl-border gl-border-blue-600!',
+ label: I18N_JOB_STATUS_RUNNING,
+ };
+ case JOB_STATUS_IDLE:
+ return {
+ classes: 'gl-text-gray-700! gl-border gl-border-gray-500!',
+ label: I18N_JOB_STATUS_IDLE,
+ };
+ default:
+ return null;
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge
+ v-if="badge"
+ v-bind="$attrs"
+ size="sm"
+ class="gl-mr-3 gl-bg-transparent!"
+ variant="muted"
+ :class="badge.classes"
+ >
+ {{ badge.label }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index e895537dcdc..b2aad0aac4f 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
@@ -7,7 +7,7 @@ import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.grap
import { formatJobCount, tableField } from '../utils';
import RunnerBulkDelete from './runner_bulk_delete.vue';
import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
-import RunnerStackedSummaryCell from './cells/runner_stacked_summary_cell.vue';
+import RunnerSummaryCell from './cells/runner_summary_cell.vue';
import RunnerStatusPopover from './runner_status_popover.vue';
import RunnerStatusCell from './cells/runner_status_cell.vue';
import RunnerOwnerCell from './cells/runner_owner_cell.vue';
@@ -28,7 +28,7 @@ export default {
RunnerBulkDelete,
RunnerBulkDeleteCheckbox,
RunnerStatusPopover,
- RunnerStackedSummaryCell,
+ RunnerSummaryCell,
RunnerStatusCell,
RunnerOwnerCell,
},
@@ -154,11 +154,14 @@ export default {
</template>
<template #cell(summary)="{ item, index }">
- <runner-stacked-summary-cell :runner="item">
+ <runner-summary-cell :runner="item">
<template #runner-name="{ runner }">
<slot name="runner-name" :runner="runner" :index="index"></slot>
</template>
- </runner-stacked-summary-cell>
+ <template #runner-job-status-badge="{ runner }">
+ <slot name="runner-job-status-badge" :runner="runner" :index="index"></slot>
+ </template>
+ </runner-summary-cell>
</template>
<template #head(owner)="{ label }">
diff --git a/app/assets/javascripts/ci/runner/components/runner_projects.vue b/app/assets/javascripts/ci/runner/components/runner_projects.vue
index 84008e8eee8..4a6e90b44a9 100644
--- a/app/assets/javascripts/ci/runner/components/runner_projects.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_projects.vue
@@ -133,7 +133,7 @@ export default {
:is-owner="isOwner(project.id)"
/>
</template>
- <div v-else class="gl-py-5 gl-text-gray-500">{{ $options.I18N_NO_PROJECTS_FOUND }}</div>
+ <div v-else class="gl-py-5 gl-text-secondary">{{ $options.I18N_NO_PROJECTS_FOUND }}</div>
<runner-pagination
:disabled="loading"
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 584236168ac..70226074993 100644
--- a/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_type_tabs.vue
@@ -59,21 +59,20 @@ export default {
return [
{
title: I18N_ALL_TYPES,
- runnerType: null,
},
...tabs,
];
},
},
methods: {
- onTabSelected({ runnerType }) {
+ onTabSelected(runnerType) {
this.$emit('input', {
...this.value,
runnerType,
pagination: { page: 1 },
});
},
- isTabActive({ runnerType }) {
+ isTabActive(runnerType = null) {
return runnerType === this.value.runnerType;
},
tabBadgeCountVariables(runnerType) {
@@ -102,8 +101,8 @@ export default {
<gl-tab
v-for="tab in tabs"
:key="`${tab.runnerType}`"
- :active="isTabActive(tab)"
- @click="onTabSelected(tab)"
+ :active="isTabActive(tab.runnerType)"
+ @click="onTabSelected(tab.runnerType)"
>
<template #title>
{{ tab.title }}
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
index 97ee8ec3eef..71a145dd4a3 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/paused_token_config.js
@@ -1,5 +1,5 @@
import { __ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import { PARAM_KEY_PAUSED, I18N_PAUSED } from '../../constants';
@@ -24,5 +24,5 @@ export const pausedTokenConfig = {
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
title: title.replace(/\s/g, '\u00a0'),
})),
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
index 117a630719e..4bc32909777 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/status_token_config.js
@@ -1,5 +1,5 @@
import {
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
TOKEN_TITLE_STATUS,
} from '~/vue_shared/components/filtered_search_bar/constants';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
@@ -38,5 +38,5 @@ export const statusTokenConfig = {
// see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
title: title.replace(/\s/g, '\u00a0'),
})),
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
diff --git a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
index fdeba714385..369b214f952 100644
--- a/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
+++ b/app/assets/javascripts/ci/runner/components/search_tokens/tag_token_config.js
@@ -1,5 +1,5 @@
import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { PARAM_KEY_TAG } from '../../constants';
import TagToken from './tag_token.vue';
@@ -8,5 +8,5 @@ export const tagTokenConfig = {
title: s__('Runners|Tags'),
type: PARAM_KEY_TAG,
token: TagToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
};
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 4ad9259f59d..c33c42f3afe 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_count.vue
@@ -16,13 +16,13 @@ import { INSTANCE_TYPE, GROUP_TYPE } from '../../constants';
* <strong/> tag.
*
* ```vue
- * <runner-count-stat
+ * <runner-count
* #default="{ count }"
* :scope="INSTANCE_TYPE"
* :variables="{ status: 'ONLINE' }"
* >
* <strong>{{ count }}</strong>
- * </runner-count-stat>
+ * </runner-count>
* ```
*
* Use `:skip="true"` to prevent data from being fetched and
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 3965e5551f1..2e50dc13d2d 100644
--- a/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
+++ b/app/assets/javascripts/ci/runner/components/stat/runner_stats.vue
@@ -1,5 +1,4 @@
<script>
-import RunnerSingleStat from '~/ci/runner/components/stat/runner_single_stat.vue';
import {
I18N_STATUS_ONLINE,
I18N_STATUS_OFFLINE,
@@ -8,9 +7,19 @@ import {
STATUS_OFFLINE,
STATUS_STALE,
} from '../../constants';
+import RunnerSingleStat from './runner_single_stat.vue';
+import RunnerCount from './runner_count.vue';
+
+/**
+ * Shows general stats about the runners.
+ *
+ * First it checks if there are any runners in this context, and if so,
+ * shows more details for different status.
+ */
export default {
components: {
+ RunnerCount,
RunnerSingleStat,
RunnerUpgradeStatusStats: () =>
import('ee_component/ci/runner/components/stat/runner_upgrade_status_stats.vue'),
@@ -71,19 +80,21 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-flex-wrap gl-py-6">
- <runner-single-stat
- v-for="stat in stats"
- :key="stat.key"
- :scope="scope"
- v-bind="stat.props"
- class="gl-px-5"
- />
+ <runner-count #default="{ count }" :scope="scope" :variables="variables">
+ <div v-if="count" class="gl-display-flex gl-flex-wrap gl-py-6">
+ <runner-single-stat
+ v-for="stat in stats"
+ :key="stat.key"
+ :scope="scope"
+ v-bind="stat.props"
+ class="gl-px-5"
+ />
- <runner-upgrade-status-stats
- class="gl-display-contents"
- :scope="scope"
- :variables="variables"
- />
- </div>
+ <runner-upgrade-status-stats
+ class="gl-display-contents"
+ :scope="scope"
+ :variables="variables"
+ />
+ </div>
+ </runner-count>
</template>
diff --git a/app/assets/javascripts/ci/runner/constants.js b/app/assets/javascripts/ci/runner/constants.js
index dfc5f0c4152..31900a1fe89 100644
--- a/app/assets/javascripts/ci/runner/constants.js
+++ b/app/assets/javascripts/ci/runner/constants.js
@@ -32,6 +32,10 @@ export const I18N_STATUS_NEVER_CONTACTED = s__('Runners|Never contacted');
export const I18N_STATUS_OFFLINE = s__('Runners|Offline');
export const I18N_STATUS_STALE = s__('Runners|Stale');
+// Executor Status
+export const I18N_JOB_STATUS_RUNNING = s__('Runners|Running');
+export const I18N_JOB_STATUS_IDLE = s__('Runners|Idle');
+
// Status help popover
export const I18N_STATUS_POPOVER_TITLE = s__('Runners|Runner statuses');
@@ -82,6 +86,7 @@ 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.',
);
@@ -94,6 +99,7 @@ export const I18N_ADMIN = s__('Runners|Administrator');
// Runner details
export const I18N_DETAILS = s__('Runners|Details');
+export const I18N_JOBS = s__('Runners|Jobs');
export const I18N_ASSIGNED_PROJECTS = s__('Runners|Assigned Projects (%{projectCount})');
export const I18N_FILTER_PROJECTS = s__('Runners|Filter projects');
export const I18N_CLEAR_FILTER_PROJECTS = __('Clear');
@@ -134,6 +140,11 @@ export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
+// CiRunnerJobExecutionStatus
+
+export const JOB_STATUS_RUNNING = 'RUNNING';
+export const JOB_STATUS_IDLE = 'IDLE';
+
// CiRunnerAccessLevel
export const ACCESS_LEVEL_NOT_PROTECTED = 'NOT_PROTECTED';
diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
index 0dff011daaa..6f72509f599 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
@@ -12,6 +12,7 @@ fragment ListItemShared on CiRunner {
createdAt
contactedAt
status(legacyMode: null)
+ jobExecutionStatus
userPermissions {
updateRunner
deleteRunner
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 91c22923075..57ceaa24b6e 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
@@ -82,6 +82,9 @@ export default {
apollo: {
runners: {
query: groupRunnersQuery,
+ context: {
+ isSingleRequest: true,
+ },
fetchPolicy: fetchPolicies.NETWORK_ONLY,
variables() {
return this.variables;
diff --git a/app/assets/javascripts/ci/runner/runner_search_utils.js b/app/assets/javascripts/ci/runner/runner_search_utils.js
index adc832b0600..3dc99baa329 100644
--- a/app/assets/javascripts/ci/runner/runner_search_utils.js
+++ b/app/assets/javascripts/ci/runner/runner_search_utils.js
@@ -176,6 +176,7 @@ export const fromSearchToUrl = (
[PARAM_KEY_RUNNER_TYPE]: [],
[PARAM_KEY_MEMBERSHIP]: [],
[PARAM_KEY_TAG]: [],
+ [PARAM_KEY_PAUSED]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index 9d8cb40b60a..661389f4059 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -13,7 +13,7 @@ import {
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import Api, { DEFAULT_PER_PAGE } from '~/api';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_PAYLOAD_TOO_LARGE } from '~/lib/utils/http_status';
import { __, s__, sprintf } from '~/locale';
import Tracking from '~/tracking';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -145,7 +145,7 @@ export default {
let message = '';
if (error?.response?.data?.message?.name) {
message = this.$options.i18n.uploadErrorMessages.duplicate;
- } else if (error.response.status === httpStatusCodes.PAYLOAD_TOO_LARGE) {
+ } else if (error.response.status === HTTP_STATUS_PAYLOAD_TOO_LARGE) {
message = sprintf(this.$options.i18n.uploadErrorMessages.tooLarge, {
limit: this.fileSizeLimit,
});
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
index c8f5ac1736d..4466a6a8081 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_group_variables.vue
@@ -46,6 +46,7 @@ export default {
:id="graphqlId"
:are-scoped-variables-available="areScopedVariablesAvailable"
component-name="GroupVariables"
+ entity="group"
:full-path="groupPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
index 2c4818e20c1..6326940148a 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_project_variables.vue
@@ -48,6 +48,7 @@ export default {
:id="graphqlId"
:are-scoped-variables-available="true"
component-name="ProjectVariables"
+ entity="project"
:full-path="projectFullPath"
:mutation-data="$options.mutationData"
:query-data="$options.queryData"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
index 94f8cb9e906..00177539cdc 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue
@@ -29,6 +29,7 @@ import {
ENVIRONMENT_SCOPE_LINK_TITLE,
EVENT_LABEL,
EVENT_ACTION,
+ EXPANDED_VARIABLES_NOTE,
EDIT_VARIABLE_ACTION,
VARIABLE_ACTIONS,
variableOptions,
@@ -46,6 +47,7 @@ export default {
awsTipMessage: AWS_TIP_MESSAGE,
containsVariableReferenceMessage: CONTAINS_VARIABLE_REFERENCE_MESSAGE,
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
+ expandedVariablesNote: EXPANDED_VARIABLES_NOTE,
components: {
CiEnvironmentsDropdown,
GlAlert,
@@ -127,7 +129,7 @@ export default {
},
containsVariableReference() {
const regex = /\$/;
- return regex.test(this.variable.value);
+ return regex.test(this.variable.value) && this.isExpanded;
},
displayMaskedError() {
return !this.canMask && this.variable.masked;
@@ -135,6 +137,9 @@ export default {
isEditing() {
return this.mode === EDIT_VARIABLE_ACTION;
},
+ isExpanded() {
+ return !this.variable.raw;
+ },
isTipVisible() {
return !this.isTipDismissed && AWS_TOKEN_CONSTANTS.includes(this.variable.key);
},
@@ -208,6 +213,9 @@ export default {
hideModal() {
this.$refs.modal.hide();
},
+ onShow() {
+ this.setVariableProtectedByDefault();
+ },
resetModalHandler() {
this.resetVariableData();
this.resetValidationErrorEvents();
@@ -220,6 +228,9 @@ export default {
setEnvironmentScope(scope) {
this.variable = { ...this.variable, environmentScope: scope };
},
+ setVariableRaw(expanded) {
+ this.variable = { ...this.variable, raw: !expanded };
+ },
setVariableProtected() {
this.variable = { ...this.variable, protected: true };
},
@@ -275,7 +286,7 @@ export default {
static
lazy
@hidden="resetModalHandler"
- @shown="setVariableProtectedByDefault"
+ @shown="onShow"
>
<form>
<gl-form-combobox
@@ -304,6 +315,13 @@ export default {
class="gl-font-monospace!"
spellcheck="false"
/>
+ <p
+ v-if="variable.raw"
+ class="gl-mt-2 gl-mb-0 text-secondary"
+ data-testid="raw-variable-tip"
+ >
+ {{ __('Variable value will be evaluated as raw string.') }}
+ </p>
</gl-form-group>
<div class="gl-display-flex">
@@ -361,7 +379,6 @@ export default {
{{ __('Export variable to pipelines running on protected branches and tags only.') }}
</p>
</gl-form-checkbox>
-
<gl-form-checkbox
ref="masked-ci-variable"
v-model="variable.masked"
@@ -371,7 +388,7 @@ export default {
<gl-link target="_blank" :href="maskedEnvironmentVariablesLink">
<gl-icon name="question" :size="12" />
</gl-link>
- <p class="gl-mt-2 gl-mb-0 text-secondary">
+ <p class="gl-mt-2 text-secondary">
{{ __('Variable will be masked in job logs.') }}
<span
:class="{
@@ -385,6 +402,24 @@ export default {
}}</gl-link>
</p>
</gl-form-checkbox>
+ <gl-form-checkbox
+ ref="expanded-ci-variable"
+ :checked="isExpanded"
+ data-testid="ci-variable-expanded-checkbox"
+ @change="setVariableRaw"
+ >
+ {{ __('Expand variable reference') }}
+ <gl-link target="_blank" :href="containsVariableReferenceLink">
+ <gl-icon name="question" :size="12" />
+ </gl-link>
+ <p class="gl-mt-2 gl-mb-0 gl-text-secondary">
+ <gl-sprintf :message="$options.expandedVariablesNote">
+ <template #code="{ content }">
+ <code>{{ content }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
+ </gl-form-checkbox>
</gl-form-group>
</form>
<gl-collapse :visible="isTipVisible">
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
index 94fd6c3892c..3c6114b38ce 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_settings.vue
@@ -14,6 +14,11 @@ export default {
required: false,
default: false,
},
+ entity: {
+ type: String,
+ required: false,
+ default: '',
+ },
environments: {
type: Array,
required: false,
@@ -27,7 +32,11 @@ export default {
isLoading: {
type: Boolean,
required: false,
- default: false,
+ },
+ maxVariableLimit: {
+ type: Number,
+ required: false,
+ default: 0,
},
variables: {
type: Array,
@@ -75,7 +84,9 @@ export default {
<div class="row">
<div class="col-lg-12">
<ci-variable-table
+ :entity="entity"
:is-loading="isLoading"
+ :max-variable-limit="maxVariableLimit"
:variables="variables"
@set-selected-variable="setSelectedVariable"
/>
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
index 7ee250cea98..6e39bda0b07 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_shared.vue
@@ -26,6 +26,11 @@ export default {
required: true,
type: String,
},
+ entity: {
+ required: false,
+ type: String,
+ default: '',
+ },
fullPath: {
required: false,
type: String,
@@ -90,6 +95,7 @@ export default {
isInitialLoading: true,
isLoadingMoreItems: false,
loadingCounter: 0,
+ maxVariableLimit: 0,
pageInfo: {},
};
},
@@ -107,6 +113,8 @@ export default {
return this.queryData.ciVariables.lookup(data)?.nodes || [];
},
result({ data }) {
+ this.maxVariableLimit = this.queryData.ciVariables.lookup(data)?.limit || 0;
+
this.pageInfo = this.queryData.ciVariables.lookup(data)?.pageInfo || this.pageInfo;
this.hasNextPage = this.pageInfo?.hasNextPage || false;
@@ -221,9 +229,11 @@ export default {
<template>
<ci-variable-settings
:are-scoped-variables-available="areScopedVariablesAvailable"
+ :entity="entity"
:hide-environment-scope="hideEnvironmentScope"
:is-loading="isLoading"
:variables="ciVariables"
+ :max-variable-limit="maxVariableLimit"
:environments="environments"
@add-variable="addVariable"
@delete-variable="deleteVariable"
diff --git a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
index 3cdcb68e919..345a8def49d 100644
--- a/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci_variable_list/components/ci_variable_table.vue
@@ -1,8 +1,21 @@
<script>
-import { GlButton, GlLoadingIcon, GlModalDirective, GlTable, GlTooltipDirective } from '@gitlab/ui';
-import { s__, __ } from '~/locale';
+import {
+ GlAlert,
+ GlButton,
+ GlLoadingIcon,
+ GlModalDirective,
+ GlTable,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { ADD_CI_VARIABLE_MODAL_ID, variableText } from '../constants';
+import {
+ ADD_CI_VARIABLE_MODAL_ID,
+ DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
+ EXCEEDS_VARIABLE_LIMIT_TEXT,
+ MAXIMUM_VARIABLE_LIMIT_REACHED,
+ variableText,
+} from '../constants';
import { convertEnvironmentScope } from '../utils';
export default {
@@ -41,6 +54,7 @@ export default {
},
],
components: {
+ GlAlert,
GlButton,
GlLoadingIcon,
GlTable,
@@ -51,10 +65,19 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ entity: {
+ type: String,
+ required: false,
+ default: '',
+ },
isLoading: {
type: Boolean,
required: true,
},
+ maxVariableLimit: {
+ type: Number,
+ required: true,
+ },
variables: {
type: Array,
required: true,
@@ -66,6 +89,23 @@ export default {
};
},
computed: {
+ exceedsVariableLimit() {
+ return this.maxVariableLimit > 0 && this.variables.length >= this.maxVariableLimit;
+ },
+ exceedsVariableLimitText() {
+ if (this.exceedsVariableLimit && this.entity) {
+ return sprintf(EXCEEDS_VARIABLE_LIMIT_TEXT, {
+ entity: this.entity,
+ currentVariableCount: this.variables.length,
+ maxVariableLimit: this.maxVariableLimit,
+ });
+ }
+
+ return DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT;
+ },
+ showAlert() {
+ return !this.isLoading && this.exceedsVariableLimit;
+ },
valuesButtonText() {
return this.areValuesHidden ? __('Reveal values') : __('Hide values');
},
@@ -104,17 +144,29 @@ export default {
if (item.masked) {
options.push(s__('CiVariables|Masked'));
}
+ if (!item.raw) {
+ options.push(s__('CiVariables|Expanded'));
+ }
return options.join(', ');
},
},
+ maximumVariableLimitReached: MAXIMUM_VARIABLE_LIMIT_REACHED,
};
</script>
<template>
<div class="ci-variable-table" data-testid="ci-variable-table">
<gl-loading-icon v-if="isLoading" />
+ <gl-alert
+ v-if="showAlert"
+ :dismissible="false"
+ :title="$options.maximumVariableLimitReached"
+ variant="info"
+ >
+ {{ exceedsVariableLimitText }}
+ </gl-alert>
<gl-table
- v-else
+ v-if="!isLoading"
:fields="fields"
:items="variablesWithOptions"
tbody-tr-class="js-ci-variable-row"
@@ -178,7 +230,7 @@ export default {
</div>
</template>
<template #cell(options)="{ item }">
- <span>{{ item.options }}</span>
+ <span data-testid="ci-variable-table-row-options">{{ item.options }}</span>
</template>
<template #cell(environmentScope)="{ item }">
<div
@@ -215,6 +267,14 @@ export default {
</p>
</template>
</gl-table>
+ <gl-alert
+ v-if="showAlert"
+ :dismissible="false"
+ :title="$options.maximumVariableLimitReached"
+ variant="info"
+ >
+ {{ exceedsVariableLimitText }}
+ </gl-alert>
<div class="ci-variable-actions gl-display-flex gl-mt-5">
<gl-button
v-gl-modal-directive="$options.modalId"
@@ -223,6 +283,7 @@ export default {
variant="confirm"
category="primary"
:aria-label="__('Add')"
+ :disabled="exceedsVariableLimit"
@click="setSelectedVariable()"
>{{ __('Add variable') }}</gl-button
>
diff --git a/app/assets/javascripts/ci_variable_list/constants.js b/app/assets/javascripts/ci_variable_list/constants.js
index ccad08ef8b6..828d0724d93 100644
--- a/app/assets/javascripts/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci_variable_list/constants.js
@@ -1,4 +1,4 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
export const ADD_CI_VARIABLE_MODAL_ID = 'add-ci-variable';
@@ -43,6 +43,7 @@ export const defaultVariableState = {
key: '',
masked: false,
protected: false,
+ raw: false,
value: '',
variableType: variableTypes.envType,
};
@@ -69,10 +70,19 @@ export const AWS_SECRET_ACCESS_KEY = 'AWS_SECRET_ACCESS_KEY';
export const AWS_TOKEN_CONSTANTS = [AWS_ACCESS_KEY_ID, AWS_DEFAULT_REGION, AWS_SECRET_ACCESS_KEY];
export const CONTAINS_VARIABLE_REFERENCE_MESSAGE = __(
- 'Values that contain the %{codeStart}$%{codeEnd} character can be considered a variable reference and expanded. %{docsLinkStart}Learn more.%{docsLinkEnd}',
+ 'Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
);
export const ENVIRONMENT_SCOPE_LINK_TITLE = __('Learn more');
+export const EXCEEDS_VARIABLE_LIMIT_TEXT = s__(
+ 'CiVariables|This %{entity} has %{currentVariableCount} defined CI/CD variables. The maximum number of variables per %{entity} is %{maxVariableLimit}. To add new variables, you must reduce the number of defined variables.',
+);
+export const DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT = s__(
+ 'CiVariables|You have reached the maximum number of variables available. To add new variables, you must reduce the number of defined variables.',
+);
+export const MAXIMUM_VARIABLE_LIMIT_REACHED = s__(
+ 'CiVariables|Maximum number of variables reached.',
+);
export const ADD_VARIABLE_ACTION = 'ADD_VARIABLE';
export const EDIT_VARIABLE_ACTION = 'EDIT_VARIABLE';
@@ -85,6 +95,10 @@ export const ADD_MUTATION_ACTION = 'add';
export const UPDATE_MUTATION_ACTION = 'update';
export const DELETE_MUTATION_ACTION = 'delete';
+export const EXPANDED_VARIABLES_NOTE = __(
+ '%{codeStart}$%{codeEnd} will be treated as the start of a reference to another variable.',
+);
+
export const environmentFetchErrorText = __(
'There was an error fetching the environments information.',
);
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
index c44ee2ecc1d..24388637672 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation addGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPath:
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
index 53e9b411dd2..f7c8e209ccd 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation deleteGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPa
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
index 2dddca14bd8..757e61a5cd3 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation updateGroupVariable($variable: CiVariable!, $endpoint: String!, $fullPa
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
index 39504770e33..fa315084d86 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql
@@ -16,6 +16,7 @@ mutation addProjectVariable($variable: CiVariable!, $endpoint: String!, $fullPat
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
index f55c255e332..c3358cc35b9 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql
@@ -21,6 +21,7 @@ mutation deleteProjectVariable(
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
index fc589e8a939..fde92cef4cb 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql
@@ -21,6 +21,7 @@ mutation updateProjectVariable(
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
index b5555fe4401..900154cd24d 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/group_variables.query.graphql
@@ -5,6 +5,7 @@ query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
group(fullPath: $fullPath) {
id
ciVariables(after: $after, first: $first) {
+ limit
pageInfo {
...PageInfo
}
@@ -14,6 +15,7 @@ query getGroupVariables($after: String, $first: Int = 100, $fullPath: ID!) {
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
index 08b5bf7af16..ee75eba7547 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/project_variables.query.graphql
@@ -5,6 +5,7 @@ query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
project(fullPath: $fullPath) {
id
ciVariables(after: $after, first: $first) {
+ limit
pageInfo {
...PageInfo
}
@@ -13,6 +14,7 @@ query getProjectVariables($after: String, $first: Int = 100, $fullPath: ID!) {
environmentScope
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
index 2667d6606fe..9b255c3c182 100644
--- a/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
+++ b/app/assets/javascripts/ci_variable_list/graphql/queries/variables.query.graphql
@@ -11,6 +11,7 @@ query getVariables($after: String, $first: Int = 100) {
... on CiInstanceVariable {
masked
protected
+ raw
}
}
}
diff --git a/app/assets/javascripts/clusters_list/clusters_util.js b/app/assets/javascripts/clusters_list/clusters_util.js
index ee36a295513..25a8426500e 100644
--- a/app/assets/javascripts/clusters_list/clusters_util.js
+++ b/app/assets/javascripts/clusters_list/clusters_util.js
@@ -1,10 +1,14 @@
-import { ACTIVE_CONNECTION_TIME } from './constants';
+import { ACTIVE_CONNECTION_TIME, NAME_MAX_LENGTH } from './constants';
+
+function getTruncatedName(name) {
+ return name.substring(0, NAME_MAX_LENGTH);
+}
export function generateAgentRegistrationCommand({ name, token, version, address }) {
return `helm repo add gitlab https://charts.gitlab.io
helm repo update
helm upgrade --install ${name} gitlab/gitlab-agent \\
- --namespace gitlab-agent \\
+ --namespace gitlab-agent-${getTruncatedName(name)} \\
--create-namespace \\
--set image.tag=v${version} \\
--set config.token=${token} \\
diff --git a/app/assets/javascripts/clusters_list/components/agent_token.vue b/app/assets/javascripts/clusters_list/components/agent_token.vue
index 4dd6d84566c..93c37226a09 100644
--- a/app/assets/javascripts/clusters_list/components/agent_token.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_token.vue
@@ -1,22 +1,24 @@
<script>
-import { GlAlert, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlFormInputGroup, GlLink, GlSprintf, GlIcon } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
import CodeBlock from '~/vue_shared/components/code_block.vue';
import { generateAgentRegistrationCommand } from '../clusters_util';
-import { I18N_AGENT_TOKEN } from '../constants';
+import { I18N_AGENT_TOKEN, HELM_VERSION_POLICY_URL } from '../constants';
export default {
i18n: I18N_AGENT_TOKEN,
advancedInstallPath: helpPagePath('user/clusters/agent/install/index', {
anchor: 'advanced-installation-method',
}),
+ HELM_VERSION_POLICY_URL,
components: {
GlAlert,
CodeBlock,
GlFormInputGroup,
GlLink,
GlSprintf,
+ GlIcon,
ModalCopyButton,
},
inject: ['kasAddress', 'kasVersion'],
@@ -77,6 +79,11 @@ export default {
<p>
{{ $options.i18n.basicInstallBody }}
+ <gl-sprintf :message="$options.i18n.helmVersionText">
+ <template #link="{ content }"
+ ><gl-link :href="$options.HELM_VERSION_POLICY_URL" target="_blank"
+ >{{ content }} <gl-icon name="external-link" :size="12" /></gl-link></template
+ ></gl-sprintf>
</p>
<p class="gl-display-flex gl-align-items-flex-start">
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 bde76c46b4b..365e0384d87 100644
--- a/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
+++ b/app/assets/javascripts/clusters_list/components/available_agents_dropdown.vue
@@ -1,23 +1,13 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownText,
- GlSearchBoxByType,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlCollapsibleListbox, GlButton, GlSprintf } from '@gitlab/ui';
import { I18N_AVAILABLE_AGENTS_DROPDOWN } from '../constants';
export default {
name: 'AvailableAgentsDropdown',
i18n: I18N_AVAILABLE_AGENTS_DROPDOWN,
components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownText,
- GlSearchBoxByType,
+ GlCollapsibleListbox,
+ GlButton,
GlSprintf,
},
props: {
@@ -46,13 +36,21 @@ export default {
return this.selectedAgent;
},
+ dropdownItems() {
+ return this.availableAgents.map((agent) => {
+ return {
+ value: agent,
+ text: agent,
+ };
+ });
+ },
shouldRenderCreateButton() {
return this.searchTerm && !this.availableAgents.includes(this.searchTerm);
},
filteredResults() {
const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
- return this.availableAgents.filter((resultString) =>
- resultString.toLowerCase().includes(lowerCasedSearchTerm),
+ return this.dropdownItems.filter((item) =>
+ item.value.toLowerCase().includes(lowerCasedSearchTerm),
);
},
},
@@ -60,59 +58,48 @@ export default {
selectAgent(agent) {
this.$emit('agentSelected', agent);
this.selectedAgent = agent;
- this.clearSearch();
- },
- isSelected(agent) {
- return this.selectedAgent === agent;
- },
- clearSearch() {
- this.searchTerm = '';
- },
- focusSearch() {
- this.$refs.searchInput.focusInput();
- },
- handleShow() {
- this.clearSearch();
- this.focusSearch();
+
+ this.$refs.dropdown.closeAndFocus();
},
onKeyEnter() {
if (!this.searchTerm?.length) {
return;
}
- this.$refs.dropdown.hide();
this.selectAgent(this.searchTerm);
},
+ searchAgent(searchQuery) {
+ this.searchTerm = searchQuery;
+ },
},
};
</script>
<template>
- <gl-dropdown ref="dropdown" :text="dropdownText" :loading="isRegistering" @shown="handleShow">
- <template #header>
- <gl-search-box-by-type
- ref="searchInput"
- v-model.trim="searchTerm"
- @keydown.enter.stop.prevent="onKeyEnter"
- />
- </template>
- <gl-dropdown-item
- v-for="agent in filteredResults"
- :key="agent"
- :is-checked="isSelected(agent)"
- is-check-item
- @click="selectAgent(agent)"
+ <div @keydown.enter.stop.prevent="onKeyEnter">
+ <gl-collapsible-listbox
+ ref="dropdown"
+ v-model="selectedAgent"
+ class="gl-w-full"
+ toggle-class="select-agent-dropdown"
+ :items="filteredResults"
+ :toggle-text="dropdownText"
+ :loading="isRegistering"
+ :searchable="true"
+ :no-results-text="$options.i18n.noResults"
+ @search="searchAgent"
+ @select="selectAgent"
>
- {{ agent }}
- </gl-dropdown-item>
- <gl-dropdown-text v-if="!filteredResults.length" ref="noMatchingResults">{{
- $options.i18n.noResults
- }}</gl-dropdown-text>
- <template v-if="shouldRenderCreateButton">
- <gl-dropdown-divider />
- <gl-dropdown-item data-testid="create-config-button" @click="selectAgent(searchTerm)">
- <gl-sprintf :message="$options.i18n.createButton">
- <template #searchTerm>{{ searchTerm }}</template>
- </gl-sprintf>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ <template v-if="shouldRenderCreateButton" #footer>
+ <gl-button
+ category="tertiary"
+ class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!"
+ :class="{ 'gl-mt-3': !filteredResults.length }"
+ @click="selectAgent(searchTerm)"
+ >
+ <gl-sprintf :message="$options.i18n.createButton">
+ <template #searchTerm>{{ searchTerm }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </template>
+ </gl-collapsible-listbox>
+ </div>
</template>
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 7bc8a1a7304..615754459d6 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -4,6 +4,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
export const MAX_LIST_COUNT = 25;
export const INSTALL_AGENT_MODAL_ID = 'install-agent';
export const ACTIVE_CONNECTION_TIME = 480000;
+export const NAME_MAX_LENGTH = 50;
export const CLUSTER_ERRORS = {
default: {
@@ -100,6 +101,9 @@ export const I18N_AGENT_TOKEN = {
basicInstallBody: s__(
'ClusterAgents|From a terminal, connect to your cluster and run this command. The token is included in the command.',
),
+ helmVersionText: s__(
+ 'ClusterAgents|Use a Helm version compatible with your Kubernetes version (see %{linkStart}Helm version support policy%{linkEnd}).',
+ ),
advancedInstallTitle: s__('ClusterAgents|Advanced installation methods'),
advancedInstallBody: s__(
@@ -107,6 +111,8 @@ export const I18N_AGENT_TOKEN = {
),
};
+export const HELM_VERSION_POLICY_URL = 'https://helm.sh/docs/topics/version_skew/';
+
export const I18N_AGENT_MODAL = {
registerAgentButton: s__('ClusterAgents|Register'),
close: __('Close'),
diff --git a/app/assets/javascripts/constants.js b/app/assets/javascripts/constants.js
new file mode 100644
index 00000000000..c56d45166a0
--- /dev/null
+++ b/app/assets/javascripts/constants.js
@@ -0,0 +1,3 @@
+import { s__ } from '~/locale';
+
+export const MODIFIER_KEY = window.gl?.client?.isMac ? '⌘' : s__('KeyboardKey|Ctrl+');
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
index a9668ebdb69..98b7203778f 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
@@ -166,9 +166,7 @@ export default {
icon="arrow-left"
@click.prevent.stop="showCustomLanguageInput = false"
/>
- <p
- class="gl-text-center gl-new-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!"
- >
+ <p class="gl-text-center gl-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!">
{{ __('Create custom type') }}
</p>
</div>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 22381377389..53a37fc0c51 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -11,7 +11,7 @@ import FormattingBubbleMenu from './bubble_menus/formatting_bubble_menu.vue';
import CodeBlockBubbleMenu from './bubble_menus/code_block_bubble_menu.vue';
import LinkBubbleMenu from './bubble_menus/link_bubble_menu.vue';
import MediaBubbleMenu from './bubble_menus/media_bubble_menu.vue';
-import TopToolbar from './top_toolbar.vue';
+import FormattingToolbar from './formatting_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
export default {
@@ -20,7 +20,7 @@ export default {
ContentEditorAlert,
ContentEditorProvider,
TiptapEditorContent,
- TopToolbar,
+ FormattingToolbar,
FormattingBubbleMenu,
CodeBlockBubbleMenu,
LinkBubbleMenu,
@@ -57,6 +57,11 @@ export default {
default: false,
validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus),
},
+ useBottomToolbar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -163,8 +168,8 @@ export default {
class="md-area"
:class="{ 'is-focused': focused }"
>
- <top-toolbar ref="toolbar" class="gl-mb-4" />
- <div class="gl-relative">
+ <formatting-toolbar v-if="!useBottomToolbar" ref="toolbar" class="gl-border-b" />
+ <div class="gl-relative gl-mt-4">
<formatting-bubble-menu />
<code-block-bubble-menu />
<link-bubble-menu />
@@ -176,6 +181,7 @@ export default {
/>
<loading-indicator v-if="isLoading" />
</div>
+ <formatting-toolbar v-if="useBottomToolbar" ref="toolbar" class="gl-border-t" />
</div>
</div>
</content-editor-provider>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index 460368b6a11..8a25ad3fd96 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -24,9 +24,7 @@ export default {
};
</script>
<template>
- <div
- class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3 gl-border-b-solid gl-border-b-1 gl-border-b-gray-200"
- >
+ <div class="gl-display-flex gl-flex-wrap gl-pb-3 gl-pt-3">
<toolbar-text-style-dropdown
data-testid="text-styles"
class="gl-mr-3"
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index 001b34a00fa..37e6ef61d50 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -210,10 +210,10 @@ export default {
<template>
<ul
:class="{ show: items.length > 0 }"
- class="gl-new-dropdown dropdown-menu gl-relative"
+ class="gl-dropdown dropdown-menu gl-relative"
data-testid="content-editor-suggestions-dropdown"
>
- <div class="gl-new-dropdown-inner gl-overflow-y-auto">
+ <div class="gl-dropdown-inner gl-overflow-y-auto">
<gl-dropdown-item
v-for="(item, index) in items"
ref="dropdownItems"
diff --git a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
index 6bb122153ef..93b31ea7d20 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_more_dropdown.vue
@@ -58,6 +58,9 @@ export default {
right
lazy
>
+ <gl-dropdown-item @click="insert('comment')">
+ {{ __('Comment') }}
+ </gl-dropdown-item>
<gl-dropdown-item @click="insert('codeBlock')">
{{ __('Code block') }}
</gl-dropdown-item>
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index 27432b1e18b..1d85bfcc965 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -23,6 +23,10 @@ export default CodeBlockLowlight.extend({
// eslint-disable-next-line @gitlab/require-i18n-strings
default: 'code highlight',
},
+ langParams: {
+ default: null,
+ parseHTML: (element) => element.dataset.langParams,
+ },
};
},
addInputRules() {
diff --git a/app/assets/javascripts/content_editor/extensions/comment.js b/app/assets/javascripts/content_editor/extensions/comment.js
new file mode 100644
index 00000000000..8e247e552a3
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/comment.js
@@ -0,0 +1,49 @@
+import { Node, textblockTypeInputRule } from '@tiptap/core';
+
+export const commentInputRegex = /^<!--[\s\n]$/;
+
+export default Node.create({
+ name: 'comment',
+ content: 'text*',
+ marks: '',
+ group: 'block',
+ code: true,
+ isolating: true,
+ defining: true,
+
+ parseHTML() {
+ return [
+ {
+ tag: 'comment',
+ preserveWhitespace: 'full',
+ getContent(element, schema) {
+ const node = schema.node('paragraph', {}, [
+ schema.text(
+ element.textContent.replace(/&#x([0-9A-F]{2,4});/gi, (_, code) =>
+ String.fromCharCode(parseInt(code, 16)),
+ ) || ' ',
+ ),
+ ]);
+ return node.content;
+ },
+ },
+ ];
+ },
+
+ renderHTML() {
+ return [
+ 'pre',
+ { class: 'gl-p-0 gl-border-0 gl-bg-transparent gl-text-gray-300' },
+ ['span', { class: 'content-editor-comment' }, 0],
+ ];
+ },
+
+ addInputRules() {
+ return [
+ textblockTypeInputRule({
+ find: commentInputRegex,
+ type: this.type,
+ }),
+ ];
+ },
+});
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 65849ec4d0d..fc4c108b773 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -52,6 +52,22 @@ export default Image.extend({
return img.getAttribute('title');
},
},
+ width: {
+ default: null,
+ parseHTML: (element) => {
+ const img = resolveImageEl(element);
+
+ return img.getAttribute('width');
+ },
+ },
+ height: {
+ default: null,
+ parseHTML: (element) => {
+ const img = resolveImageEl(element);
+
+ return img.getAttribute('height');
+ },
+ },
isReference: {
default: false,
renderHTML: () => '',
@@ -76,6 +92,8 @@ export default Image.extend({
src: HTMLAttributes.src,
alt: HTMLAttributes.alt,
title: HTMLAttributes.title,
+ width: HTMLAttributes.width,
+ height: HTMLAttributes.height,
},
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/reference_label.js b/app/assets/javascripts/content_editor/extensions/reference_label.js
index 716e191c3d5..9dff0b7a689 100644
--- a/app/assets/javascripts/content_editor/extensions/reference_label.js
+++ b/app/assets/javascripts/content_editor/extensions/reference_label.js
@@ -1,5 +1,5 @@
import { VueNodeViewRenderer } from '@tiptap/vue-2';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
import LabelWrapper from '../components/wrappers/label.vue';
import Reference from './reference';
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index ba9ce705c62..61c6be574d0 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -10,6 +10,7 @@ import BulletList from '../extensions/bullet_list';
import Code from '../extensions/code';
import CodeBlockHighlight from '../extensions/code_block_highlight';
import ColorChip from '../extensions/color_chip';
+import Comment from '../extensions/comment';
import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
@@ -100,6 +101,7 @@ export const createContentEditor = ({
BulletList,
Code,
ColorChip,
+ Comment,
CodeBlockHighlight,
DescriptionItem,
DescriptionList,
diff --git a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
index fa46bd9ff81..796dc06ad93 100644
--- a/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
+++ b/app/assets/javascripts/content_editor/services/gl_api_markdown_deserializer.js
@@ -1,4 +1,5 @@
import { DOMParser as ProseMirrorDOMParser } from 'prosemirror-model';
+import { replaceCommentsWith } from '~/lib/utils/dom_utils';
export default ({ render }) => {
/**
@@ -22,7 +23,9 @@ export default ({ render }) => {
if (!html) return {};
const parser = new DOMParser();
- const { body } = parser.parseFromString(html, 'text/html');
+ const { body } = parser.parseFromString(`<body>${html}</body>`, 'text/html');
+
+ replaceCommentsWith(body, 'comment');
// append original source as a comment that nodes can access
body.append(document.createComment(markdown));
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 958c27c281a..4e29f85004b 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -12,6 +12,7 @@ import DescriptionItem from '../extensions/description_item';
import DescriptionList from '../extensions/description_list';
import Details from '../extensions/details';
import DetailsContent from '../extensions/details_content';
+import Comment from '../extensions/comment';
import Diagram from '../extensions/diagram';
import Emoji from '../extensions/emoji';
import Figure from '../extensions/figure';
@@ -50,6 +51,7 @@ import Text from '../extensions/text';
import Video from '../extensions/video';
import WordBreak from '../extensions/word_break';
import {
+ renderComment,
renderCodeBlock,
renderHardBreak,
renderTable,
@@ -130,6 +132,7 @@ const defaultSerializerConfig = {
}),
[BulletList.name]: preserveUnchanged(renderBulletList),
[CodeBlockHighlight.name]: preserveUnchanged(renderCodeBlock),
+ [Comment.name]: renderComment,
[Diagram.name]: preserveUnchanged(renderCodeBlock),
[DescriptionList.name]: renderHTMLNode('dl', true),
[DescriptionItem.name]: (state, node, parent, index) => {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 5c0cb21075a..131c79357bf 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -308,7 +308,7 @@ export function renderHardBreak(state, node, parent, index) {
}
export function renderImage(state, node) {
- const { alt, canonicalSrc, src, title, isReference } = node.attrs;
+ const { alt, canonicalSrc, src, title, width, height, isReference } = node.attrs;
if (isString(src) || isString(canonicalSrc)) {
const quotedTitle = title ? ` ${state.quote(title)}` : '';
@@ -316,7 +316,17 @@ export function renderImage(state, node) {
? `[${canonicalSrc}]`
: `(${state.esc(canonicalSrc || src)}${quotedTitle})`;
- state.write(`![${state.esc(alt || '')}]${sourceExpression}`);
+ const sizeAttributes = [];
+ if (width) {
+ sizeAttributes.push(`width=${JSON.stringify(width)}`);
+ }
+ if (height) {
+ sizeAttributes.push(`height=${JSON.stringify(height)}`);
+ }
+
+ const attributes = sizeAttributes.length ? `{${sizeAttributes.join(' ')}}` : '';
+
+ state.write(`![${state.esc(alt || '')}]${sourceExpression}${attributes}`);
}
}
@@ -324,8 +334,19 @@ export function renderPlayable(state, node) {
renderImage(state, node);
}
+export function renderComment(state, node) {
+ state.text('<!--');
+ state.text(node.textContent);
+ state.text('-->');
+ state.closeBlock(node);
+}
+
export function renderCodeBlock(state, node) {
- state.write(`\`\`\`${node.attrs.language || ''}\n`);
+ state.write(
+ `\`\`\`${
+ (node.attrs.language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '')
+ }\n`,
+ );
state.text(node.textContent, false);
state.ensureNewLine();
state.write('```');
diff --git a/app/assets/javascripts/crm/components/form.vue b/app/assets/javascripts/crm/components/crm_form.vue
index ea6a6892bbd..ea6a6892bbd 100644
--- a/app/assets/javascripts/crm/components/form.vue
+++ b/app/assets/javascripts/crm/components/crm_form.vue
diff --git a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
index b29089519e2..a851c7a9e85 100644
--- a/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
+++ b/app/assets/javascripts/crm/contacts/components/contact_form_wrapper.vue
@@ -2,7 +2,7 @@
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_CONTACT, TYPE_GROUP } from '~/graphql_shared/constants';
-import ContactForm from '../../components/form.vue';
+import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from '../../organizations/components/graphql/get_group_organizations.query.graphql';
import getGroupContactsQuery from './graphql/get_group_contacts.query.graphql';
import createContactMutation from './graphql/create_contact.mutation.graphql';
@@ -10,7 +10,7 @@ import updateContactMutation from './graphql/update_contact.mutation.graphql';
export default {
components: {
- ContactForm,
+ CrmForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
@@ -111,7 +111,7 @@ export default {
</script>
<template>
- <contact-form
+ <crm-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.contacts"
diff --git a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
index 32900d45f22..01bff4b69d6 100644
--- a/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
+++ b/app/assets/javascripts/crm/organizations/components/organization_form_wrapper.vue
@@ -2,14 +2,14 @@
import { s__, __ } from '~/locale';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_CRM_ORGANIZATION, TYPE_GROUP } from '~/graphql_shared/constants';
-import OrganizationForm from '../../components/form.vue';
+import CrmForm from '../../components/crm_form.vue';
import getGroupOrganizationsQuery from './graphql/get_group_organizations.query.graphql';
import createOrganizationMutation from './graphql/create_organization.mutation.graphql';
import updateOrganizationMutation from './graphql/update_organization.mutation.graphql';
export default {
components: {
- OrganizationForm,
+ CrmForm,
},
inject: ['groupFullPath', 'groupId'],
props: {
@@ -73,7 +73,7 @@ export default {
</script>
<template>
- <organization-form
+ <crm-form
:drawer-open="true"
:get-query="getQuery"
get-query-node-path="group.organizations"
diff --git a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
index 411e482b0ce..c6aeb6c726d 100644
--- a/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
+++ b/app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue
@@ -185,7 +185,6 @@ export default {
name="prometheus_metric[title]"
class="form-control"
:placeholder="s__('Metrics|e.g. Throughput')"
- data-qa-selector="custom_metric_prometheus_title_field"
required
/>
<span class="form-text text-muted">{{ s__('Metrics|Used as a title for the chart') }}</span>
@@ -209,7 +208,6 @@ export default {
<gl-form-input
id="prometheus_metric_query"
v-model.trim="query"
- data-qa-selector="custom_metric_prometheus_query_field"
name="prometheus_metric[query]"
class="form-control"
:placeholder="s__('Metrics|e.g. rate(http_requests_total[5m])')"
@@ -247,7 +245,6 @@ export default {
<gl-form-input
id="prometheus_metric_y_label"
v-model="yLabel"
- data-qa-selector="custom_metric_prometheus_y_label_field"
name="prometheus_metric[y_label]"
class="form-control"
:placeholder="s__('Metrics|e.g. Requests/second')"
@@ -267,7 +264,6 @@ export default {
<gl-form-input
id="prometheus_metric_unit"
v-model="unit"
- data-qa-selector="custom_metric_prometheus_unit_label_field"
name="prometheus_metric[unit]"
class="form-control"
:placeholder="s__('Metrics|e.g. req/sec')"
@@ -282,7 +278,6 @@ export default {
<gl-form-input
id="prometheus_metric_legend"
v-model="legend"
- data-qa-selector="custom_metric_prometheus_legend_label_field"
name="prometheus_metric[legend]"
class="form-control"
:placeholder="s__('Metrics|e.g. HTTP requests')"
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 81d74c64124..48ab9ce0a3c 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -13,27 +13,7 @@ import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { s__ } from '~/locale';
-
-function defaultData() {
- return {
- expiresAt: null,
- name: '',
- newTokenDetails: null,
- readRepository: false,
- writeRepository: false,
- readRegistry: false,
- writeRegistry: false,
- readPackageRegistry: false,
- writePackageRegistry: false,
- username: '',
- placeholders: {
- link: { link: ['link_start', 'link_end'] },
- i: { i: ['i_start', 'i_end'] },
- code: { code: ['code_start', 'code_end'] },
- },
- };
-}
+import translations from '../deploy_token_translations';
export default {
components: {
@@ -72,45 +52,9 @@ export default {
},
data() {
- return defaultData();
- },
- translations: {
- addTokenButton: s__('DeployTokens|Create deploy token'),
- addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
- addTokenExpiryDescription: s__(
- 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
- ),
- addTokenHeader: s__('DeployTokens|New deploy token'),
- addTokenDescription: s__(
- 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
- ),
- addTokenNameLabel: s__('DeployTokens|Name'),
- addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
- addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
- addTokenUsernameDescription: s__(
- 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
- ),
- addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
- newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
- newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
- newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
- newTokenDescription: s__(
- 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
- ),
- newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
- newTokenUsernameCopy: s__('DeployTokens|Copy username'),
- newTokenUsernameDescription: s__(
- 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
- ),
- readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
- readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
- writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
- readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
- writePackageRegistryHelp: s__(
- 'DeployTokens|Allows read and write access to the package registry.',
- ),
- createTokenFailedAlert: s__('DeployTokens|Failed to create a new deployment token'),
+ return this.defaultData();
},
+ translations,
computed: {
formattedExpiryDate() {
return this.expiresAt ? formatDate(this.expiresAt, 'yyyy-mm-dd') : '';
@@ -122,20 +66,78 @@ export default {
},
},
methods: {
+ defaultData() {
+ return {
+ expiresAt: null,
+ name: '',
+ newTokenDetails: null,
+ readRepository: false,
+ writeRepository: false,
+ readRegistry: false,
+ writeRegistry: false,
+ readPackageRegistry: false,
+ writePackageRegistry: false,
+ scopes: [
+ {
+ id: 'deploy_token_read_repository',
+ isShown: true,
+ value: false,
+ helpText: this.$options.translations.readRepositoryHelp,
+ scopeName: 'read_repository',
+ },
+ {
+ id: 'deploy_token_read_registry',
+ isShown: this.$props.containerRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.readRegistryHelp,
+ scopeName: 'read_registry',
+ },
+ {
+ id: 'deploy_token_write_registry',
+ isShown: this.$props.containerRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.writeRegistryHelp,
+ scopeName: 'write_registry',
+ },
+ {
+ id: 'deploy_token_read_package_registry',
+ isShown: this.$props.packagesRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.readPackageRegistryHelp,
+ scopeName: 'read_package_registry',
+ },
+ {
+ id: 'deploy_token_write_package_registry',
+ isShown: this.$props.packagesRegistryEnabled,
+ value: false,
+ helpText: this.$options.translations.writePackageRegistryHelp,
+ scopeName: 'write_package_registry',
+ },
+ ],
+ username: '',
+ placeholders: {
+ link: { link: ['link_start', 'link_end'] },
+ i: { i: ['i_start', 'i_end'] },
+ code: { code: ['code_start', 'code_end'] },
+ },
+ };
+ },
createDeployToken() {
+ const scopes = {};
+ this.scopes.forEach((scope) => {
+ scopes[scope.scopeName] = scope.value;
+ });
+ const body = {
+ deploy_token: {
+ expires_at: this.expiresAt,
+ name: this.name,
+ username: this.username,
+ ...scopes,
+ },
+ };
+
return axios
- .post(this.createNewTokenPath, {
- deploy_token: {
- expires_at: this.expiresAt,
- name: this.name,
- read_repository: this.readRepository,
- read_registry: this.readRegistry,
- write_registry: this.writeRegistry,
- read_package_registry: this.readPackageRegistry,
- write_package_registry: this.writePackageRegistry,
- username: this.username,
- },
- })
+ .post(this.createNewTokenPath, body)
.then((response) => {
this.newTokenDetails = response.data;
this.resetData();
@@ -152,7 +154,7 @@ export default {
});
},
resetData() {
- const newData = defaultData();
+ const newData = this.defaultData();
delete newData.newTokenDetails;
Object.keys(newData).forEach((k) => {
this[k] = newData[k];
@@ -269,55 +271,19 @@ export default {
>
<div id="deploy-token-scopes">
<!-- eslint-disable @gitlab/vue-require-i18n-strings -->
- <gl-form-checkbox
- id="deploy_token_read_repository"
- v-model="readRepository"
- name="deploy_token_read_repository"
- data-qa-selector="deploy_token_read_repository_checkbox"
- >
- read_repository
- <template #help>{{ $options.translations.readRepositoryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="containerRegistryEnabled"
- id="deploy_token_read_registry"
- v-model="readRegistry"
- name="deploy_token_read_registry"
- data-qa-selector="deploy_token_read_registry_checkbox"
- >
- read_registry
- <template #help>{{ $options.translations.readRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="containerRegistryEnabled"
- id="deploy_token_write_registry"
- v-model="writeRegistry"
- name="deploy_token_write_registry"
- data-qa-selector="deploy_token_write_registry_checkbox"
- >
- write_registry
- <template #help>{{ $options.translations.writeRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="packagesRegistryEnabled"
- id="deploy_token_read_package_registry"
- v-model="readPackageRegistry"
- name="deploy_token_read_package_registry"
- data-qa-selector="deploy_token_read_package_registry_checkbox"
- >
- read_package_registry
- <template #help>{{ $options.translations.readPackageRegistryHelp }}</template>
- </gl-form-checkbox>
- <gl-form-checkbox
- v-if="packagesRegistryEnabled"
- id="deploy_token_write_package_registry"
- v-model="writePackageRegistry"
- name="deploy_token_write_package_registry"
- data-qa-selector="deploy_token_write_package_registry_checkbox"
- >
- write_package_registry
- <template #help>{{ $options.translations.writePackageRegistryHelp }}</template>
- </gl-form-checkbox>
+ <template v-for="scope in scopes">
+ <gl-form-checkbox
+ v-if="scope.isShown"
+ :id="scope.id"
+ :key="scope.id"
+ v-model="scope.value"
+ :name="scope.id"
+ :data-qa-selector="`${scope.id}_checkbox`"
+ >
+ {{ scope.scopeName }}
+ <template #help>{{ scope.helpText }}</template>
+ </gl-form-checkbox>
+ </template>
<!-- eslint-enable @gitlab/vue-require-i18n-strings -->
</div>
</gl-form-group>
diff --git a/app/assets/javascripts/deploy_tokens/deploy_token_translations.js b/app/assets/javascripts/deploy_tokens/deploy_token_translations.js
new file mode 100644
index 00000000000..3767e9e6170
--- /dev/null
+++ b/app/assets/javascripts/deploy_tokens/deploy_token_translations.js
@@ -0,0 +1,41 @@
+import { s__ } from '~/locale';
+
+const translations = {
+ addTokenButton: s__('DeployTokens|Create deploy token'),
+ addTokenExpiryLabel: s__('DeployTokens|Expiration date (optional)'),
+ addTokenExpiryDescription: s__(
+ 'DeployTokens|Enter an expiration date for your token. Defaults to never expire.',
+ ),
+ addTokenHeader: s__('DeployTokens|New deploy token'),
+ addTokenDescription: s__(
+ 'DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}',
+ ),
+ addTokenNameLabel: s__('DeployTokens|Name'),
+ addTokenNameDescription: s__('DeployTokens|Enter a unique name for your deploy token.'),
+ addTokenScopesLabel: s__('DeployTokens|Scopes (select at least one)'),
+ addTokenUsernameDescription: s__(
+ 'DeployTokens|Enter a username for your token. Defaults to %{code_start}gitlab+deploy-token-{n}%{code_end}.',
+ ),
+ addTokenUsernameLabel: s__('DeployTokens|Username (optional)'),
+ newTokenCopyMessage: s__('DeployTokens|Copy deploy token'),
+ newProjectTokenCreated: s__('DeployTokens|Your new project deploy token has been created.'),
+ newGroupTokenCreated: s__('DeployTokens|Your new group deploy token has been created.'),
+ newTokenDescription: s__(
+ 'DeployTokens|Use this token as a password. Save it. This password can %{i_start}not%{i_end} be recovered.',
+ ),
+ newTokenMessage: s__('DeployTokens|Your New Deploy Token'),
+ newTokenUsernameCopy: s__('DeployTokens|Copy username'),
+ newTokenUsernameDescription: s__(
+ 'DeployTokens|This username supports access. %{link_start}What kind of access?%{link_end}',
+ ),
+ readRepositoryHelp: s__('DeployTokens|Allows read-only access to the repository.'),
+ readRegistryHelp: s__('DeployTokens|Allows read-only access to registry images.'),
+ writeRegistryHelp: s__('DeployTokens|Allows read and write access to registry images.'),
+ readPackageRegistryHelp: s__('DeployTokens|Allows read-only access to the package registry.'),
+ writePackageRegistryHelp: s__(
+ 'DeployTokens|Allows read and write access to the package registry.',
+ ),
+ createTokenFailedAlert: s__('DeployTokens|Failed to create a new deployment token'),
+};
+
+export default translations;
diff --git a/app/assets/javascripts/deprecated_jquery_dropdown/render.js b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
index 0f612989bb4..97698d55011 100644
--- a/app/assets/javascripts/deprecated_jquery_dropdown/render.js
+++ b/app/assets/javascripts/deprecated_jquery_dropdown/render.js
@@ -149,7 +149,7 @@ function renderLink(row, data, { options, group, index }) {
}
function getOptionRenderer({ options, instance }) {
- return options.renderRow && ((li, data) => options.renderRow(data, instance));
+ return options.renderRow && ((li, data, params) => options.renderRow(data, instance, params));
}
function getRenderer(data, params) {
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 2ac62b9b927..c090a66a69d 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -15,6 +15,7 @@ import Autosize from 'autosize';
import $ from 'jquery';
import { escape, uniqueId } from 'lodash';
import Vue from 'vue';
+import { createAlert, VARIANT_INFO } from '~/flash';
import '~/lib/utils/jquery_at_who';
import AjaxCache from '~/lib/utils/ajax_cache';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
@@ -24,7 +25,6 @@ import * as constants from '~/notes/constants';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import Autosave from './autosave';
import loadAwardsHandler from './awards_handler';
-import createFlash from './flash';
import { defaultAutocompleteConfig } from './gfm_auto_complete';
import GLForm from './gl_form';
import axios from './lib/utils/axios_utils';
@@ -40,6 +40,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import { getLocationHash } from './lib/utils/url_utility';
import { sprintf, s__, __ } from './locale';
import TaskList from './task_list';
+import '~/behaviors/markdown/init_gfm';
window.autosize = Autosize;
@@ -81,7 +82,7 @@ export default class Notes {
this.keydownNoteText = this.keydownNoteText.bind(this);
this.toggleCommitList = this.toggleCommitList.bind(this);
this.postComment = this.postComment.bind(this);
- this.clearFlashWrapper = this.clearFlash.bind(this);
+ this.clearAlertWrapper = this.clearAlert.bind(this);
this.onHashChange = this.onHashChange.bind(this);
this.notes_url = notes_url;
@@ -431,9 +432,9 @@ export default class Notes {
if (noteEntity.commands_changes && Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
}
- this.addFlash({
+ this.addAlert({
message: noteEntity.errors.commands_only,
- type: 'notice',
+ variant: VARIANT_INFO,
parent: this.parentTimeline.get(0),
});
this.refresh();
@@ -656,7 +657,7 @@ export default class Notes {
} else if ($form.hasClass('js-discussion-note-form')) {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
- return this.addFlash({
+ return this.addAlert({
message: __(
'Your comment could not be submitted! Please check your network connection and try again.',
),
@@ -665,7 +666,7 @@ export default class Notes {
}
updateNoteError() {
- createFlash({
+ createAlert({
message: __(
'Your comment could not be updated! Please check your network connection and try again.',
),
@@ -1338,15 +1339,12 @@ export default class Notes {
});
}
- addFlash(...flashParams) {
- this.flashContainer = createFlash(...flashParams);
+ addAlert(...alertParams) {
+ this.alert = createAlert(...alertParams);
}
- clearFlash() {
- if (this.flashContainer) {
- this.flashContainer.style.display = 'none';
- this.flashContainer = null;
- }
+ clearAlert() {
+ this.alert?.dismiss();
}
cleanForm($form) {
@@ -1535,7 +1533,7 @@ export default class Notes {
* b. Reset comment form to original state.
* b) If request failed
* 1. Remove placeholder element
- * 2. Show error Flash message about failure
+ * 2. Show error alert message about failure
*/
postComment(e) {
e.preventDefault();
@@ -1645,7 +1643,7 @@ export default class Notes {
}
// Clear previous form errors
- this.clearFlashWrapper();
+ this.clearAlertWrapper();
// Check if this was discussion comment
if (isDiscussionForm) {
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
index a4430b15752..3091c6703b4 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_discussion.vue
@@ -4,7 +4,7 @@ import { ApolloMutation } from 'vue-apollo';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
import { isLoggedIn } from '~/lib/utils/common_utils';
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note.vue b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
index e629f74ba02..af4bf7eb14d 100644
--- a/app/assets/javascripts/design_management/components/design_notes/design_note.vue
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note.vue
@@ -1,13 +1,7 @@
<script>
-import {
- GlAvatar,
- GlAvatarLink,
- GlButton,
- GlLink,
- GlSafeHtmlDirective,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink, GlButton, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
@@ -33,7 +27,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
note: {
diff --git a/app/assets/javascripts/design_management/components/design_todo_button.vue b/app/assets/javascripts/design_management/components/design_todo_button.vue
index 013dd1d89f3..a1a23d61093 100644
--- a/app/assets/javascripts/design_management/components/design_todo_button.vue
+++ b/app/assets/javascripts/design_management/components/design_todo_button.vue
@@ -1,6 +1,6 @@
<script>
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
+import TodoButton from '~/sidebar/components/todo_toggle/todo_button.vue';
import createDesignTodoMutation from '../graphql/mutations/create_design_todo.mutation.graphql';
import getDesignQuery from '../graphql/queries/get_design.query.graphql';
import allVersionsMixin from '../mixins/all_versions';
diff --git a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
index f10545faea6..c96487d0d08 100644
--- a/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
+++ b/app/assets/javascripts/design_management/components/upload/design_version_dropdown.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAvatar, GlCollapsibleListbox } from '@gitlab/ui';
import defaultAvatarUrl from 'images/no_avatar.png';
import { __, sprintf } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -8,13 +8,19 @@ import { findVersionId } from '../../utils/design_management_utils';
export default {
components: {
- GlDropdown,
- GlDropdownItem,
- GlSprintf,
+ GlAvatar,
+ GlCollapsibleListbox,
TimeAgo,
},
mixins: [allVersionsMixin],
computed: {
+ allVersionsList() {
+ return this.allVersions.map(({ id, ...item }, index) => ({
+ value: id,
+ index,
+ ...item,
+ }));
+ },
queryVersion() {
return this.$route.query.version;
},
@@ -29,17 +35,11 @@ export default {
// then return the latest version (index 0)
return idx !== -1 ? idx : 0;
},
- currentVersionId() {
- if (this.queryVersion) return this.queryVersion;
-
- const currentVersion = this.allVersions[this.currentVersionIdx];
- return this.findVersionId(currentVersion.id);
- },
dropdownText() {
if (this.isLatestVersion) {
return __('Showing latest version');
}
- // allVersions is sorted in reverse chronological order (latest first)
+ // allVersions is sorted in reverse chronological order (the latest first)
const currentVersionNumber = this.allVersions.length - this.currentVersionIdx;
return sprintf(__('Showing version #%{versionNumber}'), {
@@ -55,47 +55,49 @@ export default {
query: { version: this.findVersionId(versionId) },
});
},
- versionText(versionId) {
- if (this.findVersionId(versionId) === this.latestVersionId) {
- return __('Version %{versionNumber} (latest)');
- }
- return __('Version %{versionNumber}');
+ versionText(item) {
+ const versionNumber = this.allVersions.length - item.index;
+ const message =
+ this.findVersionId(item.value) === this.latestVersionId
+ ? __('Version %{versionNumber} (latest)')
+ : __('Version %{versionNumber}');
+ return sprintf(message, { versionNumber });
},
getAvatarUrl(version) {
return version?.author?.avatarUrl || defaultAvatarUrl;
},
+ getAuthorName(author) {
+ return author?.name;
+ },
},
};
</script>
<template>
- <gl-dropdown :text="dropdownText" size="small">
- <gl-dropdown-item
- v-for="(version, index) in allVersions"
- :key="version.id"
- is-check-item
- is-check-centered
- :is-checked="findVersionId(version.id) === currentVersionId"
- :avatar-url="getAvatarUrl(version)"
- @click="routeToVersion(version.id)"
- >
- <strong>
- <gl-sprintf :message="versionText(version.id)">
- <template #versionNumber>
- {{ allVersions.length - index }}
- </template>
- </gl-sprintf>
- </strong>
-
- <div v-if="version.author" class="gl-text-gray-600 gl-mt-1">
- <div>{{ version.author.name }}</div>
- <time-ago
- v-if="version.createdAt"
- class="text-1"
- :time="version.createdAt"
- tooltip-placement="bottom"
- />
- </div>
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-collapsible-listbox
+ is-check-centered
+ :items="allVersionsList"
+ :toggle-text="dropdownText"
+ :selected="designsVersion"
+ size="small"
+ @select="routeToVersion"
+ >
+ <template #list-item="{ item }">
+ <span class="gl-display-flex gl-align-items-center">
+ <gl-avatar :alt="getAuthorName(item.author)" :size="32" :src="getAvatarUrl(item)" />
+ <span class="gl-display-flex gl-flex-direction-column">
+ <span class="gl-font-weight-bold">{{ versionText(item) }}</span>
+ <span v-if="item.author" class="gl-text-gray-600 gl-mt-1">
+ <span class="gl-display-block">{{ getAuthorName(item.author) }}</span>
+ <time-ago
+ v-if="item.createdAt"
+ class="text-1"
+ :time="item.createdAt"
+ tooltip-placement="bottom"
+ />
+ </span>
+ </span>
+ </span>
+ </template>
+ </gl-collapsible-listbox>
</template>
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index d4c177e2e5f..f448e2f9e3d 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -6,7 +6,7 @@ import { ApolloMutation } from 'vue-apollo';
import { keysFor, ISSUE_CLOSE_DESIGN } from '~/behaviors/shortcuts/keybindings';
import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
-import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
+import { updateGlobalTodoCount } from '~/sidebar/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DesignDestroyer from '../../components/design_destroyer.vue';
import DesignReplyForm from '../../components/design_notes/design_reply_form.vue';
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 5a45797ed98..1857ff557e6 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButtonGroup, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
@@ -32,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
diff --git a/app/assets/javascripts/diffs/components/diff_code_quality.vue b/app/assets/javascripts/diffs/components/diff_code_quality.vue
index 8498724740f..11aa856619b 100644
--- a/app/assets/javascripts/diffs/components/diff_code_quality.vue
+++ b/app/assets/javascripts/diffs/components/diff_code_quality.vue
@@ -1,8 +1,12 @@
<script>
import { GlButton, GlIcon } from '@gitlab/ui';
-import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/reports/codequality_report/constants';
+import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
+import { NEW_CODE_QUALITY_FINDINGS } from '../i18n';
export default {
+ i18n: {
+ newFindings: NEW_CODE_QUALITY_FINDINGS,
+ },
components: { GlButton, GlIcon },
props: {
codeQuality: {
@@ -22,22 +26,33 @@ export default {
</script>
<template>
- <div data-testid="diff-codequality" class="gl-relative">
- <ul
- class="gl-list-style-none gl-mb-0 gl-p-0 codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10"
+ <div
+ data-testid="diff-codequality"
+ class="gl-relative codequality-findings-list gl-border-top-1 gl-border-bottom-1 gl-bg-gray-10 gl-pl-5 gl-pt-4 gl-pb-4"
+ >
+ <h4
+ data-testid="diff-codequality-findings-heading"
+ class="gl-mt-0 gl-mb-0 gl-font-base gl-font-regular"
>
+ {{ $options.i18n.newFindings }}
+ </h4>
+ <ul class="gl-list-style-none gl-mb-0 gl-p-0">
<li
v-for="finding in codeQuality"
:key="finding.description"
- class="gl-pt-1 gl-pb-1 gl-pl-3 gl-border-solid gl-border-bottom-0 gl-border-right-0 gl-border-1 gl-border-gray-100 gl-font-regular"
+ class="gl-pt-1 gl-pb-1 gl-font-regular gl-display-flex"
>
- <gl-icon
- :size="12"
- :name="severityIcon(finding.severity)"
- :class="severityClass(finding.severity)"
- class="codequality-severity-icon"
- />
- {{ finding.description }}
+ <span class="gl-mr-3">
+ <gl-icon
+ :size="12"
+ :name="severityIcon(finding.severity)"
+ :class="severityClass(finding.severity)"
+ class="codequality-severity-icon"
+ />
+ </span>
+ <span>
+ <span class="severity-copy">{{ finding.severity }}</span> - {{ finding.description }}
+ </span>
</li>
</ul>
<gl-button
diff --git a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
index 3766c125325..8b747aa08dd 100644
--- a/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
+++ b/app/assets/javascripts/diffs/components/diff_discussion_reply.vue
@@ -1,13 +1,18 @@
<script>
+import { GlButton } from '@gitlab/ui';
import { mapGetters } from 'vuex';
-import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import NoteSignedOutWidget from '~/notes/components/note_signed_out_widget.vue';
+import { START_THREAD } from '../i18n';
+
export default {
name: 'DiffDiscussionReply',
+ i18n: {
+ START_THREAD,
+ },
components: {
+ GlButton,
NoteSignedOutWidget,
- ReplyPlaceholder,
},
props: {
hasForm: {
@@ -34,11 +39,9 @@ export default {
<template v-if="userCanReply">
<slot v-if="hasForm" name="form"></slot>
<template v-else-if="renderReplyPlaceholder">
- <reply-placeholder
- :placeholder-text="__('Start a new discussion…')"
- :label-text="__('New discussion')"
- @focus="$emit('showNewDiscussionForm')"
- />
+ <gl-button @click="$emit('showNewDiscussionForm')">
+ {{ $options.i18n.START_THREAD }}
+ </gl-button>
</template>
</template>
<note-signed-out-widget v-else />
diff --git a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
index b2098b9e82d..8fcbc4b5cce 100644
--- a/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
+++ b/app/assets/javascripts/diffs/components/diff_expansion_cell.vue
@@ -1,6 +1,7 @@
<script>
-import { GlTooltipDirective, GlSafeHtmlDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { s__, sprintf } from '~/locale';
import { UNFOLD_COUNT, INLINE_DIFF_LINES_KEY } from '../constants';
@@ -21,7 +22,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
file: {
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 8f041d1e670..564f776edd2 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -1,13 +1,8 @@
<script>
-import {
- GlButton,
- GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
- GlAlert,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlSprintf, GlAlert } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
import { createAlert } from '~/flash';
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 91c3df39e32..dff61acdfba 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -1,7 +1,6 @@
<script>
import {
GlTooltipDirective,
- GlSafeHtmlDirective,
GlIcon,
GlBadge,
GlButton,
@@ -14,6 +13,7 @@ import {
} from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { IdState } from 'vendor/vue-virtual-scroller';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -44,7 +44,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [IdState({ idProp: (vm) => vm.diffFile.file_hash }), glFeatureFlagsMixin()],
i18n: {
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index 5ea118afe78..aa9a17d18e3 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -1,5 +1,4 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters, mapState, mapActions } from 'vuex';
import { IdState } from 'vendor/vue-virtual-scroller';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -22,9 +21,6 @@ export default {
DiffCommentCell,
DraftNote,
},
- directives: {
- SafeHtml,
- },
mixins: [
draftCommentsMixin,
IdState({ idProp: (vm) => vm.diffFile.file_hash }),
@@ -307,7 +303,11 @@ export default {
class="diff-td notes-content parallel old"
>
<div v-for="draft in lineDrafts(line, 'left')" :key="draft.id" class="content">
- <draft-note :draft="draft" :line="line.left" />
+ <article class="note-wrapper">
+ <ul class="notes draft-notes">
+ <draft-note :draft="draft" :line="line.left" />
+ </ul>
+ </article>
</div>
</div>
<div
@@ -315,7 +315,11 @@ export default {
class="diff-td notes-content parallel new"
>
<div v-for="draft in lineDrafts(line, 'right')" :key="draft.id" class="content">
- <draft-note :draft="draft" :line="line.right" />
+ <article class="note-wrapper">
+ <ul class="notes draft-notes">
+ <draft-note :draft="draft" :line="line.right" />
+ </ul>
+ </article>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index f7f4aad3ad0..0f44eb06cb3 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -19,6 +19,7 @@ export const DIFF_FILE = {
autoCollapsed: __('Files with large changes are collapsed by default.'),
expand: __('Expand file'),
};
+export const START_THREAD = __('Start another thread');
export const SETTINGS_DROPDOWN = {
whitespace: __('Show whitespace changes'),
@@ -49,3 +50,5 @@ export const CONFLICT_TEXT = {
};
export const HIDE_COMMENTS = __('Hide comments');
+
+export const NEW_CODE_QUALITY_FINDINGS = __('New code quality findings');
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index b4ff5e4f250..7da5ef54b80 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { getCookie, parseBoolean, removeCookie } from '~/lib/utils/common_utils';
+import notesStore from '~/mr_notes/stores';
import eventHub from '../notes/event_hub';
import DiffsApp from './components/app.vue';
@@ -9,7 +10,7 @@ import { TREE_LIST_STORAGE_KEY, DIFF_WHITESPACE_COOKIE_NAME } from './constants'
import { getReviewsForMergeRequest } from './utils/file_reviews';
import { getDerivedMergeRequestInformation } from './utils/merge_request';
-export default function initDiffsApp(store) {
+export default function initDiffsApp(store = notesStore) {
const vm = new Vue({
el: '#js-diffs-app',
name: 'MergeRequestDiffs',
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index c73012527a2..96a73917820 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,7 @@ import { isCollapsed } from '../utils/diff_file';
import { markFileReview, setReviewsForMergeRequest } from '../utils/file_reviews';
import { getDerivedMergeRequestInformation } from '../utils/merge_request';
import { queueRedisHllEvents } from '../utils/queue_events';
-import TreeWorker from '../workers/tree_worker';
+import TreeWorker from '../workers/tree_worker?worker';
import * as types from './mutation_types';
import {
getDiffPositionByLineCode,
@@ -444,20 +444,27 @@ export const scrollToLineIfNeededParallel = (_, line) => {
}
};
-export const loadCollapsedDiff = ({ commit, getters, state }, file) =>
- axios
- .get(file.load_collapsed_diff_url, {
- params: {
- commit_id: getters.commitId,
- w: state.showWhitespace ? '0' : '1',
- },
- })
- .then((res) => {
- commit(types.ADD_COLLAPSED_DIFFS, {
- file,
- data: res.data,
- });
+export const loadCollapsedDiff = ({ commit, getters, state }, file) => {
+ const versionPath = state.mergeRequestDiff?.version_path;
+ const loadParams = {
+ commit_id: getters.commitId,
+ w: state.showWhitespace ? '0' : '1',
+ };
+
+ if (versionPath) {
+ const { diffId, startSha } = getDerivedMergeRequestInformation({ endpoint: versionPath });
+
+ loadParams.diff_id = diffId;
+ loadParams.start_sha = startSha;
+ }
+
+ return axios.get(file.load_collapsed_diff_url, { params: loadParams }).then((res) => {
+ commit(types.ADD_COLLAPSED_DIFFS, {
+ file,
+ data: res.data,
});
+ });
+};
/**
* Toggles the file discussions after user clicked on the toggle discussions button.
diff --git a/app/assets/javascripts/diffs/utils/merge_request.js b/app/assets/javascripts/diffs/utils/merge_request.js
index edb4304f558..43e04a814c5 100644
--- a/app/assets/javascripts/diffs/utils/merge_request.js
+++ b/app/assets/javascripts/diffs/utils/merge_request.js
@@ -1,14 +1,30 @@
const endpointRE = /^(\/?(.+?)\/(.+?)\/-\/merge_requests\/(\d+)).*$/i;
+function getVersionInfo({ endpoint } = {}) {
+ const dummyRoot = 'https://gitlab.com';
+ const endpointUrl = new URL(endpoint, dummyRoot);
+ const params = Object.fromEntries(endpointUrl.searchParams.entries());
+
+ const { start_sha: startSha, diff_id: diffId } = params;
+
+ return {
+ diffId,
+ startSha,
+ };
+}
+
export function getDerivedMergeRequestInformation({ endpoint } = {}) {
let mrPath;
let userOrGroup;
let project;
let id;
+ let diffId;
+ let startSha;
const matches = endpointRE.exec(endpoint);
if (matches) {
[, mrPath, userOrGroup, project, id] = matches;
+ ({ diffId, startSha } = getVersionInfo({ endpoint }));
}
return {
@@ -16,5 +32,7 @@ export function getDerivedMergeRequestInformation({ endpoint } = {}) {
userOrGroup,
project,
id,
+ diffId,
+ startSha,
};
}
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar.vue b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
index 2c177634bbe..c72145f9d2f 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar.vue
@@ -57,13 +57,12 @@ export default {
>
<div v-for="group in $options.groups" :key="group">
<gl-button-group v-if="hasGroupItems(group)">
- <template v-for="item in getGroupItems(group)">
- <source-editor-toolbar-button
- :key="item.id"
- :button="item"
- @click="$emit('click', item)"
- />
- </template>
+ <source-editor-toolbar-button
+ v-for="item in getGroupItems(group)"
+ :key="item.id"
+ :button="item"
+ @click="$emit('click', item)"
+ />
</gl-button-group>
</div>
</section>
diff --git a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
index 6ce48ddf89a..38f586f0773 100644
--- a/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
+++ b/app/assets/javascripts/editor/components/source_editor_toolbar_button.vue
@@ -31,12 +31,19 @@ export default {
return Object.entries(this.button).length > 0;
},
},
+ mounted() {
+ if (this.button.data) {
+ Object.entries(this.button.data).forEach(([attr, value]) => {
+ this.$el.dataset[attr] = value;
+ });
+ }
+ },
methods: {
- clickHandler() {
+ clickHandler(event) {
if (this.button.onClick) {
- this.button.onClick();
+ this.button.onClick(event);
}
- this.$emit('click');
+ this.$emit('click', event);
},
},
};
@@ -52,7 +59,7 @@ export default {
:icon="icon"
:title="label"
:aria-label="label"
- data-qa-selector="editor_toolbar_button"
- @click="clickHandler"
+ :class="button.class"
+ @click="clickHandler($event)"
/>
</template>
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index 83cfdd25757..d0649ecccba 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,5 +1,6 @@
+import { MODIFIER_KEY } from '~/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { s__, __ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
export const URI_PREFIX = 'gitlab';
export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -62,3 +63,104 @@ export const EXTENSION_MARKDOWN_PREVIEW_PANEL_WIDTH = 0.5; // 50% of the width
export const EXTENSION_MARKDOWN_PREVIEW_UPDATE_DELAY = 250; // ms
export const EXTENSION_MARKDOWN_PREVIEW_LABEL = __('Preview Markdown');
export const EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL = __('Hide Live Preview');
+export const EXTENSION_MARKDOWN_BUTTONS = [
+ {
+ id: 'bold',
+ label: sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '**',
+ mdShortcuts: '["mod+b"]',
+ },
+ },
+ {
+ id: 'italic',
+ label: sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '_',
+ mdShortcuts: '["mod+i"]',
+ },
+ },
+ {
+ id: 'strikethrough',
+ label: sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}⇧X)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '~~',
+ mdShortcuts: '["mod+shift+x]',
+ },
+ },
+ {
+ id: 'quote',
+ label: __('Insert a quote'),
+ data: {
+ mdTag: '> ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'code',
+ label: __('Insert code'),
+ data: {
+ mdTag: '`',
+ mdBlock: '```',
+ },
+ },
+ {
+ id: 'link',
+ label: sprintf(s__('MarkdownEditor|Add a link (%{modifier_key}K)'), {
+ modifierKey: MODIFIER_KEY,
+ }),
+ data: {
+ mdTag: '[{text}](url)',
+ mdSelect: 'url',
+ mdShortcuts: '["mod+k"]',
+ },
+ },
+ {
+ id: 'list-bulleted',
+ label: __('Add a bullet list'),
+ data: {
+ mdTag: '- ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'list-numbered',
+ label: __('Add a numbered list'),
+ data: {
+ mdTag: '1. ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'list-task',
+ label: __('Add a checklist'),
+ data: {
+ mdTag: '- [ ] ',
+ mdPrepend: true,
+ },
+ },
+ {
+ id: 'details-block',
+ label: __('Add a collapsible section'),
+ data: {
+ mdTag: '<details><summary>Click to expand</summary>\n{text}\n</details>',
+ mdPrepend: true,
+ mdSelect: __('Click to expand'),
+ },
+ },
+ {
+ id: 'table',
+ label: __('Add a table'),
+ data: {
+ /* eslint-disable-next-line @gitlab/require-i18n-strings */
+ mdTag: '| header | header |\n| ------ | ------ |\n| | |\n| | |',
+ mdPrepend: true,
+ },
+ },
+];
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
index a16fe93026e..6105a577996 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
@@ -1,8 +1,37 @@
+import { insertMarkdownText } from '~/lib/utils/text_markdown';
+import { EDITOR_TOOLBAR_RIGHT_GROUP, EXTENSION_MARKDOWN_BUTTONS } from '../constants';
+
export class EditorMarkdownExtension {
static get extensionName() {
return 'EditorMarkdown';
}
+ onSetup(instance) {
+ this.toolbarButtons = [];
+ if (instance.toolbar) {
+ this.setupToolbar(instance);
+ }
+ }
+ onBeforeUnuse(instance) {
+ const ids = this.toolbarButtons.map((item) => item.id);
+ if (instance.toolbar) {
+ instance.toolbar.removeItems(ids);
+ }
+ }
+
+ setupToolbar(instance) {
+ this.toolbarButtons = EXTENSION_MARKDOWN_BUTTONS.map((btn) => {
+ return {
+ ...btn,
+ icon: btn.id,
+ group: EDITOR_TOOLBAR_RIGHT_GROUP,
+ category: 'tertiary',
+ onClick: (e) => instance.insertMarkdown(e),
+ };
+ });
+ instance.toolbar.addItems(this.toolbarButtons);
+ }
+
// eslint-disable-next-line class-methods-use-this
provides() {
return {
@@ -36,6 +65,25 @@ export class EditorMarkdownExtension {
pos.lineNumber += dy;
instance.setPosition(pos);
},
+ insertMarkdown: (instance, e) => {
+ const {
+ mdTag: tag,
+ mdBlock: blockTag,
+ mdPrepend,
+ mdSelect: select,
+ } = e.currentTarget.dataset;
+
+ insertMarkdownText({
+ tag,
+ blockTag,
+ wrap: !mdPrepend,
+ select,
+ selected: instance.getSelectedText(),
+ text: instance.getValue(),
+ editor: instance,
+ });
+ instance.focus();
+ },
/**
* Adjust existing selection to select text within the original selection.
* - If `selectedText` is not supplied, we fetch selected text with
diff --git a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
index dd4a7a689d7..58ddaa94d5e 100644
--- a/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_livepreview_ext.js
@@ -120,6 +120,9 @@ export class EditorMarkdownPreviewExtension {
category: 'primary',
selectedLabel: EXTENSION_MARKDOWN_HIDE_PREVIEW_LABEL,
onClick: () => instance.togglePreview(),
+ data: {
+ qaSelector: 'editor_toolbar_button',
+ },
},
];
instance.toolbar.addItems(this.toolbarButtons);
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 45f063a2048..d94aa73e43a 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -41,6 +41,9 @@
"before_script": {
"$ref": "#/definitions/before_script"
},
+ "hooks": {
+ "$ref": "#/definitions/hooks"
+ },
"cache": {
"$ref": "#/definitions/cache"
},
@@ -202,25 +205,11 @@
"when": {
"markdownDescription": "Configure when artifacts are uploaded depended on job status. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#artifactswhen).",
"default": "on_success",
- "oneOf": [
- {
- "enum": [
- "on_success"
- ],
- "description": "Upload artifacts only when the job succeeds (this is the default)."
- },
- {
- "enum": [
- "on_failure"
- ],
- "description": "Upload artifacts only when the job fails."
- },
- {
- "enum": [
- "always"
- ],
- "description": "Upload artifacts regardless of job status."
- }
+ "type": "string",
+ "enum": [
+ "on_success",
+ "on_failure",
+ "always"
]
},
"expire_in": {
@@ -347,10 +336,10 @@
"include_item": {
"oneOf": [
{
- "description": "Will infer the method based on the value. E.g. `https://...` strings will be of type `include:remote`, and `/templates/...` will be of type `include:local`.",
+ "description": "Will infer the method based on the value. E.g. `https://...` strings will be of type `include:remote`, and `/templates/...` or `templates/...` will be of type `include:local`.",
"type": "string",
"format": "uri-reference",
- "pattern": "^(https?://|/).+\\.ya?ml$"
+ "pattern": "^(https?://|/?.?-?(?!\\w+://)\\w).+\\.ya?ml$"
},
{
"type": "object",
@@ -585,56 +574,98 @@
]
}
},
+ "id_tokens": {
+ "type": "object",
+ "markdownDescription": "Defines JWTs to be injected as environment variables.",
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "properties": {
+ "aud": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
+ }
+ },
+ "required": [
+ "aud"
+ ],
+ "additionalProperties": false
+ }
+ }
+ },
"secrets": {
"type": "object",
"markdownDescription": "Defines secrets to be injected as environment variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secrets).",
- "additionalProperties": {
- "type": "object",
- "description": "Environment variable name",
- "properties": {
- "vault": {
- "oneOf": [
- {
- "type": "string",
- "description": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`)"
- },
- {
- "type": "object",
- "properties": {
- "engine": {
- "type": "object",
- "properties": {
- "name": {
- "type": "string"
+ "patternProperties": {
+ ".*": {
+ "type": "object",
+ "properties": {
+ "vault": {
+ "oneOf": [
+ {
+ "type": "string",
+ "markdownDescription": "The secret to be fetched from Vault (e.g. 'production/db/password@ops' translates to secret 'ops/data/production/db', field `password`). [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsvault)"
+ },
+ {
+ "type": "object",
+ "properties": {
+ "engine": {
+ "type": "object",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "path": {
+ "type": "string"
+ }
},
- "path": {
- "type": "string"
- }
+ "required": [
+ "name",
+ "path"
+ ]
},
- "required": [
- "name",
- "path"
- ]
- },
- "path": {
- "type": "string"
+ "path": {
+ "type": "string"
+ },
+ "field": {
+ "type": "string"
+ }
},
- "field": {
- "type": "string"
- }
- },
- "required": [
- "engine",
- "path",
- "field"
- ]
- }
- ]
- }
- },
- "required": [
- "vault"
- ]
+ "required": [
+ "engine",
+ "path",
+ "field"
+ ],
+ "additionalProperties": false
+ }
+ ]
+ },
+ "file": {
+ "type": "boolean",
+ "default": true,
+ "markdownDescription": "Configures the secret to be stored as either a file or variable type CI/CD variable. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#secretsfile)"
+ },
+ "token": {
+ "type": "string",
+ "description": "Specifies the JWT variable that should be used to authenticate with Hashicorp Vault."
+ }
+ },
+ "required": [
+ "vault"
+ ],
+ "additionalProperties": false
+ }
}
},
"before_script": {
@@ -739,7 +770,17 @@
"type": "object",
"properties": {
"value": {
- "type": "string"
+ "type": "string",
+ "markdownDescription": "Default value of the variable. If used with `options`, `value` must be included in the array. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#prefill-variables-in-manual-pipelines)"
+ },
+ "options": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true,
+ "markdownDescription": "A list of predefined values that users can select from in the **Run pipeline** page when running a pipeline manually. [Learn More](https://docs.gitlab.com/ee/ci/pipelines/index.html#configure-a-list-of-selectable-values-for-a-prefilled-variable)"
},
"description": {
"type": "string",
@@ -959,6 +1000,7 @@
"default": false
},
"when": {
+ "type": "string",
"markdownDescription": "Defines when to save the cache, based on the status of the job. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#cachewhen).",
"default": "on_success",
"enum": [
@@ -1200,6 +1242,9 @@
"after_script": {
"$ref": "#/definitions/after_script"
},
+ "hooks": {
+ "$ref": "#/definitions/hooks"
+ },
"rules": {
"$ref": "#/definitions/rules"
},
@@ -1209,6 +1254,9 @@
"cache": {
"$ref": "#/definitions/cache"
},
+ "id_tokens": {
+ "$ref": "#/definitions/id_tokens"
+ },
"secrets": {
"$ref": "#/definitions/secrets"
},
@@ -1861,6 +1909,39 @@
}
]
}
+ },
+ "hooks": {
+ "type": "object",
+ "markdownDescription": "Specifies lists of commands to execute on the runner at certain stages of job execution. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hooks).",
+ "properties": {
+ "pre_get_sources_script": {
+ "markdownDescription": "Specifies a list of commands to execute on the runner before updating the Git repository and any submodules. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#hookspre_get_sources_script).",
+ "oneOf": [
+ {
+ "type": "string",
+ "minLength": 1
+ },
+ {
+ "type": "array",
+ "items": {
+ "anyOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ "minItems": 1
+ }
+ ]
+ }
+ },
+ "additionalProperties": false
}
}
} \ No newline at end of file
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index f22a0705b3d..31bc462f0b9 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -15,10 +15,10 @@ import {
GlLink,
GlTooltip,
GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
GlSprintf,
} from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import InstanceComponent from '~/vue_shared/components/deployment_instance.vue';
import { STATUS_MAP, CANARY_STATUS } from '../constants';
diff --git a/app/assets/javascripts/environments/environment_details/constants.js b/app/assets/javascripts/environments/environment_details/constants.js
new file mode 100644
index 00000000000..56c70c354b7
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/constants.js
@@ -0,0 +1,47 @@
+import { __ } from '~/locale';
+
+export const ENVIRONMENT_DETAILS_PAGE_SIZE = 20;
+export const ENVIRONMENT_DETAILS_TABLE_FIELDS = [
+ {
+ key: 'status',
+ label: __('Status'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'id',
+ label: __('ID'),
+ columnClass: 'gl-w-5p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'triggerer',
+ label: __('Triggerer'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'commit',
+ label: __('Commit'),
+ columnClass: 'gl-w-20p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'job',
+ label: __('Job'),
+ columnClass: 'gl-w-20p',
+ tdClass: 'gl-vertical-align-middle!',
+ },
+ {
+ key: 'created',
+ label: __('Created'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
+ },
+ {
+ key: 'deployed',
+ label: __('Deployed'),
+ columnClass: 'gl-w-10p',
+ tdClass: 'gl-vertical-align-middle! gl-white-space-nowrap',
+ },
+];
diff --git a/app/assets/javascripts/environments/environment_details/index.vue b/app/assets/javascripts/environments/environment_details/index.vue
new file mode 100644
index 00000000000..435d3fd820e
--- /dev/null
+++ b/app/assets/javascripts/environments/environment_details/index.vue
@@ -0,0 +1,118 @@
+<script>
+import {
+ GlTableLite,
+ GlAvatarLink,
+ GlAvatar,
+ GlLink,
+ GlTooltipDirective,
+ GlTruncate,
+ GlBadge,
+ GlLoadingIcon,
+} from '@gitlab/ui';
+import Commit from '~/vue_shared/components/commit.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import environmentDetailsQuery from '../graphql/queries/environment_details.query.graphql';
+import { convertToDeploymentTableRow } from '../helpers/deployment_data_transformation_helper';
+import DeploymentStatusBadge from '../components/deployment_status_badge.vue';
+import { ENVIRONMENT_DETAILS_PAGE_SIZE, ENVIRONMENT_DETAILS_TABLE_FIELDS } from './constants';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlBadge,
+ DeploymentStatusBadge,
+ TimeAgoTooltip,
+ GlTableLite,
+ GlAvatarLink,
+ GlAvatar,
+ GlLink,
+ GlTruncate,
+ Commit,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ projectFullPath: {
+ type: String,
+ required: true,
+ },
+ environmentName: {
+ type: String,
+ required: true,
+ },
+ },
+ apollo: {
+ project: {
+ query: environmentDetailsQuery,
+ variables() {
+ return {
+ projectFullPath: this.projectFullPath,
+ environmentName: this.environmentName,
+ pageSize: ENVIRONMENT_DETAILS_PAGE_SIZE,
+ };
+ },
+ },
+ },
+ data() {
+ return {
+ project: {
+ loading: true,
+ },
+ loading: 0,
+ tableFields: ENVIRONMENT_DETAILS_TABLE_FIELDS,
+ };
+ },
+ computed: {
+ deployments() {
+ return this.project.environment?.deployments.nodes.map(convertToDeploymentTableRow) || [];
+ },
+ isLoading() {
+ return this.$apollo.queries.project.loading;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="isLoading" size="lg" class="mt-3" />
+ <gl-table-lite v-else :items="deployments" :fields="tableFields" fixed stacked="lg">
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+ <template #cell(status)="{ item }">
+ <div>
+ <deployment-status-badge :status="item.status" />
+ </div>
+ </template>
+ <template #cell(id)="{ item }">
+ <strong>{{ item.id }}</strong>
+ </template>
+ <template #cell(triggerer)="{ item }">
+ <gl-avatar-link :href="item.triggerer.webUrl">
+ <gl-avatar
+ v-gl-tooltip
+ :title="item.triggerer.name"
+ :src="item.triggerer.avatarUrl"
+ :size="24"
+ />
+ </gl-avatar-link>
+ </template>
+ <template #cell(commit)="{ item }">
+ <commit v-bind="item.commit" />
+ </template>
+ <template #cell(job)="{ item }">
+ <gl-link v-if="item.job" :href="item.job.webPath">
+ <gl-truncate :text="item.job.label" />
+ </gl-link>
+ <gl-badge v-else variant="info">{{ __('API') }}</gl-badge>
+ </template>
+ <template #cell(created)="{ item }">
+ <time-ago-tooltip :time="item.created" />
+ </template>
+ <template #cell(deployed)="{ item }">
+ <time-ago-tooltip :time="item.deployed" />
+ </template>
+ </gl-table-lite>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
new file mode 100644
index 00000000000..e8f2a2cdf7f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql
@@ -0,0 +1,48 @@
+query getEnvironmentDetails($projectFullPath: ID!, $environmentName: String, $pageSize: Int) {
+ project(fullPath: $projectFullPath) {
+ id
+ name
+ fullPath
+ environment(name: $environmentName) {
+ id
+ name
+ deployments(orderBy: { createdAt: DESC }, first: $pageSize) {
+ nodes {
+ id
+ iid
+ status
+ ref
+ tag
+ job {
+ name
+ id
+ webPath
+ }
+ commit {
+ id
+ shortId
+ message
+ webUrl
+ authorGravatar
+ authorName
+ authorEmail
+ author {
+ id
+ name
+ avatarUrl
+ webUrl
+ }
+ }
+ triggerer {
+ id
+ webUrl
+ name
+ avatarUrl
+ }
+ createdAt
+ finishedAt
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
new file mode 100644
index 00000000000..bfe92fe3125
--- /dev/null
+++ b/app/assets/javascripts/environments/helpers/deployment_data_transformation_helper.js
@@ -0,0 +1,62 @@
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+
+/**
+ * This function transforms Commit object coming from GraphQL to object compatible with app/assets/javascripts/vue_shared/components/commit.vue author object
+ * @param {Object} Commit
+ * @returns {Object}
+ */
+export const getAuthorFromCommit = (commit) => {
+ if (commit.author) {
+ return {
+ username: commit.author.name,
+ path: commit.author.webUrl,
+ avatar_url: commit.author.avatarUrl,
+ };
+ }
+ return {
+ username: commit.authorName,
+ path: `mailto:${commit.authorEmail}`,
+ avatar_url: commit.authorGravatar,
+ };
+};
+
+/**
+ * This function transforms deploymentNode object coming from GraphQL to object compatible with app/assets/javascripts/vue_shared/components/commit.vue
+ * @param {Object} deploymentNode
+ * @returns {Object}
+ */
+export const getCommitFromDeploymentNode = (deploymentNode) => {
+ if (!deploymentNode.commit) {
+ throw new Error("deploymentNode argument doesn't have 'commit' field", deploymentNode);
+ }
+ return {
+ title: deploymentNode.commit.message,
+ commitUrl: deploymentNode.commit.webUrl,
+ shortSha: deploymentNode.commit.shortId,
+ tag: deploymentNode.tag,
+ commitRef: {
+ name: deploymentNode.ref,
+ },
+ author: getAuthorFromCommit(deploymentNode.commit),
+ };
+};
+
+/**
+ * This function transforms deploymentNode object coming from GraphQL to object compatible with app/assets/javascripts/environments/environment_details/page.vue table
+ * @param {Object} deploymentNode
+ * @returns {Object}
+ */
+export const convertToDeploymentTableRow = (deploymentNode) => {
+ return {
+ status: deploymentNode.status.toLowerCase(),
+ id: deploymentNode.iid,
+ triggerer: deploymentNode.triggerer,
+ commit: getCommitFromDeploymentNode(deploymentNode),
+ job: deploymentNode.job && {
+ webPath: deploymentNode.job.webPath,
+ label: `${deploymentNode.job.name} (#${getIdFromGraphQLId(deploymentNode.job.id)})`,
+ },
+ created: deploymentNode.createdAt || '',
+ deployed: deploymentNode.finishedAt || '',
+ };
+};
diff --git a/app/assets/javascripts/environments/mount_show.js b/app/assets/javascripts/environments/mount_show.js
index 6df4fad83f2..ba816599ac2 100644
--- a/app/assets/javascripts/environments/mount_show.js
+++ b/app/assets/javascripts/environments/mount_show.js
@@ -1,6 +1,8 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import EnvironmentsDetailHeader from './components/environments_detail_header.vue';
+import { apolloProvider } from './graphql/client';
import environmentsMixin from './mixins/environments_mixin';
export const initHeader = () => {
@@ -41,7 +43,33 @@ export const initHeader = () => {
cancelAutoStopPath: dataset.environmentCancelAutoStopPath,
terminalPath: dataset.environmentTerminalPath,
metricsPath: dataset.environmentMetricsPath,
- updatePath: dataset.environmentEditPath,
+ updatePath: dataset.tnvironmentEditPath,
+ },
+ });
+ },
+ });
+};
+
+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');
+ const dataSet = convertObjectPropsToCamelCase(JSON.parse(dataElement.dataset.details));
+
+ Vue.use(VueApollo);
+ const el = document.getElementById('environment_details_page');
+ return new Vue({
+ el,
+ apolloProvider: apolloProvider(),
+ provide: {},
+ render(createElement) {
+ return createElement(EnvironmentsDetailPage, {
+ props: {
+ projectFullPath: dataSet.projectFullPath,
+ environmentName: dataSet.name,
},
});
},
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index de4b11699fc..122c7c005e9 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -357,7 +357,7 @@ export default {
>
<span class="d-flex">
<gl-icon
- class="gl-new-dropdown-item-check-icon"
+ class="gl-dropdown-item-check-icon"
:class="{ invisible: !isCurrentStatusFilter(status) }"
name="mobile-issue-close"
/>
@@ -374,7 +374,7 @@ export default {
>
<span class="d-flex">
<gl-icon
- class="gl-new-dropdown-item-check-icon"
+ class="gl-dropdown-item-check-icon"
:class="{ invisible: !isCurrentSortField(field) }"
name="mobile-issue-close"
/>
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index 34d01f21da2..6ddd982ebf1 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -1,5 +1,6 @@
<script>
-import { GlTooltip, GlSprintf, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlTooltip, GlSprintf, GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
index f0f42d19ea5..286b214b511 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -4,6 +4,8 @@ import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { labelForStrategy } from '../utils';
+import StrategyLabel from './strategy_label.vue';
+
export default {
i18n: {
deleteLabel: __('Delete'),
@@ -15,6 +17,7 @@ export default {
GlButton,
GlModal,
GlToggle,
+ StrategyLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -166,14 +169,13 @@ export default {
<div
class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments"
>
- <gl-badge
+ <strategy-label
v-for="strategy in featureFlag.strategies"
:key="strategy.id"
- data-testid="strategy-badge"
- variant="info"
- class="gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left gl-px-5"
- >{{ strategyBadgeText(strategy) }}</gl-badge
- >
+ data-testid="strategy-label"
+ class="w-100 gl-mr-3 gl-mt-2 gl-white-space-normal gl-text-left"
+ v-bind="strategyBadgeText(strategy)"
+ />
</div>
</div>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
index 1a470d74b59..0fde87dd0ba 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/flexible_rollout.vue
@@ -90,10 +90,10 @@ export default {
:id="inputId"
:value="percentage"
:state="isValid"
- class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
+ size="xs"
@input="onPercentageChange"
/>
<span class="ml-1">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
index 91e1b85d66e..0acb0d4366c 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/percent_rollout.vue
@@ -56,10 +56,10 @@ export default {
:id="inputId"
:value="percentage"
:state="isValid"
- class="rollout-percentage gl-text-right gl-w-9"
type="number"
min="0"
max="100"
+ size="xs"
@input="onPercentageChange"
/>
<span class="gl-ml-2">%</span>
diff --git a/app/assets/javascripts/feature_flags/components/strategy_label.vue b/app/assets/javascripts/feature_flags/components/strategy_label.vue
new file mode 100644
index 00000000000..c2d3ec5708f
--- /dev/null
+++ b/app/assets/javascripts/feature_flags/components/strategy_label.vue
@@ -0,0 +1,29 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ scopes: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ parameters: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <strong class="gl-fw-bold"
+ >{{ name }}<span v-if="parameters"> - {{ parameters }}</span
+ >:</strong
+ >
+ <span v-if="scopes">{{ scopes }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/feature_flags/utils.js b/app/assets/javascripts/feature_flags/utils.js
index e77cb8406cc..47deeab0571 100644
--- a/app/assets/javascripts/feature_flags/utils.js
+++ b/app/assets/javascripts/feature_flags/utils.js
@@ -50,17 +50,11 @@ const scopeName = ({ environment_scope: scope }) =>
export const labelForStrategy = (strategy) => {
const { name, parameters } = badgeTextByType[strategy.name];
+ const scopes = strategy.scopes.map(scopeName).join(', ');
- if (parameters) {
- return sprintf('%{name} - %{parameters}: %{scopes}', {
- name,
- parameters: parameters(strategy),
- scopes: strategy.scopes.map(scopeName).join(', '),
- });
- }
-
- return sprintf('%{name}: %{scopes}', {
+ return {
name,
- scopes: strategy.scopes.map(scopeName).join(', '),
- });
+ parameters: parameters ? parameters(strategy) : null,
+ scopes,
+ };
};
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
index 79d7eb94569..1c6e6380e76 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
@@ -1,12 +1,7 @@
<script>
import clusterPopover from '@gitlab/svgs/dist/illustrations/cluster_popover.svg';
-import {
- GlPopover,
- GlSprintf,
- GlLink,
- GlButton,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlPopover, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import { POPOVER_TARGET_ID } from './constants';
import { dismiss } from './feature_highlight_helper';
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index d9c627f5c93..397ba879866 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -1,9 +1,16 @@
-import { __, s__ } from '~/locale';
+import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_APPROVED_BY,
+ TOKEN_TITLE_REVIEWER,
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_REVIEWER,
+ TOKEN_TYPE_TARGET_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const reviewerToken = {
- formattedKey: s__('SearchToken|Reviewer'),
- key: 'reviewer',
+ formattedKey: TOKEN_TITLE_REVIEWER,
+ key: TOKEN_TYPE_REVIEWER,
type: 'string',
param: 'username',
symbol: '@',
@@ -53,7 +60,7 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
if (!disableTargetBranchFilter) {
const targetBranchToken = {
formattedKey: __('Target-Branch'),
- key: 'target-branch',
+ key: TOKEN_TYPE_TARGET_BRANCH,
type: 'string',
param: '',
symbol: '',
@@ -67,8 +74,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const approvedBy = {
token: {
- formattedKey: __('Approved-By'),
- key: 'approved-by',
+ formattedKey: TOKEN_TITLE_APPROVED_BY,
+ key: TOKEN_TYPE_APPROVED_BY,
type: 'array',
param: 'usernames[]',
symbol: '@',
@@ -76,8 +83,8 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
tag: '@approved-by',
},
tokenAlternative: {
- formattedKey: __('Approved-By'),
- key: 'approved-by',
+ formattedKey: TOKEN_TITLE_APPROVED_BY,
+ key: TOKEN_TYPE_APPROVED_BY,
type: 'string',
param: 'usernames',
symbol: '@',
@@ -85,25 +92,25 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
condition: [
{
url: 'approved_by_usernames[]=None',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('None'),
operator: '=',
},
{
url: 'not[approved_by_usernames][]=None',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('None'),
operator: '!=',
},
{
url: 'approved_by_usernames[]=Any',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('Any'),
operator: '=',
},
{
url: 'not[approved_by_usernames][]=Any',
- tokenKey: 'approved-by',
+ tokenKey: TOKEN_TYPE_APPROVED_BY,
value: __('Any'),
operator: '!=',
},
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 3913e4e8d81..1f8baa470d8 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,5 +1,17 @@
import { sortMilestonesByDueDate } from '~/milestones/utils';
-import { mergeUrlParams } from '../lib/utils/url_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_REVIEWER,
+ TOKEN_TYPE_TARGET_BRANCH,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import DropdownEmoji from './dropdown_emoji';
import DropdownHint from './dropdown_hint';
import DropdownNonUser from './dropdown_non_user';
@@ -58,17 +70,17 @@ export default class AvailableDropdownMappings {
getMappings() {
return {
- author: {
+ [TOKEN_TYPE_AUTHOR]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-author'),
},
- assignee: {
+ [TOKEN_TYPE_ASSIGNEE]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-assignee'),
},
- reviewer: {
+ [TOKEN_TYPE_REVIEWER]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-reviewer'),
@@ -78,12 +90,12 @@ export default class AvailableDropdownMappings {
gl: DropdownUser,
element: this.container.getElementById('js-dropdown-attention-requested'),
},
- 'approved-by': {
+ [TOKEN_TYPE_APPROVED_BY]: {
reference: null,
gl: DropdownUser,
element: this.container.querySelector('#js-dropdown-approved-by'),
},
- milestone: {
+ [TOKEN_TYPE_MILESTONE]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -93,7 +105,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
- release: {
+ [TOKEN_TYPE_RELEASE]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -106,7 +118,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-release'),
},
- label: {
+ [TOKEN_TYPE_LABEL]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
@@ -116,7 +128,7 @@ export default class AvailableDropdownMappings {
},
element: this.container.querySelector('#js-dropdown-label'),
},
- 'my-reaction': {
+ [TOKEN_TYPE_MY_REACTION]: {
reference: null,
gl: DropdownEmoji,
element: this.container.querySelector('#js-dropdown-my-reaction'),
@@ -126,12 +138,12 @@ export default class AvailableDropdownMappings {
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-wip'),
},
- confidential: {
+ [TOKEN_TYPE_CONFIDENTIAL]: {
reference: null,
gl: DropdownNonUser,
element: this.container.querySelector('#js-dropdown-confidential'),
},
- 'target-branch': {
+ [TOKEN_TYPE_TARGET_BRANCH]: {
reference: null,
gl: DropdownNonUser,
extraArguments: {
diff --git a/app/assets/javascripts/filtered_search/constants.js b/app/assets/javascripts/filtered_search/constants.js
index e07dccd11e8..b328ae6a872 100644
--- a/app/assets/javascripts/filtered_search/constants.js
+++ b/app/assets/javascripts/filtered_search/constants.js
@@ -1,4 +1,17 @@
-export const USER_TOKEN_TYPES = ['author', 'assignee', 'approved-by', 'reviewer', 'attention'];
+import {
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_REVIEWER,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+
+export const USER_TOKEN_TYPES = [
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_APPROVED_BY,
+ TOKEN_TYPE_REVIEWER,
+ 'attention',
+];
export const DROPDOWN_TYPE = {
hint: 'hint',
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 22e1604871a..38909db0555 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -1,4 +1,5 @@
import { last } from 'lodash';
+import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import FilteredSearchDropdownManager from './filtered_search_dropdown_manager';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
@@ -113,7 +114,7 @@ export default class DropdownUtils {
visualToken &&
visualToken.querySelector('.value') &&
visualToken.querySelector('.value').textContent.trim();
- if (tokenName === 'label' && tokenValue) {
+ if (tokenName === TOKEN_TYPE_LABEL && tokenValue) {
// remove leading symbol and wrapping quotes
tokenValue = tokenValue.replace(/^~("|')?(.*)/, '$2').replace(/("|')$/, '');
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index bc0f5398b4c..16c70fdd069 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -10,8 +10,12 @@ import {
DOWN_KEY_CODE,
} from '~/lib/utils/keycodes';
import { __ } from '~/locale';
-import { addClassIfElementExists } from '../lib/utils/dom_utils';
-import { visitUrl, getUrlParamsArray, getParameterByName } from '../lib/utils/url_utility';
+import { addClassIfElementExists } from '~/lib/utils/dom_utils';
+import { visitUrl, getUrlParamsArray, getParameterByName } from '~/lib/utils/url_utility';
+import {
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import DropdownUtils from './dropdown_utils';
import eventHub from './event_hub';
@@ -675,7 +679,7 @@ export default class FilteredSearchManager {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- const tokenName = 'assignee';
+ const tokenName = TOKEN_TYPE_ASSIGNEE;
const canEdit = this.canEdit && this.canEdit(tokenName);
const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]);
const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]);
@@ -688,7 +692,7 @@ export default class FilteredSearchManager {
const id = parseInt(value, 10);
if (usernameParams[id]) {
hasFilteredSearch = true;
- const tokenName = 'author';
+ const tokenName = TOKEN_TYPE_AUTHOR;
const canEdit = this.canEdit && this.canEdit(tokenName);
const operator = FilteredSearchVisualTokens.getOperatorToken(usernameParams[id]);
const valueToken = FilteredSearchVisualTokens.getValueToken(usernameParams[id]);
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index 0c01220a7be..4994559e923 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,6 @@
import { spriteIcon } from '~/lib/utils/common_utils';
import { objectToQuery } from '~/lib/utils/url_utility';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchContainer from './container';
import VisualTokenValue from './visual_token_value';
@@ -38,7 +39,7 @@ export default class FilteredSearchVisualTokens {
lastVisualToken,
isLastVisualTokenValid:
lastVisualToken === null ||
- lastVisualToken.className.indexOf('filtered-search-term') !== -1 ||
+ lastVisualToken.className.indexOf(FILTERED_SEARCH_TERM) !== -1 ||
(lastVisualToken &&
lastVisualToken.querySelector('.operator') !== null &&
lastVisualToken.querySelector('.value') !== null),
@@ -113,7 +114,7 @@ export default class FilteredSearchVisualTokens {
} = options;
const li = document.createElement('li');
li.classList.add('js-visual-token');
- li.classList.add(isSearchTerm ? 'filtered-search-term' : 'filtered-search-token');
+ li.classList.add(isSearchTerm ? FILTERED_SEARCH_TERM : 'filtered-search-token');
if (!isSearchTerm) {
li.classList.add(tokenClass);
@@ -239,7 +240,7 @@ export default class FilteredSearchVisualTokens {
static addSearchVisualToken(searchTerm) {
const { lastVisualToken } = FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
- if (lastVisualToken && lastVisualToken.classList.contains('filtered-search-term')) {
+ if (lastVisualToken && lastVisualToken.classList.contains(FILTERED_SEARCH_TERM)) {
lastVisualToken.querySelector('.name').textContent += ` ${searchTerm}`;
} else {
FilteredSearchVisualTokens.addVisualTokenElement({
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index d6e7887f93f..8aa99ec52f9 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -7,13 +7,20 @@ import {
TOKEN_TITLE_MILESTONE,
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_RELEASE,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_REVIEWER,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
export const tokenKeys = [
{
formattedKey: TOKEN_TITLE_AUTHOR,
- key: 'author',
+ key: TOKEN_TYPE_AUTHOR,
type: 'string',
param: 'username',
symbol: '@',
@@ -22,7 +29,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_ASSIGNEE,
- key: 'assignee',
+ key: TOKEN_TYPE_ASSIGNEE,
type: 'string',
param: 'username',
symbol: '@',
@@ -31,7 +38,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_MILESTONE,
- key: 'milestone',
+ key: TOKEN_TYPE_MILESTONE,
type: 'string',
param: 'title',
symbol: '%',
@@ -40,7 +47,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_RELEASE,
- key: 'release',
+ key: TOKEN_TYPE_RELEASE,
type: 'string',
param: 'tag',
symbol: '',
@@ -49,7 +56,7 @@ export const tokenKeys = [
},
{
formattedKey: TOKEN_TITLE_LABEL,
- key: 'label',
+ key: TOKEN_TYPE_LABEL,
type: 'array',
param: 'name[]',
symbol: '~',
@@ -62,7 +69,7 @@ if (gon.current_user_id) {
// Appending tokenkeys only logged-in
tokenKeys.push({
formattedKey: TOKEN_TITLE_MY_REACTION,
- key: 'my-reaction',
+ key: TOKEN_TYPE_MY_REACTION,
type: 'string',
param: 'emoji',
symbol: '',
@@ -74,7 +81,7 @@ if (gon.current_user_id) {
export const alternativeTokenKeys = [
{
formattedKey: TOKEN_TITLE_LABEL,
- key: 'label',
+ key: TOKEN_TYPE_LABEL,
type: 'string',
param: 'name',
symbol: '~',
@@ -85,77 +92,77 @@ export const conditions = flattenDeep(
[
{
url: 'assignee_id=None',
- tokenKey: 'assignee',
+ tokenKey: TOKEN_TYPE_ASSIGNEE,
value: __('None'),
},
{
url: 'assignee_id=Any',
- tokenKey: 'assignee',
+ tokenKey: TOKEN_TYPE_ASSIGNEE,
value: __('Any'),
},
{
url: 'reviewer_id=None',
- tokenKey: 'reviewer',
+ tokenKey: TOKEN_TYPE_REVIEWER,
value: __('None'),
},
{
url: 'reviewer_id=Any',
- tokenKey: 'reviewer',
+ tokenKey: TOKEN_TYPE_REVIEWER,
value: __('Any'),
},
{
url: 'author_username=support-bot',
- tokenKey: 'author',
+ tokenKey: TOKEN_TYPE_AUTHOR,
value: 'support-bot',
},
{
url: 'milestone_title=None',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('None'),
},
{
url: 'milestone_title=Any',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Any'),
},
{
url: 'milestone_title=%23upcoming',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Upcoming'),
},
{
url: 'milestone_title=%23started',
- tokenKey: 'milestone',
+ tokenKey: TOKEN_TYPE_MILESTONE,
value: __('Started'),
},
{
url: 'release_tag=None',
- tokenKey: 'release',
+ tokenKey: TOKEN_TYPE_RELEASE,
value: __('None'),
},
{
url: 'release_tag=Any',
- tokenKey: 'release',
+ tokenKey: TOKEN_TYPE_RELEASE,
value: __('Any'),
},
{
url: 'label_name[]=None',
- tokenKey: 'label',
+ tokenKey: TOKEN_TYPE_LABEL,
value: __('None'),
},
{
url: 'label_name[]=Any',
- tokenKey: 'label',
+ tokenKey: TOKEN_TYPE_LABEL,
value: __('Any'),
},
{
url: 'my_reaction_emoji=None',
- tokenKey: 'my-reaction',
+ tokenKey: TOKEN_TYPE_MY_REACTION,
value: __('None'),
},
{
url: 'my_reaction_emoji=Any',
- tokenKey: 'my-reaction',
+ tokenKey: TOKEN_TYPE_MY_REACTION,
value: __('Any'),
},
].map((condition) => {
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 1ad2006d689..33fda7533e4 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -8,6 +8,7 @@ import { createAlert } from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
+import { TOKEN_TYPE_LABEL } from '~/vue_shared/components/filtered_search_bar/constants';
export default class VisualTokenValue {
constructor(tokenValue, tokenType, tokenOperator) {
@@ -23,7 +24,7 @@ export default class VisualTokenValue {
return;
}
- if (tokenType === 'label') {
+ if (tokenType === TOKEN_TYPE_LABEL) {
this.updateLabelTokenColor(tokenValueContainer);
} else if (USER_TOKEN_TYPES.includes(tokenType)) {
this.updateUserTokenAppearance(tokenValueContainer, tokenValueElement);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index dc6c4642e94..9e804b60d59 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -114,6 +114,7 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
* @param {object} [options.parent] - Reference to parent element under which alert needs to appear. Defaults to `document`.
* @param {Function} [options.onDismiss] - Handler to call when this alert is dismissed.
* @param {string} [options.containerSelector] - Selector for the container of the alert
+ * @param {boolean} [options.preservePrevious] - Set to `true` to preserve previous alerts. Defaults to `false`.
* @param {object} [options.primaryButton] - Object describing primary button of alert
* @param {string} [options.primaryButton.link] - Href of primary button
* @param {string} [options.primaryButton.text] - Text of primary button
@@ -131,6 +132,7 @@ const createAlert = function createAlert({
variant = VARIANT_DANGER,
parent = document,
containerSelector = '.flash-container',
+ preservePrevious = false,
primaryButton = null,
secondaryButton = null,
onDismiss = null,
@@ -143,7 +145,11 @@ const createAlert = function createAlert({
if (!alertContainer) return null;
const el = document.createElement('div');
- alertContainer.appendChild(el);
+ if (preservePrevious) {
+ alertContainer.appendChild(el);
+ } else {
+ alertContainer.replaceChildren(el);
+ }
return new Vue({
el,
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
index 33ab1d5cd7f..89b6885091c 100644
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
@@ -1,6 +1,7 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { snakeCase } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
@@ -15,7 +16,7 @@ export default {
ProjectAvatar,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [trackingMixin],
inject: ['vuexModule'],
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 49c47e9d778..293cd2df16f 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -538,7 +538,12 @@ class GfmAutoComplete {
setupLabels($input) {
const instance = this;
const fetchData = this.fetchData.bind(this);
- const LABEL_COMMAND = { LABEL: '/label', UNLABEL: '/unlabel', RELABEL: '/relabel' };
+ const LABEL_COMMAND = {
+ LABEL: '/label',
+ LABELS: '/labels',
+ UNLABEL: '/unlabel',
+ RELABEL: '/relabel',
+ };
let command = '';
$input.atwho({
@@ -570,13 +575,9 @@ class GfmAutoComplete {
matcher(flag, subtext) {
const subtextNodes = subtext.split(/\n+/g).pop().split(GfmAutoComplete.regexSubtext);
- // Check if ~ is followed by '/label', '/relabel' or '/unlabel' commands.
+ // Check if ~ is followed by '/label', '/labels', '/relabel' or '/unlabel' commands.
command = subtextNodes.find((node) => {
- if (
- node === LABEL_COMMAND.LABEL ||
- node === LABEL_COMMAND.RELABEL ||
- node === LABEL_COMMAND.UNLABEL
- ) {
+ if (Object.values(LABEL_COMMAND).includes(node)) {
return node;
}
return null;
@@ -621,7 +622,7 @@ class GfmAutoComplete {
// The `LABEL_COMMAND.RELABEL` is intentionally skipped
// because we want to return all the labels (unfiltered) for that command.
- if (command === LABEL_COMMAND.LABEL) {
+ 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) {
@@ -996,7 +997,7 @@ GfmAutoComplete.Issues = {
return value.reference || '${atwho-at}${id}';
},
templateFunction({ id, title, reference }) {
- return `<li><small>${reference || id}</small> ${escape(title)}</li>`;
+ return `<li><small>${escape(reference || id)}</small> ${escape(title)}</li>`;
},
};
// Milestones
diff --git a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
index f17a05999b0..bf71f682048 100644
--- a/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
+++ b/app/assets/javascripts/gitlab_pages/components/pages_pipeline_wizard.vue
@@ -2,7 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
import { captureException } from '@sentry/browser';
import PipelineWizard from '~/pipeline_wizard/pipeline_wizard.vue';
-import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml';
+import PagesWizardTemplate from '~/pipeline_wizard/templates/pages.yml?raw';
import { logError } from '~/lib/logger';
import { s__ } from '~/locale';
import { redirectTo } from '~/lib/utils/url_utility';
diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue
new file mode 100644
index 00000000000..89dc68ec73e
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert.vue
@@ -0,0 +1,76 @@
+<script>
+import { GlAlert, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import Tracking from '~/tracking';
+import { UPGRADE_DOCS_URL, ABOUT_RELEASES_PAGE } from '../constants';
+
+export default {
+ name: 'SecurityPatchUpgradeAlert',
+ i18n: {
+ alertTitle: s__('VersionCheck|Critical security upgrade available'),
+ alertBody: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}. We strongly recommend upgrading your GitLab installation. %{link}',
+ ),
+ learnMore: s__('VersionCheck|Learn more about this critical security release.'),
+ primaryButtonText: s__('VersionCheck|Upgrade now'),
+ },
+ components: {
+ GlAlert,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ currentVersion: {
+ type: String,
+ required: true,
+ },
+ },
+ mounted() {
+ this.track('render', {
+ label: 'security_patch_upgrade_alert',
+ property: this.currentVersion,
+ });
+ },
+ methods: {
+ trackLearnMoreClick() {
+ this.track('click_link', {
+ label: 'security_patch_upgrade_alert_learn_more',
+ property: this.currentVersion,
+ });
+ },
+ trackUpgradeNowClick() {
+ this.track('click_link', {
+ label: 'security_patch_upgrade_alert_upgrade_now',
+ property: this.currentVersion,
+ });
+ },
+ },
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+};
+</script>
+
+<template>
+ <gl-alert :title="$options.i18n.alertTitle" variant="danger" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.alertBody">
+ <template #currentVersion>
+ <span class="gl-font-weight-bold">{{ currentVersion }}</span>
+ </template>
+ <template #link>
+ <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ <template #actions>
+ <gl-button
+ :href="$options.UPGRADE_DOCS_URL"
+ variant="confirm"
+ @click="trackUpgradeNowClick"
+ >{{ $options.i18n.primaryButtonText }}</gl-button
+ >
+ </template>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
new file mode 100644
index 00000000000..4638ba8a268
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue
@@ -0,0 +1,160 @@
+<script>
+import { GlModal, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { glEmojiTag } from '~/emoji';
+import { s__, sprintf } from '~/locale';
+import Tracking from '~/tracking';
+import { getHideAlertModalCookie, setHideAlertModalCookie } from '../utils';
+import {
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+ TRACKING_ACTIONS,
+ TRACKING_LABELS,
+} from '../constants';
+
+export default {
+ name: 'SecurityPatchUpgradeAlertModal',
+ i18n: {
+ modalTitle: s__('VersionCheck|Important notice - Critical security release'),
+ modalBodyNoStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation immediately.',
+ ),
+ modalBodyStableVersions: s__(
+ 'VersionCheck|You are currently on version %{currentVersion}! We strongly recommend upgrading your GitLab installation to one of the following versions immediately: %{latestStableVersions}.',
+ ),
+ modalDetails: s__('VersionCheck|%{details}'),
+ learnMore: s__('VersionCheck|Learn more about this critical security release.'),
+ primaryButtonText: s__('VersionCheck|Upgrade now'),
+ secondaryButtonText: s__('VersionCheck|Remind me again in 3 days'),
+ },
+ components: {
+ GlModal,
+ GlSprintf,
+ GlLink,
+ GlButton,
+ },
+ directives: {
+ SafeHtml,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ currentVersion: {
+ type: String,
+ required: true,
+ },
+ details: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ latestStableVersions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ visible: true,
+ };
+ },
+ computed: {
+ alertEmoji() {
+ return glEmojiTag('rotating_light');
+ },
+ modalBody() {
+ if (this.latestStableVersions?.length > 0) {
+ return this.$options.i18n.modalBodyStableVersions;
+ }
+
+ return this.$options.i18n.modalBodyNoStableVersions;
+ },
+ modalDetails() {
+ return sprintf(this.$options.i18n.modalDetails, { details: this.details });
+ },
+ latestStableVersionsStrings() {
+ return this.latestStableVersions?.length > 0 ? this.latestStableVersions.join(', ') : '';
+ },
+ },
+ created() {
+ if (getHideAlertModalCookie(this.currentVersion)) {
+ this.visible = false;
+ return;
+ }
+
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.RENDER, TRACKING_LABELS.MODAL);
+ },
+ methods: {
+ dispatchTrackingEvent(action, label) {
+ this.track(action, {
+ label,
+ property: this.currentVersion,
+ });
+ },
+ trackLearnMoreClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.LEARN_MORE_LINK);
+ },
+ trackRemindMeLaterClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.REMIND_ME_BTN);
+ setHideAlertModalCookie(this.currentVersion);
+ this.$refs.alertModal.hide();
+ },
+ trackUpgradeNowClick() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_LINK, TRACKING_LABELS.UPGRADE_BTN_LINK);
+ setHideAlertModalCookie(this.currentVersion);
+ },
+ trackModalDismissed() {
+ this.dispatchTrackingEvent(TRACKING_ACTIONS.CLICK_BUTTON, TRACKING_LABELS.DISMISS);
+ },
+ },
+ safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
+ UPGRADE_DOCS_URL,
+ ABOUT_RELEASES_PAGE,
+ ALERT_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="alertModal"
+ :modal-id="$options.ALERT_MODAL_ID"
+ :visible="visible"
+ @close="trackModalDismissed"
+ >
+ <template #modal-title>
+ <span v-safe-html:[$options.safeHtmlConfig]="alertEmoji"></span>
+ <span data-testid="alert-modal-title">{{ $options.i18n.modalTitle }}</span>
+ </template>
+ <template #default>
+ <div data-testid="alert-modal-body" class="gl-mb-6">
+ <gl-sprintf :message="modalBody">
+ <template #currentVersion>
+ <span class="gl-font-weight-bold">{{ currentVersion }}</span>
+ </template>
+ <template #latestStableVersions>
+ <span class="gl-font-weight-bold">{{ latestStableVersionsStrings }}</span>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div v-if="details" data-testid="alert-modal-details" class="gl-mb-6">
+ {{ modalDetails }}
+ </div>
+ <gl-link :href="$options.ABOUT_RELEASES_PAGE" @click="trackLearnMoreClick">{{
+ $options.i18n.learnMore
+ }}</gl-link>
+ </template>
+ <template #modal-footer>
+ <gl-button data-testid="alert-modal-remind-button" @click="trackRemindMeLaterClick">{{
+ $options.i18n.secondaryButtonText
+ }}</gl-button>
+ <gl-button
+ data-testid="alert-modal-upgrade-button"
+ :href="$options.UPGRADE_DOCS_URL"
+ variant="confirm"
+ @click="trackUpgradeNowClick"
+ >{{ $options.i18n.primaryButtonText }}</gl-button
+ >
+ </template>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/gitlab_version_check/constants.js b/app/assets/javascripts/gitlab_version_check/constants.js
index 259723a4e22..049397148ab 100644
--- a/app/assets/javascripts/gitlab_version_check/constants.js
+++ b/app/assets/javascripts/gitlab_version_check/constants.js
@@ -7,3 +7,25 @@ export const STATUS_TYPES = {
};
export const UPGRADE_DOCS_URL = helpPagePath('update/index');
+
+export const ABOUT_RELEASES_PAGE = 'https://about.gitlab.com/releases/categories/releases/';
+
+export const ALERT_MODAL_ID = 'security-patch-upgrade-alert-modal';
+
+export const COOKIE_EXPIRATION = 3;
+
+export const COOKIE_SUFFIX = '-hide-alert-modal';
+
+export const TRACKING_ACTIONS = {
+ RENDER: 'render',
+ CLICK_LINK: 'click_link',
+ CLICK_BUTTON: 'click_button',
+};
+
+export const TRACKING_LABELS = {
+ MODAL: 'security_patch_upgrade_alert_modal',
+ LEARN_MORE_LINK: 'security_patch_upgrade_alert_modal_learn_more',
+ REMIND_ME_BTN: 'security_patch_upgrade_alert_modal_remind_3_days',
+ UPGRADE_BTN_LINK: 'security_patch_upgrade_alert_modal_upgrade_now',
+ DISMISS: 'security_patch_upgrade_alert_modal_close',
+};
diff --git a/app/assets/javascripts/gitlab_version_check/index.js b/app/assets/javascripts/gitlab_version_check/index.js
index 203ce10ef57..edb7e9abe49 100644
--- a/app/assets/javascripts/gitlab_version_check/index.js
+++ b/app/assets/javascripts/gitlab_version_check/index.js
@@ -1,50 +1,98 @@
import Vue from 'vue';
-import * as Sentry from '@sentry/browser';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import axios from '~/lib/utils/axios_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import GitlabVersionCheckBadge from './components/gitlab_version_check_badge.vue';
+import SecurityPatchUpgradeAlert from './components/security_patch_upgrade_alert.vue';
+import SecurityPatchUpgradeAlertModal from './components/security_patch_upgrade_alert_modal.vue';
-const mountGitlabVersionCheckBadge = ({ el, status }) => {
- const { size } = el.dataset;
+const mountGitlabVersionCheckBadge = (el) => {
+ const { size, version } = el.dataset;
const actionable = parseBoolean(el.dataset.actionable);
- return new Vue({
- el,
- render(createElement) {
- return createElement(GitlabVersionCheckBadge, {
- props: {
- size,
- actionable,
- status,
- },
- });
- },
- });
+ try {
+ const { severity } = JSON.parse(version);
+
+ // If no severity (status) data don't worry about rendering
+ if (!severity) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GitlabVersionCheckBadge, {
+ props: {
+ size,
+ actionable,
+ status: severity,
+ },
+ });
+ },
+ });
+ } catch {
+ return null;
+ }
};
-export default async () => {
- const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
+const mountSecurityPatchUpgradeAlert = (el) => {
+ const { currentVersion } = el.dataset;
- // If there are no version check elements, exit out
- if (versionCheckBadges?.length <= 0) {
+ try {
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SecurityPatchUpgradeAlert, {
+ props: {
+ currentVersion,
+ },
+ });
+ },
+ });
+ } catch {
return null;
}
+};
- const status = await axios
- .get(joinPaths('/', gon.relative_url_root, '/admin/version_check.json'))
- .then((res) => {
- return res.data?.severity;
- })
- .catch((e) => {
- Sentry.captureException(e);
- return null;
+const mountSecurityPatchUpgradeAlertModal = (el) => {
+ const { currentVersion, version } = el.dataset;
+
+ try {
+ const { details, latestStableVersions } = convertObjectPropsToCamelCase(JSON.parse(version));
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(SecurityPatchUpgradeAlertModal, {
+ props: {
+ currentVersion,
+ details,
+ latestStableVersions,
+ },
+ });
+ },
});
+ } catch {
+ return null;
+ }
+};
+
+export default () => {
+ const renderedApps = [];
- // If we don't have a status there is nothing to render
- if (status) {
- return versionCheckBadges.map((el) => mountGitlabVersionCheckBadge({ el, status }));
+ const securityPatchUpgradeAlert = document.getElementById('js-security-patch-upgrade-alert');
+ const securityPatchUpgradeAlertModal = document.getElementById(
+ 'js-security-patch-upgrade-alert-modal',
+ );
+ const versionCheckBadges = [...document.querySelectorAll('.js-gitlab-version-check-badge')];
+
+ if (securityPatchUpgradeAlert) {
+ renderedApps.push(mountSecurityPatchUpgradeAlert(securityPatchUpgradeAlert));
}
- return null;
+ if (securityPatchUpgradeAlertModal) {
+ renderedApps.push(mountSecurityPatchUpgradeAlertModal(securityPatchUpgradeAlertModal));
+ }
+
+ renderedApps.push(...versionCheckBadges.map((el) => mountGitlabVersionCheckBadge(el)));
+
+ return renderedApps;
};
diff --git a/app/assets/javascripts/gitlab_version_check/utils.js b/app/assets/javascripts/gitlab_version_check/utils.js
new file mode 100644
index 00000000000..d2f4349483c
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check/utils.js
@@ -0,0 +1,18 @@
+import { setCookie, getCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { COOKIE_EXPIRATION, COOKIE_SUFFIX } from './constants';
+
+const buildKey = (currentVersion) => {
+ return `${currentVersion}${COOKIE_SUFFIX}`;
+};
+
+export const setHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ setCookie(key, true, { expires: COOKIE_EXPIRATION });
+};
+
+export const getHideAlertModalCookie = (currentVersion) => {
+ const key = buildKey(currentVersion);
+
+ return parseBoolean(getCookie(key));
+};
diff --git a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
index 64f547f933a..3ecaee435e2 100644
--- a/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
+++ b/app/assets/javascripts/graphql_shared/fragments/alert.fragment.graphql
@@ -1,4 +1,5 @@
fragment AlertListItem on AlertManagementAlert {
+ id
iid
title
severity
diff --git a/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
index ba1e607bc10..9ec87ba291d 100644
--- a/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
+++ b/app/assets/javascripts/graphql_shared/mutations/alert_status_update.mutation.graphql
@@ -4,6 +4,7 @@ mutation updateAlertStatus($projectPath: ID!, $status: AlertManagementStatus!, $
updateAlertStatus(input: { iid: $iid, status: $status, projectPath: $projectPath }) {
errors
alert {
+ id
iid
status
endedAt
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index e8b0174b8f6..5467105ac3c 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -7,10 +7,12 @@
"CiGroupVariable",
"CiInstanceVariable",
"CiManualVariable",
- "CiProjectVariable"
+ "CiProjectVariable",
+ "PipelineScheduleVariable"
],
"CommitSignature": [
"GpgSignature",
+ "SshSignature",
"X509Signature"
],
"CurrentUserTodos": [
@@ -144,10 +146,13 @@
"WorkItemWidget": [
"WorkItemWidgetAssignees",
"WorkItemWidgetDescription",
+ "WorkItemWidgetHealthStatus",
"WorkItemWidgetHierarchy",
"WorkItemWidgetIteration",
"WorkItemWidgetLabels",
"WorkItemWidgetMilestone",
+ "WorkItemWidgetNotes",
+ "WorkItemWidgetProgress",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetWeight"
diff --git a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
index 8debc6113d1..77b95bb8910 100644
--- a/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
+++ b/app/assets/javascripts/graphql_shared/queries/alert_details.query.graphql
@@ -5,6 +5,7 @@ query alertDetails($fullPath: ID!, $alertId: String) {
id
alertManagementAlerts(iid: $alertId) {
nodes {
+ id
...AlertDetailItem
}
}
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 15f5a3518a5..46d5341ea97 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,21 +1,25 @@
<script>
-import { GlLoadingIcon, GlModal } from '@gitlab/ui';
+import { GlLoadingIcon, GlModal, GlEmptyState } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
-import { HIDDEN_CLASS } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
+import { COMMON_STR } from '../constants';
import eventHub from '../event_hub';
import GroupsComponent from './groups.vue';
-import EmptyState from './empty_state.vue';
export default {
+ i18n: {
+ searchEmptyState: {
+ title: __('No results found'),
+ description: __('Edit your search and try again'),
+ },
+ },
components: {
GroupsComponent,
GlModal,
GlLoadingIcon,
- EmptyState,
+ GlEmptyState,
},
props: {
action: {
@@ -40,20 +44,14 @@ export default {
type: Boolean,
required: true,
},
- renderEmptyState: {
- type: Boolean,
- required: false,
- default: false,
- },
},
data() {
return {
isModalVisible: false,
isLoading: true,
- isSearchEmpty: false,
+ fromSearch: false,
targetGroup: null,
targetParentGroup: null,
- showEmptyState: false,
};
},
computed: {
@@ -79,6 +77,9 @@ export default {
groups() {
return this.store.getGroups();
},
+ hasGroups() {
+ return this.groups && this.groups.length > 0;
+ },
pageInfo() {
return this.store.getPaginationInfo();
},
@@ -231,47 +232,17 @@ export default {
this.targetGroup.isBeingRemoved = false;
});
},
- showLegacyEmptyState() {
- const { containerEl } = this;
-
- if (!containerEl) return;
-
- const contentListEl = containerEl.querySelector(CONTENT_LIST_CLASS);
- const emptyStateEl = containerEl.querySelector('.empty-state');
-
- if (contentListEl) {
- contentListEl.remove();
- }
-
- if (emptyStateEl) {
- emptyStateEl.classList.remove(HIDDEN_CLASS);
- }
- },
updatePagination(headers) {
this.store.setPaginationInfo(headers);
},
updateGroups(groups, fromSearch) {
- const hasGroups = groups && groups.length > 0;
-
- if (this.renderEmptyState) {
- this.isSearchEmpty = fromSearch && !hasGroups;
- } else {
- this.isSearchEmpty = !hasGroups;
- }
+ this.fromSearch = fromSearch;
if (fromSearch) {
this.store.setSearchedGroups(groups);
} else {
this.store.setGroups(groups);
}
-
- if (this.action && !hasGroups && !fromSearch) {
- if (this.renderEmptyState) {
- this.showEmptyState = true;
- } else {
- this.showLegacyEmptyState();
- }
- }
},
},
};
@@ -285,14 +256,16 @@ export default {
size="lg"
class="loading-animation prepend-top-20"
/>
- <groups-component
- v-else
- :groups="groups"
- :search-empty="isSearchEmpty"
- :page-info="pageInfo"
- :action="action"
- />
- <empty-state v-if="showEmptyState" />
+ <template v-else>
+ <groups-component v-if="hasGroups" :groups="groups" :page-info="pageInfo" :action="action" />
+ <gl-empty-state
+ v-else-if="fromSearch"
+ :title="$options.i18n.searchEmptyState.title"
+ :description="$options.i18n.searchEmptyState.description"
+ data-testid="search-empty-state"
+ />
+ <slot v-else name="empty-state"></slot>
+ </template>
<gl-modal
modal-id="leave-group-modal"
:visible="isModalVisible"
diff --git a/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue
new file mode 100644
index 00000000000..535758750f9
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/archived_projects_empty_state.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ i18n: {
+ title: s__('GroupsEmptyState|No archived projects.'),
+ },
+ inject: ['newProjectIllustration'],
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="newProjectIllustration"
+ :svg-height="100"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue b/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue
new file mode 100644
index 00000000000..7223321bf3e
--- /dev/null
+++ b/app/assets/javascripts/groups/components/empty_states/shared_projects_empty_state.vue
@@ -0,0 +1,21 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+export default {
+ components: { GlEmptyState },
+ i18n: {
+ title: s__('GroupsEmptyState|No shared projects.'),
+ },
+ inject: ['newProjectIllustration'],
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="$options.i18n.title"
+ :svg-path="newProjectIllustration"
+ :svg-height="100"
+ />
+</template>
diff --git a/app/assets/javascripts/groups/components/empty_state.vue b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
index 4219b52737d..955cb1ca63e 100644
--- a/app/assets/javascripts/groups/components/empty_state.vue
+++ b/app/assets/javascripts/groups/components/empty_states/subgroups_and_projects_empty_state.vue
@@ -83,7 +83,6 @@ export default {
</div>
<gl-empty-state
v-else
- class="gl-mt-5"
:title="$options.i18n.withoutLinks.title"
:svg-path="emptySubgroupIllustration"
:description="$options.i18n.withoutLinks.description"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 961af800971..d9781ef9c84 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -9,8 +9,8 @@ import {
GlPopover,
GlLink,
GlTooltipDirective,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { visitUrl } from '~/lib/utils/url_utility';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
@@ -29,7 +29,7 @@ import ItemTypeIcon from './item_type_icon.vue';
export default {
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlAvatar,
@@ -200,11 +200,9 @@ export default {
class="no-expand gl-mr-3 gl-text-gray-900!"
:itemprop="microdata.nameItemprop"
>
- {{
- // ending bracket must be by closing tag to prevent
- // link hover text-decoration from over-extending
- group.name
- }}
+ <!-- ending bracket must be by closing tag to prevent -->
+ <!-- link hover text-decoration from over-extending -->
+ {{ group.name }}
</a>
<gl-icon
v-gl-tooltip.hover.bottom
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
index 9a1ea2f1812..5f997ecc7ba 100644
--- a/app/assets/javascripts/groups/components/group_name_and_path.vue
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -59,7 +59,7 @@ export default {
learnMore: s__('Groups|Learn more'),
},
inputSize: { md: 'lg' },
- changingGroupPathHelpPagePath: helpPagePath('user/group/index', {
+ changingGroupPathHelpPagePath: helpPagePath('user/group/manage', {
anchor: 'change-a-groups-path',
}),
mattermostDataBindName: 'create_chat_team',
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 43aa0753082..5075be62214 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,5 +1,4 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -12,7 +11,6 @@ export default {
},
components: {
PaginationLinks,
- GlEmptyState,
},
props: {
groups: {
@@ -23,10 +21,6 @@ export default {
type: Object,
required: true,
},
- searchEmpty: {
- type: Boolean,
- required: true,
- },
action: {
type: String,
required: false,
@@ -46,18 +40,11 @@ export default {
<template>
<div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
- <gl-empty-state
- v-if="searchEmpty"
- :title="$options.i18n.emptyStateTitle"
- :description="$options.i18n.emptyStateDescription"
+ <group-folder :groups="groups" :action="action" />
+ <pagination-links
+ :change="change"
+ :page-info="pageInfo"
+ class="d-flex justify-content-center gl-mt-3"
/>
- <template v-else>
- <group-folder :groups="groups" :action="action" />
- <pagination-links
- :change="change"
- :page-info="pageInfo"
- class="d-flex justify-content-center gl-mt-3"
- />
- </template>
</div>
</template>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 46ab30367a0..79a2e11b0bb 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -13,19 +13,32 @@ import {
} from '../constants';
import eventHub from '../event_hub';
import GroupsApp from './app.vue';
+import SubgroupsAndProjectsEmptyState from './empty_states/subgroups_and_projects_empty_state.vue';
+import SharedProjectsEmptyState from './empty_states/shared_projects_empty_state.vue';
+import ArchivedProjectsEmptyState from './empty_states/archived_projects_empty_state.vue';
const [SORTING_ITEM_NAME] = OVERVIEW_TABS_SORTING_ITEMS;
const MIN_SEARCH_LENGTH = 3;
export default {
- components: { GlTabs, GlTab, GroupsApp, GlSearchBoxByType, GlSorting, GlSortingItem },
+ components: {
+ GlTabs,
+ GlTab,
+ GroupsApp,
+ GlSearchBoxByType,
+ GlSorting,
+ GlSortingItem,
+ SubgroupsAndProjectsEmptyState,
+ SharedProjectsEmptyState,
+ ArchivedProjectsEmptyState,
+ },
inject: ['endpoints', 'initialSort'],
data() {
const tabs = [
{
title: this.$options.i18n[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
key: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
- renderEmptyState: true,
+ emptyStateComponent: SubgroupsAndProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
store: new GroupsStore({ showSchemaMarkup: true }),
@@ -33,7 +46,7 @@ export default {
{
title: this.$options.i18n[ACTIVE_TAB_SHARED],
key: ACTIVE_TAB_SHARED,
- renderEmptyState: false,
+ emptyStateComponent: SharedProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_SHARED,
service: new GroupsService(this.endpoints[ACTIVE_TAB_SHARED]),
store: new GroupsStore(),
@@ -41,7 +54,7 @@ export default {
{
title: this.$options.i18n[ACTIVE_TAB_ARCHIVED],
key: ACTIVE_TAB_ARCHIVED,
- renderEmptyState: false,
+ emptyStateComponent: ArchivedProjectsEmptyState,
lazy: this.$route.name !== ACTIVE_TAB_ARCHIVED,
service: new GroupsService(this.endpoints[ACTIVE_TAB_ARCHIVED]),
store: new GroupsStore(),
@@ -158,18 +171,16 @@ export default {
<template>
<gl-tabs content-class="gl-pt-0" :value="activeTabIndex" @input="handleTabInput">
<gl-tab
- v-for="{ key, title, renderEmptyState, lazy, service, store } in tabs"
+ v-for="{ key, title, emptyStateComponent, lazy, service, store } in tabs"
:key="key"
:title="title"
:lazy="lazy"
>
- <groups-app
- :action="key"
- :service="service"
- :store="store"
- :hide-projects="false"
- :render-empty-state="renderEmptyState"
- />
+ <groups-app :action="key" :service="service" :store="store" :hide-projects="false">
+ <template v-if="emptyStateComponent" #empty-state>
+ <component :is="emptyStateComponent" />
+ </template>
+ </groups-app>
</gl-tab>
<template #tabs-end>
<li class="gl-flex-grow-1 gl-align-self-center gl-w-full gl-lg-w-auto gl-py-2">
diff --git a/app/assets/javascripts/groups/components/transfer_group_form.vue b/app/assets/javascripts/groups/components/transfer_group_form.vue
index 15a193f7cb8..3da417ebf0a 100644
--- a/app/assets/javascripts/groups/components/transfer_group_form.vue
+++ b/app/assets/javascripts/groups/components/transfer_group_form.vue
@@ -73,6 +73,7 @@ export default {
:disabled="disableSubmitButton"
:phrase="confirmationPhrase"
:button-text="confirmButtonText"
+ button-qa-selector="transfer_group_button"
@confirm="$emit('confirm')"
/>
</div>
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 4d03a523486..f58781fa9ec 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -1,7 +1,9 @@
import Vue from 'vue';
+import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import { highCountTrim } from '~/lib/utils/text_utility';
import Tracking from '~/tracking';
import Translate from '~/vue_shared/translate';
+import { parseBoolean } from '~/lib/utils/common_utils';
/**
* Updates todo counter when todos are toggled.
@@ -99,6 +101,7 @@ function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
});
});
}
+
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-pipeline-minutes-link');
@@ -108,5 +111,23 @@ export function initNavUserDropdownTracking() {
}
}
+function initNewNavToggle() {
+ const el = document.querySelector('.js-new-nav-toggle');
+ if (!el) return false;
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(NewNavToggle, {
+ props: {
+ enabled: parseBoolean(el.dataset.enabled),
+ endpoint: el.dataset.endpoint,
+ },
+ });
+ },
+ });
+}
+
requestIdleCallback(initStatusTriggers);
requestIdleCallback(initNavUserDropdownTracking);
+requestIdleCallback(initNewNavToggle);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index 8fc0ce48e61..bf5daf29b21 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -4,7 +4,6 @@ import {
GlOutsideDirective as Outside,
GlIcon,
GlToken,
- GlSafeHtmlDirective as SafeHtml,
GlTooltipDirective,
GlResizeObserverDirective,
} from '@gitlab/ui';
@@ -56,7 +55,7 @@ export default {
false,
),
},
- directives: { SafeHtml, Outside, GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
+ directives: { Outside, GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
components: {
GlSearchBoxByType,
HeaderSearchDefaultItems,
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
index 025c48f355d..c85fb4f4158 100644
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
@@ -6,9 +6,9 @@ import {
GlAvatar,
GlAlert,
GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { mapState, mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import highlight from '~/lib/utils/highlight';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
index 332ccee510f..cda3379309c 100644
--- a/app/assets/javascripts/header_search/constants.js
+++ b/app/assets/javascripts/header_search/constants.js
@@ -26,6 +26,8 @@ export const GROUPS_CATEGORY = s__('GlobalSearch|Groups');
export const PROJECTS_CATEGORY = s__('GlobalSearch|Projects');
+export const USERS_CATEGORY = s__('GlobalSearch|Users');
+
export const ISSUES_CATEGORY = s__('GlobalSearch|Recent issues');
export const MERGE_REQUEST_CATEGORY = s__('GlobalSearch|Recent merge requests');
@@ -68,6 +70,7 @@ export const DROPDOWN_ORDER = [
RECENT_EPICS_CATEGORY,
GROUPS_CATEGORY,
PROJECTS_CATEGORY,
+ USERS_CATEGORY,
IN_THIS_PROJECT_CATEGORY,
SETTINGS_CATEGORY,
HELP_CATEGORY,
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index d02dc67d933..ef3da57c240 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -1,6 +1,7 @@
<script>
-import { GlModal, GlSafeHtmlDirective, GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { GlModal, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { n__ } from '~/locale';
import { leftSidebarViews, MAX_WINDOW_HEIGHT_COMPACT } from '../../constants';
import { createUnexpectedCommitError } from '../../lib/errors';
@@ -17,7 +18,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlTooltip: GlTooltipDirective,
},
data() {
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
index 5272c4310d8..dd343bc5f79 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/success_message.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
directives: {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index 67eedc6b37f..eba9bbcdf09 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -1,6 +1,7 @@
<script>
-import { GlAlert, GlLoadingIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
@@ -8,7 +9,7 @@ export default {
GlLoadingIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
message: {
diff --git a/app/assets/javascripts/ide/components/jobs/detail.vue b/app/assets/javascripts/ide/components/jobs/detail.vue
index 8d6a0b99e0c..9676233a443 100644
--- a/app/assets/javascripts/ide/components/jobs/detail.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail.vue
@@ -1,7 +1,8 @@
<script>
-import { GlTooltipDirective, GlButton, GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlIcon } from '@gitlab/ui';
import { throttle } from 'lodash';
import { mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import JobDescription from './detail/description.vue';
import ScrollButton from './detail/scroll_button.vue';
@@ -14,7 +15,7 @@ const scrollPositions = {
export default {
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlButton,
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 9a529bdcee1..ea1dbee4669 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -80,7 +80,7 @@ export default {
@click="createNewItem('blob')"
/>
</li>
- <li><upload :path="path" @create="createTempEntry" /></li>
+ <upload :path="path" @create="createTempEntry" />
<li>
<item-button
:label="__('New directory')"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 76d8a0aff3d..7c10e055e91 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -65,7 +65,7 @@ export default {
</script>
<template>
- <div>
+ <li>
<item-button
:class="buttonCssClasses"
:show-label="showLabel"
@@ -84,5 +84,5 @@ export default {
data-qa-selector="file_upload_field"
@change="openFile"
/>
- </div>
+ </li>
</template>
diff --git a/app/assets/javascripts/ide/components/panes/right.vue b/app/assets/javascripts/ide/components/panes/right.vue
index c74a5052573..da2d4fbe7f0 100644
--- a/app/assets/javascripts/ide/components/panes/right.vue
+++ b/app/assets/javascripts/ide/components/panes/right.vue
@@ -7,7 +7,6 @@ import PipelinesList from '../pipelines/list.vue';
import Clientside from '../preview/clientside.vue';
import ResizablePanel from '../resizable_panel.vue';
import TerminalView from '../terminal/view.vue';
-import SwitchEditorsView from '../switch_editors/switch_editors_view.vue';
import CollapsibleSidebar from './collapsible_sidebar.vue';
// Need to add the width of the nav buttons since the resizable container contains those as well
@@ -21,7 +20,7 @@ export default {
},
computed: {
...mapState('terminal', { isTerminalVisible: 'isVisible' }),
- ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled', 'canUseNewWebIde']),
+ ...mapState(['currentMergeRequestId', 'clientsidePreviewEnabled']),
...mapGetters(['packageJson']),
...mapState('rightPane', ['isOpen']),
showLivePreview() {
@@ -30,12 +29,6 @@ export default {
rightExtensionTabs() {
return [
{
- show: this.canUseNewWebIde,
- title: __('Switch editors'),
- views: [{ component: SwitchEditorsView, ...rightSidebarViews.switchEditors }],
- icon: 'bullhorn',
- },
- {
show: true,
title: __('Pipelines'),
views: [
@@ -60,7 +53,6 @@ export default {
},
},
WIDTH,
- SWITCH_EDITORS_VIEW_NAME: rightSidebarViews.switchEditors.name,
};
</script>
@@ -72,11 +64,6 @@ export default {
:min-size="$options.WIDTH"
:resizable="isOpen"
>
- <collapsible-sidebar
- class="gl-w-full"
- :extension-tabs="rightExtensionTabs"
- :init-open-view="$options.SWITCH_EDITORS_VIEW_NAME"
- side="right"
- />
+ <collapsible-sidebar class="gl-w-full" :extension-tabs="rightExtensionTabs" side="right" />
</resizable-panel>
</template>
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 7f513afe82e..7f662f528d7 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -1,17 +1,8 @@
<script>
-import {
- GlLoadingIcon,
- GlIcon,
- GlSafeHtmlDirective as SafeHtml,
- GlTabs,
- GlTab,
- GlBadge,
- GlAlert,
-} from '@gitlab/ui';
-import { escape } from 'lodash';
+import { GlLoadingIcon, GlIcon, GlTabs, GlTab, GlBadge, GlAlert } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import IDEServices from '~/ide/services';
-import { sprintf, __ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import JobsList from '../jobs/list.vue';
import EmptyState from './empty_state.vue';
@@ -48,16 +39,6 @@ export default {
'stages',
'isLoadingJobs',
]),
- ciLintText() {
- return sprintf(
- __('You can test your .gitlab-ci.yml in %{linkStart}CI Lint%{linkEnd}.'),
- {
- linkStart: `<a href="${escape(this.currentProject.web_url)}/-/ci/lint">`,
- linkEnd: '</a>',
- },
- false,
- );
- },
showLoadingIcon() {
return this.isLoadingPipeline && !this.hasLoadedPipeline;
},
@@ -101,9 +82,8 @@ export default {
:dismissible="false"
class="gl-mt-5"
>
- <p class="gl-mb-0">{{ __('Found errors in your .gitlab-ci.yml:') }}</p>
+ <p class="gl-mb-0">{{ __('Unable to create pipeline') }}</p>
<p class="gl-mb-0 break-word">{{ latestPipeline.yamlError }}</p>
- <p v-safe-html="ciLintText" class="gl-mb-0"></p>
</gl-alert>
<gl-tabs v-else>
<gl-tab :active="!pipelineFailed">
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 5f35dbdc5e7..3c9c0b1ade1 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -7,6 +7,7 @@ import {
EDITOR_TYPE_CODE,
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
+ EXTENSION_CI_SCHEMA_FILE_NAME_MATCH,
} from '~/editor/constants';
import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
@@ -26,6 +27,7 @@ import { performanceMarkAndMeasure } from '~/performance/utils';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import { viewerInformationForPath } from '~/vue_shared/components/content_viewer/lib/viewer_utils';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
leftSidebarViews,
viewerTypes,
@@ -53,6 +55,7 @@ export default {
DiffViewer,
FileTemplatesBar,
},
+ mixins: [glFeatureFlagMixin()],
props: {
file: {
type: Object,
@@ -145,6 +148,12 @@ export default {
showTabs() {
return !this.shouldHideEditor && this.isEditModeActive && this.previewMode;
},
+ isCiConfigFile() {
+ return (
+ this.file.path === EXTENSION_CI_SCHEMA_FILE_NAME_MATCH &&
+ this.editor?.getEditorType() === EDITOR_TYPE_CODE
+ );
+ },
},
watch: {
'file.name': {
@@ -232,8 +241,6 @@ export default {
return;
}
- this.registerSchemaForFile();
-
Promise.all([this.fetchFileData(), this.fetchEditorconfigRules()])
.then(() => {
this.createEditorInstance();
@@ -357,6 +364,8 @@ export default {
this.model.updateOptions(this.rules);
+ this.registerSchemaForFile();
+
this.model.onChange((model) => {
const { file } = model;
if (!file.active) return;
@@ -446,8 +455,33 @@ export default {
return Promise.resolve();
},
registerSchemaForFile() {
- const schema = this.getJsonSchemaForPath(this.file.path);
- registerSchema(schema);
+ const registerExternalSchema = () => {
+ const schema = this.getJsonSchemaForPath(this.file.path);
+ return registerSchema(schema);
+ };
+ const registerLocalSchema = async () => {
+ if (!this.CiSchemaExtension) {
+ const { CiSchemaExtension } = await import(
+ '~/editor/extensions/source_editor_ci_schema_ext'
+ ).catch((e) =>
+ createAlert({
+ message: e,
+ }),
+ );
+ this.CiSchemaExtension = CiSchemaExtension;
+ }
+ this.editor.use({ definition: this.CiSchemaExtension });
+ this.editor.registerCiSchema();
+ };
+
+ if (this.isCiConfigFile && this.glFeatures.schemaLinting) {
+ registerLocalSchema();
+ } else {
+ if (this.CiSchemaExtension) {
+ this.editor.unuse(this.CiSchemaExtension);
+ }
+ registerExternalSchema();
+ }
},
updateEditor(data) {
// Looks like our model wrapper `.dispose` causes the monaco editor to emit some position changes after
diff --git a/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue b/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
deleted file mode 100644
index 00164f65e33..00000000000
--- a/app/assets/javascripts/ide/components/switch_editors/switch_editors_view.vue
+++ /dev/null
@@ -1,103 +0,0 @@
-<script>
-import { GlButton, GlEmptyState, GlLink } from '@gitlab/ui';
-import { mapState } from 'vuex';
-import { createAlert } from '~/flash';
-import { logError } from '~/lib/logger';
-import axios from '~/lib/utils/axios_utils';
-import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
-import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
-import { s__, __ } from '~/locale';
-import eventHub from '../../eventhub';
-
-export const MSG_DESCRIPTION = s__('WebIDE|You are invited to experience the new Web IDE.');
-export const MSG_BUTTON_TEXT = s__('WebIDE|Switch to new Web IDE');
-export const MSG_LEARN_MORE = __('Learn more');
-export const MSG_TITLE = s__('WebIDE|Ready for something new?');
-
-export const MSG_CONFIRM = s__(
- 'WebIDE|Are you sure you want to switch editors? You will lose any unsaved changes.',
-);
-export const MSG_ERROR_ALERT = s__(
- 'WebIDE|Something went wrong while updating the user preferences. Please see developer console for details.',
-);
-
-export default {
- components: {
- GlButton,
- GlEmptyState,
- GlLink,
- },
- data() {
- return {
- loading: false,
- };
- },
- computed: {
- ...mapState(['switchEditorSvgPath', 'links', 'userPreferencesPath']),
- },
- methods: {
- async submitSwitch() {
- const confirmed = await confirmAction(MSG_CONFIRM, {
- primaryBtnText: __('Switch editors'),
- cancelBtnText: __('Cancel'),
- });
-
- if (!confirmed) {
- return;
- }
-
- try {
- await axios.put(this.userPreferencesPath, {
- user: { use_legacy_web_ide: false },
- });
- } catch (e) {
- // why: We do not want to translate console logs
- // eslint-disable-next-line @gitlab/require-i18n-strings
- logError('Error while updating user preferences', e);
- createAlert({
- message: MSG_ERROR_ALERT,
- });
- return;
- }
-
- eventHub.$emit('skip-beforeunload');
- window.location.reload();
- },
- // what: ignoreWhilePending prevents double confirmation boxes
- onSwitchClicked: ignoreWhilePending(async function onSwitchClicked() {
- this.loading = true;
-
- try {
- await this.submitSwitch();
- } finally {
- this.loading = false;
- }
- }),
- },
- MSG_TITLE,
- MSG_DESCRIPTION,
- MSG_BUTTON_TEXT,
- MSG_LEARN_MORE,
-};
-</script>
-
-<template>
- <div class="gl-h-full gl-display-flex gl-flex-direction-column gl-justify-content-center">
- <gl-empty-state :svg-path="switchEditorSvgPath" :svg-height="150" :title="$options.MSG_TITLE">
- <template #description>
- <span>{{ $options.MSG_DESCRIPTION }}</span>
- <gl-link :href="links.newWebIDEHelpPagePath">{{ $options.MSG_LEARN_MORE }}</gl-link
- >.
- </template>
- <template #actions>
- <gl-button
- category="primary"
- variant="confirm"
- :loading="loading"
- @click="onSwitchClicked"
- >{{ $options.MSG_BUTTON_TEXT }}</gl-button
- >
- </template>
- </gl-empty-state>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index 623ba719b28..fa93f6d42a5 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlLoadingIcon, GlButton, GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlButton, GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
@@ -8,7 +9,7 @@ export default {
GlAlert,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
isLoading: {
diff --git a/app/assets/javascripts/ide/constants.js b/app/assets/javascripts/ide/constants.js
index c8e737fa6f5..01ce5fa07ee 100644
--- a/app/assets/javascripts/ide/constants.js
+++ b/app/assets/javascripts/ide/constants.js
@@ -61,7 +61,6 @@ export const leftSidebarViews = {
};
export const rightSidebarViews = {
- switchEditors: { name: 'switch-editors', keepAlive: true },
pipelines: { name: 'pipelines-list', keepAlive: true },
jobsDetail: { name: 'jobs-detail', keepAlive: false },
mergeRequestInfo: { name: 'merge-request-info', keepAlive: true },
@@ -119,3 +118,5 @@ export const DEFAULT_BRANCH = 'main';
// Ping Usage Metrics Keys
export const PING_USAGE_PREVIEW_KEY = 'web_ide_clientside_preview';
export const PING_USAGE_PREVIEW_SUCCESS_KEY = 'web_ide_clientside_preview_success';
+
+export const GITLAB_WEB_IDE_FEEDBACK_ISSUE = 'https://gitlab.com/gitlab-org/gitlab/-/issues/377367';
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index dec282239d9..1347d92b3b7 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -8,7 +8,6 @@ import { parseBoolean } from '../lib/utils/common_utils';
import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
import ide from './components/ide.vue';
import { createRouter } from './ide_router';
-import { initGitlabWebIDE } from './init_gitlab_web_ide';
import { DEFAULT_THEME } from './lib/themes';
import { createStore } from './stores';
@@ -74,7 +73,6 @@ export const initLegacyWebIDE = (el, options = {}) => {
codesandboxBundlerUrl: el.dataset.codesandboxBundlerUrl,
environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
previewMarkdownPath: el.dataset.previewMarkdownPath,
- canUseNewWebIde: parseBoolean(el.dataset.canUseNewWebIde),
userPreferencesPath: el.dataset.userPreferencesPath,
});
},
@@ -96,7 +94,7 @@ export const initLegacyWebIDE = (el, options = {}) => {
*
* @param {Objects} options - Extra options for the IDE (Used by EE).
*/
-export function startIde(options) {
+export async function startIde(options) {
const ideElement = document.getElementById('ide');
if (!ideElement) {
@@ -106,6 +104,7 @@ export function startIde(options) {
const useNewWebIde = parseBoolean(ideElement.dataset.useNewWebIde);
if (useNewWebIde) {
+ const { initGitlabWebIDE } = await import('./init_gitlab_web_ide');
initGitlabWebIDE(ideElement);
} else {
resetServiceWorkersPublicPath();
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 140f2895a29..d3c64754e8a 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -1,29 +1,89 @@
-import { cleanTrailingSlash } from './stores/utils';
+import { start } from '@gitlab/web-ide';
+import { __ } from '~/locale';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
+import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
+import csrf from '~/lib/utils/csrf';
+import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
+import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
+import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
-export const initGitlabWebIDE = async (el) => {
- const { start } = await import('@gitlab/web-ide');
+const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
+ const remotePath = cleanLeadingSeparator(remotePathArg);
- const { gitlab_url: gitlabUrl } = window.gon;
- const baseUrl = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
+ const replacers = {
+ ':remote_host': encodeURIComponent(remoteHost),
+ ':remote_path': encodeURIComponent(remotePath).replaceAll('%2F', '/'),
+ };
- // what: Pull what we need from the element. We will replace it soon.
- const { cspNonce: nonce, branchName: ref, projectPath } = el.dataset;
+ // why: Use the function callback of "replace" so we replace both keys at once
+ return ideRemotePath.replace(/(:remote_host|:remote_path)/g, (key) => {
+ return replacers[key];
+ });
+};
+
+const getMRTargetProject = () => {
+ const url = new URL(window.location.href);
+
+ return url.searchParams.get('target_project') || '';
+};
- // what: Clean up the element, but preserve id.
- // why: This way we don't inherit any `ide-loading` side-effects. This
- // mirrors the behavior of Vue when it mounts to an element.
- const newEl = document.createElement(el.tagName);
- newEl.id = el.id;
- newEl.classList.add('gl--flex-center', 'gl-relative', 'gl-h-full');
+export const initGitlabWebIDE = async (el) => {
+ // what: Pull what we need from the element. We will replace it soon.
+ const {
+ cspNonce: nonce,
+ branchName: ref,
+ projectPath,
+ ideRemotePath,
+ filePath,
+ mergeRequest: mrId,
+ forkInfo: forkInfoJSON,
+ } = el.dataset;
- el.replaceWith(newEl);
+ const rootEl = setupRootElement(el);
+ const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
- // what: Trigger start on our new mounting element
- await start(newEl, {
- baseUrl: cleanTrailingSlash(baseUrl.href),
+ // See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
+ start(rootEl, {
+ ...getBaseConfig(),
+ nonce,
+ // Use same headers as defined in axios_utils
+ httpHeaders: {
+ [csrf.headerKey]: csrf.token,
+ 'X-Requested-With': 'XMLHttpRequest',
+ },
projectPath,
- gitlabUrl,
ref,
- nonce,
+ 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,
+ links: {
+ feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
+ userPreferences: el.dataset.userPreferencesPath,
+ },
+ async handleStartRemote({ remoteHost, remotePath, connectionToken }) {
+ const confirmed = await confirmAction(
+ __('Are you sure you want to leave the Web IDE? All unsaved changes will be lost.'),
+ {
+ primaryBtnText: __('Start remote connection'),
+ cancelBtnText: __('Continue editing'),
+ },
+ );
+
+ if (!confirmed) {
+ return;
+ }
+
+ createAndSubmitForm({
+ url: buildRemoteIdeURL(ideRemotePath, remoteHost, remotePath),
+ data: {
+ connection_token: connectionToken,
+ return_url: window.location.href,
+ },
+ });
+ },
});
};
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 682914df9ec..7595a1cedf1 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -2,7 +2,7 @@ import { throttle } from 'lodash';
import { Range } from 'monaco-editor';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import Disposable from '../common/disposable';
-import DirtyDiffWorker from './diff_worker';
+import DirtyDiffWorker from './diff_worker?worker';
export const getDiffChangeType = (change) => {
if (change.modified) {
diff --git a/app/assets/javascripts/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 525afcb2083..289027c3054 100644
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
@@ -1,3 +1,12 @@
+import { useNewFonts } from '~/lib/utils/common_utils';
+import { getCssVariable } from '~/lib/utils/css_utils';
+
+const fontOptions = {};
+
+if (useNewFonts()) {
+ fontOptions.fontFamily = getCssVariable('--code-editor-font');
+}
+
export const defaultEditorOptions = {
model: null,
readOnly: false,
@@ -9,6 +18,7 @@ export const defaultEditorOptions = {
wordWrap: 'on',
glyphMargin: true,
automaticLayout: true,
+ ...fontOptions,
};
export const defaultDiffOptions = {
@@ -27,7 +37,6 @@ export const defaultDiffEditorOptions = {
};
export const defaultModelOptions = {
- endOfLine: 0,
insertFinalNewline: true,
trimTrailingWhitespace: false,
};
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
new file mode 100644
index 00000000000..fbd2ce4ce69
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_base_config.js
@@ -0,0 +1,12 @@
+import { cleanEndingSeparator } from '~/lib/utils/url_utility';
+
+const getBaseUrl = () => {
+ const baseUrlObj = new URL(process.env.GITLAB_WEB_IDE_PUBLIC_PATH, window.location.origin);
+
+ return cleanEndingSeparator(baseUrlObj.href);
+};
+
+export const getBaseConfig = () => ({
+ baseUrl: getBaseUrl(),
+ gitlabUrl: window.gon.gitlab_url,
+});
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
new file mode 100644
index 00000000000..8311e11672e
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
@@ -0,0 +1,2 @@
+export * from './get_base_config';
+export * from './setup_root_element';
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js
new file mode 100644
index 00000000000..b0e06c88d26
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/setup_root_element.js
@@ -0,0 +1,14 @@
+/**
+ * Cleans up the given element and prepares it for mounting to `@gitlab/web-ide`
+ *
+ * @param {Element} root The original root element
+ * @returns {Element} A new element ready to be used by `@gitlab/web-ide`
+ */
+export const setupRootElement = (el) => {
+ const newEl = document.createElement(el.tagName);
+ newEl.id = el.id;
+ newEl.classList.add('gl--flex-center', 'gl-relative', 'gl-h-full');
+ el.replaceWith(newEl);
+
+ return newEl;
+};
diff --git a/app/assets/javascripts/ide/remote/index.js b/app/assets/javascripts/ide/remote/index.js
new file mode 100644
index 00000000000..fb8db20c0c1
--- /dev/null
+++ b/app/assets/javascripts/ide/remote/index.js
@@ -0,0 +1,40 @@
+import { startRemote } from '@gitlab/web-ide';
+import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
+import { isSameOriginUrl, joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * @param {Element} rootEl
+ */
+export const mountRemoteIDE = async (el) => {
+ const {
+ remoteHost: remoteAuthority,
+ remotePath: hostPath,
+ cspNonce,
+ connectionToken,
+ returnUrl,
+ } = el.dataset;
+
+ const rootEl = setupRootElement(el);
+
+ const visitReturnUrl = () => {
+ // security: Only change `href` if of the same origin as current page
+ if (returnUrl && isSameOriginUrl(returnUrl)) {
+ window.location.href = returnUrl;
+ } else {
+ window.location.reload();
+ }
+ };
+
+ startRemote(rootEl, {
+ ...getBaseConfig(),
+ nonce: cspNonce,
+ connectionToken,
+ // remoteAuthority must start with "/"
+ remoteAuthority: joinPaths('/', remoteAuthority),
+ // hostPath must start with "/"
+ hostPath: joinPaths('/', hostPath),
+ // TODO Handle error better
+ handleError: visitReturnUrl,
+ handleClose: visitReturnUrl,
+ });
+};
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 805476c71bc..1f9bc834140 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -4,7 +4,7 @@ import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/pipeline_editor/graphql/queries/ci_config.query.graphql';
+import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { query, mutate } from './gql';
export default {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
index 91868132a5a..a510ec0847b 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/actions/session_controls.js
@@ -1,6 +1,6 @@
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import * as terminalService from '../../../../services/terminals';
import { STARTING, STOPPING, STOPPED } from '../constants';
import * as messages from '../messages';
@@ -108,7 +108,7 @@ export const restartSession = ({ state, dispatch, rootState }) => {
// We may have removed the build, in this case we'll just create a new session
if (
responseStatus === httpStatus.NOT_FOUND ||
- responseStatus === httpStatus.UNPROCESSABLE_ENTITY
+ responseStatus === HTTP_STATUS_UNPROCESSABLE_ENTITY
) {
dispatch('startSession');
} else {
diff --git a/app/assets/javascripts/ide/stores/modules/terminal/messages.js b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
index ec05ca84754..fa1c7f23677 100644
--- a/app/assets/javascripts/ide/stores/modules/terminal/messages.js
+++ b/app/assets/javascripts/ide/stores/modules/terminal/messages.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import httpStatus from '~/lib/utils/http_status';
+import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import { __, sprintf } from '~/locale';
export const UNEXPECTED_ERROR_CONFIG = __(
@@ -28,7 +28,7 @@ export const ERROR_PERMISSION = __(
);
export const configCheckError = (status, helpUrl) => {
- if (status === httpStatus.UNPROCESSABLE_ENTITY) {
+ if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY) {
return sprintf(
ERROR_CONFIG,
{
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 70efda970bf..b89d9d38a1a 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -34,5 +34,4 @@ export default () => ({
environmentsGuidanceAlertDetected: false,
previewMarkdownPath: '',
userPreferencesPath: '',
- canUseNewWebIde: false,
});
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 25d4037bbe5..f351a9a392f 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -1,5 +1,21 @@
<script>
import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import { s__ } from '~/locale';
+import { createAlert } from '~/flash';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+const reportNamespaceLoadError = debounce(
+ () =>
+ createAlert({
+ message: s__('ImportProjects|Requesting namespaces failed'),
+ }),
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+);
export default {
components: {
@@ -7,18 +23,32 @@ export default {
GlSearchBoxByType,
},
inheritAttrs: false,
- props: {
- namespaces: {
- type: Array,
- required: true,
- },
- },
data() {
return { searchTerm: '' };
},
+ apollo: {
+ namespaces: {
+ query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ variables() {
+ return {
+ search: this.searchTerm,
+ };
+ },
+ skip() {
+ const hasNotEnoughSearchCharacters =
+ this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
+ return hasNotEnoughSearchCharacters;
+ },
+ update(data) {
+ return data.currentUser.groups.nodes;
+ },
+ error: reportNamespaceLoadError,
+ debounce: DEBOUNCE_DELAY,
+ },
+ },
computed: {
filteredNamespaces() {
- return this.namespaces.filter((ns) =>
+ return (this.namespaces ?? []).filter((ns) =>
ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
);
},
diff --git a/app/assets/javascripts/import_entities/components/import_status.vue b/app/assets/javascripts/import_entities/components/import_status.vue
index 5455a034106..bd69165f0ca 100644
--- a/app/assets/javascripts/import_entities/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/components/import_status.vue
@@ -49,7 +49,7 @@ const STATUS_MAP = {
text: __('Timeout'),
variant: 'danger',
},
- [STATUSES.CANCELLED]: {
+ [STATUSES.CANCELED]: {
icon: 'status-stopped',
text: __('Cancelled'),
variant: 'neutral',
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index c470da21765..48b7febca4b 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -9,6 +9,10 @@ export const STATUSES = {
STARTED: 'started',
NONE: 'none',
SCHEDULING: 'scheduling',
- CANCELLED: 'cancelled',
+ CANCELED: 'canceled',
TIMEOUT: 'timeout',
};
+
+export const PROVIDERS = {
+ GITHUB: 'github',
+};
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 66dff77eef8..6412f26fde7 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -21,12 +21,13 @@ import { getGroupPathAvailability } from '~/rest_api';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import searchNamespacesWhereUserCanCreateProjectsQuery from '~/projects/new/queries/search_namespaces_where_user_can_create_projects.query.graphql';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUSES } from '../../constants';
import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
-import availableNamespacesQuery from '../graphql/queries/available_namespaces.query.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants';
import { StatusPoller } from '../services/status_poller';
@@ -107,7 +108,12 @@ export default {
return { page: this.page, filter: this.filter, perPage: this.perPage };
},
},
- availableNamespaces: availableNamespacesQuery,
+ availableNamespaces: {
+ query: searchNamespacesWhereUserCanCreateProjectsQuery,
+ update(data) {
+ return data.currentUser.groups.nodes;
+ },
+ },
},
fields: [
@@ -158,7 +164,7 @@ export default {
}
return this.groups.map((group) => {
- const importTarget = this.getImportTarget(group);
+ const importTarget = this.importTargets[group.id];
const status = this.getStatus(group);
const flags = {
@@ -250,10 +256,14 @@ export default {
this.page = 1;
},
- groupsTableData() {
+ groups() {
const table = this.getTableRef();
const matches = new Set();
- this.groupsTableData.forEach((g, idx) => {
+ this.groups.forEach((g, idx) => {
+ if (!this.importGroups[g.id]) {
+ this.setDefaultImportTarget(g);
+ }
+
if (this.selectedGroupsIds.includes(g.id)) {
matches.add(g.id);
this.$nextTick(() => {
@@ -421,7 +431,7 @@ export default {
data: { exists },
} = await getGroupPathAvailability(
importTarget.newName,
- importTarget.targetNamespace.id,
+ getIdFromGraphQLId(importTarget.targetNamespace.id),
{
cancelToken: importTarget.cancellationToken?.token,
},
@@ -444,11 +454,7 @@ export default {
importTarget.validationErrors = newValidationErrors;
}, VALIDATION_DEBOUNCE_TIME),
- getImportTarget(group) {
- if (this.importTargets[group.id]) {
- return this.importTargets[group.id];
- }
-
+ setDefaultImportTarget(group) {
// If we've reached this Vue application we have at least one potential import destination
const defaultTargetNamespace =
// first option: namespace id was explicitly provided
@@ -482,9 +488,13 @@ export default {
validationErrors: [],
});
- getGroupPathAvailability(importTarget.newName, importTarget.targetNamespace.id, {
- cancelToken: cancellationToken.token,
- })
+ getGroupPathAvailability(
+ importTarget.newName,
+ getIdFromGraphQLId(importTarget.targetNamespace.id),
+ {
+ cancelToken: cancellationToken.token,
+ },
+ )
.then(({ data: { exists, suggests: suggestions } }) => {
if (!exists) return;
@@ -505,7 +515,6 @@ export default {
.catch(() => {
// empty catch intended
});
- return this.importTargets[group.id];
},
},
@@ -692,7 +701,6 @@ export default {
<template #cell(importTarget)="{ item: group }">
<import-target-cell
:group="group"
- :available-namespaces="availableNamespaces"
:group-path-regex="groupPathRegex"
@update-target-namespace="updateImportTarget(group, { targetNamespace: $event })"
@update-new-name="updateImportTarget(group, { newName: $event })"
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
index 4fbbd5b239c..04a90d9c20c 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue
@@ -22,10 +22,6 @@ export default {
type: Object,
required: true,
},
- availableNamespaces: {
- type: Array,
- required: true,
- },
},
computed: {
@@ -53,7 +49,6 @@ export default {
#default="{ namespaces }"
:text="fullPath"
:disabled="!group.flags.isAvailableForImport"
- :namespaces="availableNamespaces"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index 36da996ea17..913a5a659b3 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -10,7 +10,6 @@ import typeDefs from './typedefs.graphql';
export const clientTypenames = {
BulkImportSourceGroupConnection: 'ClientBulkImportSourceGroupConnection',
BulkImportSourceGroup: 'ClientBulkImportSourceGroup',
- AvailableNamespace: 'ClientAvailableNamespace',
BulkImportPageInfo: 'ClientBulkImportPageInfo',
BulkImportTarget: 'ClientBulkImportTarget',
BulkImportProgress: 'ClientBulkImportProgress',
@@ -110,15 +109,6 @@ export function createResolvers({ endpoints }) {
};
return response;
},
-
- availableNamespaces: () =>
- axios.get(endpoints.availableNamespaces).then(({ data }) =>
- data.map((namespace) => ({
- __typename: clientTypenames.AvailableNamespace,
- id: namespace.id,
- fullPath: namespace.full_path,
- })),
- ),
},
Mutation: {
async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
deleted file mode 100644
index b0741dfbe5c..00000000000
--- a/app/assets/javascripts/import_entities/import_groups/graphql/queries/available_namespaces.query.graphql
+++ /dev/null
@@ -1,6 +0,0 @@
-query availableNamespaces {
- availableNamespaces @client {
- id
- fullPath
- }
-}
diff --git a/app/assets/javascripts/import_entities/import_groups/index.js b/app/assets/javascripts/import_entities/import_groups/index.js
index 5d7e7911f5a..494a845b1f9 100644
--- a/app/assets/javascripts/import_entities/import_groups/index.js
+++ b/app/assets/javascripts/import_entities/import_groups/index.js
@@ -12,7 +12,6 @@ export function mountImportGroupsApp(mountElement) {
const {
statusPath,
- availableNamespacesPath,
createBulkImportPath,
jobsPath,
historyPath,
@@ -25,7 +24,6 @@ export function mountImportGroupsApp(mountElement) {
sourceUrl,
endpoints: {
status: statusPath,
- availableNamespaces: availableNamespacesPath,
createBulkImport: createBulkImportPath,
},
}),
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index 97a7ed4bf55..63a36f1a79f 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -37,6 +37,11 @@ export default {
required: false,
default: false,
},
+ cancelable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
optionalStages: {
type: Array,
required: false,
@@ -58,9 +63,8 @@ export default {
},
computed: {
- ...mapState(['filter', 'repositories', 'namespaces', 'defaultTargetNamespace', 'pageInfo']),
+ ...mapState(['filter', 'repositories', 'defaultTargetNamespace', 'pageInfo', 'isLoadingRepos']),
...mapGetters([
- 'isLoading',
'isImportingAnyRepo',
'importingRepoCount',
'hasImportableRepos',
@@ -98,7 +102,6 @@ export default {
},
mounted() {
- this.fetchNamespaces();
this.fetchJobs();
if (!this.paginatable) {
@@ -115,7 +118,6 @@ export default {
...mapActions([
'fetchRepos',
'fetchJobs',
- 'fetchNamespaces',
'stopJobsPolling',
'clearJobsEtagPoll',
'setFilter',
@@ -196,22 +198,22 @@ export default {
<provider-repo-table-row
:key="repo.importSource.providerLink"
:repo="repo"
- :available-namespaces="namespaces"
:user-namespace="defaultTargetNamespace"
:optional-stages="optionalStagesSelection"
+ :cancelable="cancelable"
/>
</template>
</tbody>
</table>
</div>
<gl-intersection-observer
- v-if="paginatable"
+ v-if="paginatable && pageInfo.hasNextPage"
:key="pagePaginationStateKey"
@appear="fetchRepos"
/>
- <gl-loading-icon v-if="isLoading" class="gl-mt-7" size="lg" />
+ <gl-loading-icon v-if="isLoadingRepos" class="gl-mt-7" size="lg" />
- <div v-if="!isLoading && repositories.length === 0" class="gl-text-center">
+ <div v-if="!isLoadingRepos && repositories.length === 0" class="gl-text-center">
<strong>{{ emptyStateText }}</strong>
</div>
</div>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index 458e0fb1cb1..b8faf349375 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -8,13 +8,15 @@ import {
GlDropdownItem,
GlDropdownDivider,
GlDropdownSectionHeader,
+ GlTooltip,
} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
-import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
+import { isProjectImportable, isImporting, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
@@ -29,6 +31,7 @@ export default {
GlIcon,
GlBadge,
GlLink,
+ GlTooltip,
},
props: {
repo: {
@@ -39,14 +42,15 @@ export default {
type: String,
required: true,
},
- availableNamespaces: {
- type: Array,
- required: true,
- },
optionalStages: {
type: Object,
required: true,
},
+ cancelable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -73,6 +77,14 @@ export default {
return getImportStatus(this.repo);
},
+ isImporting() {
+ return isImporting(this.repo);
+ },
+
+ isCancelable() {
+ return this.cancelable && this.isImporting && this.importStatus !== STATUSES.SCHEDULING;
+ },
+
stats() {
return this.repo.importedProject?.stats;
},
@@ -96,7 +108,7 @@ export default {
},
methods: {
- ...mapActions(['fetchImport', 'setImportTarget']),
+ ...mapActions(['fetchImport', 'cancelImport', 'setImportTarget']),
updateImportTarget(changedValues) {
this.setImportTarget({
repoId: this.repo.importSource.id,
@@ -104,6 +116,8 @@ export default {
});
},
},
+
+ helpUrl: helpPagePath('/user/project/import/github.md'),
};
</script>
@@ -127,11 +141,7 @@ export default {
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted">
<div class="import-entities-target-select gl-display-flex gl-align-items-stretch gl-w-full">
- <import-group-dropdown
- #default="{ namespaces }"
- :text="importTarget.targetNamespace"
- :namespaces="availableNamespaces"
- >
+ <import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
<template v-if="namespaces.length">
<gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
<gl-dropdown-item
@@ -168,6 +178,26 @@ export default {
<import-status :status="importStatus" :stats="stats" />
</td>
<td data-testid="actions" class="gl-vertical-align-top gl-pt-4">
+ <gl-tooltip :target="() => $refs.cancelButton.$el">
+ <div class="gl-text-left">
+ <p class="gl-mb-5 gl-font-weight-bold">{{ s__('ImportProjects|Cancel import') }}</p>
+ {{
+ s__(
+ 'ImportProjects|Imported files will be kept. You can import this repository again later.',
+ )
+ }}
+ <gl-link :href="$options.helpUrl" target="_blank">{{ __('Learn more.') }}</gl-link>
+ </div>
+ </gl-tooltip>
+ <gl-button
+ v-show="isCancelable"
+ ref="cancelButton"
+ variant="danger"
+ category="secondary"
+ icon="cancel"
+ :aria-label="__('Cancel')"
+ @click="cancelImport({ repoId: repo.importSource.id })"
+ />
<gl-button
v-if="isFinished"
class="btn btn-default"
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index df26d6ac4f6..197fb03af2c 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -1,10 +1,14 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { parseBoolean } from '~/lib/utils/common_utils';
import Translate from '~/vue_shared/translate';
+import createDefaultClient from '~/lib/graphql';
import ImportProjectsTable from './components/import_projects_table.vue';
+
import createStore from './store';
Vue.use(Translate);
+Vue.use(VueApollo);
export function initStoreFromElement(element) {
const {
@@ -15,7 +19,7 @@ export function initStoreFromElement(element) {
reposPath,
jobsPath,
importPath,
- namespacesPath,
+ cancelPath,
defaultTargetNamespace,
paginatable,
} = element.dataset;
@@ -31,7 +35,7 @@ export function initStoreFromElement(element) {
reposPath,
jobsPath,
importPath,
- namespacesPath,
+ cancelPath,
},
hasPagination: parseBoolean(paginatable),
});
@@ -43,9 +47,16 @@ export function initPropsFromElement(element) {
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
optionalStages: JSON.parse(element.dataset.optionalStages),
+ cancelable: Boolean(element.dataset.cancelPath),
};
}
+const defaultClient = createDefaultClient();
+
+const apolloProvider = new VueApollo({
+ defaultClient,
+});
+
export default function mountImportProjectsTable(mountElement) {
if (!mountElement) return undefined;
@@ -55,6 +66,7 @@ export default function mountImportProjectsTable(mountElement) {
return new Vue({
el: mountElement,
store,
+ apolloProvider,
render(createElement) {
return createElement(ImportProjectsTable, { props });
},
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index a30c14f9d28..e0db585eb3e 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,20 +1,22 @@
import Visibility from 'visibilityjs';
+import _ from 'lodash';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { isProjectImportable } from '../utils';
+import { PROVIDERS } from '../../constants';
import * as types from './mutation_types';
let eTagPoll;
const hasRedirectInError = (e) => e?.response?.data?.error?.redirect;
const redirectToUrlInError = (e) => visitUrl(e.response.data.error.redirect);
-const tooManyRequests = (e) => e.response.status === httpStatusCodes.TOO_MANY_REQUESTS;
+const tooManyRequests = (e) => e.response.status === HTTP_STATUS_TOO_MANY_REQUESTS;
const pathWithParams = ({ path, ...params }) => {
const filteredParams = Object.fromEntries(
Object.entries(params).filter(([, value]) => value !== ''),
@@ -22,6 +24,24 @@ const pathWithParams = ({ path, ...params }) => {
const queryString = objectToQuery(filteredParams);
return queryString ? `${path}?${queryString}` : path;
};
+const commitPaginationData = ({ state, commit, data }) => {
+ const cursorsGitHubResponse = !_.isEmpty(data.pageInfo || {});
+
+ if (state.provider === PROVIDERS.GITHUB && cursorsGitHubResponse) {
+ commit(types.SET_PAGE_CURSORS, data.pageInfo);
+ } else {
+ const nextPage = state.pageInfo.page + 1;
+ commit(types.SET_PAGE, nextPage);
+ }
+};
+const paginationParams = ({ state }) => {
+ if (state.provider === PROVIDERS.GITHUB && state.pageInfo.endCursor) {
+ return { after: state.pageInfo.endCursor };
+ }
+
+ const nextPage = state.pageInfo.page + 1;
+ return { page: nextPage === 1 ? '' : nextPage.toString() };
+};
const isRequired = () => {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -55,7 +75,6 @@ const importAll = ({ state, dispatch }, config = {}) => {
};
const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit }) => {
- const nextPage = state.pageInfo.page + 1;
commit(types.REQUEST_REPOS);
const { provider, filter } = state;
@@ -65,12 +84,13 @@ const fetchReposFactory = ({ reposPath = isRequired() }) => ({ state, commit })
pathWithParams({
path: reposPath,
filter: filter ?? '',
- page: nextPage === 1 ? '' : nextPage.toString(),
+ ...paginationParams({ state }),
}),
)
.then(({ data }) => {
- commit(types.SET_PAGE, nextPage);
- commit(types.RECEIVE_REPOS_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true }));
+ const camelData = convertObjectPropsToCamelCase(data, { deep: true });
+ commitPaginationData({ state, commit, data: camelData });
+ commit(types.RECEIVE_REPOS_SUCCESS, camelData);
})
.catch((e) => {
if (hasRedirectInError(e)) {
@@ -139,6 +159,42 @@ const fetchImportFactory = (importPath = isRequired()) => (
});
};
+export const cancelImportFactory = (cancelImportPath) => ({ state, commit }, { repoId }) => {
+ const existingRepo = state.repositories.find((r) => r.importSource.id === repoId);
+
+ if (!existingRepo?.importedProject) {
+ throw new Error(`Attempting to cancel project which is not started: ${repoId}`);
+ }
+
+ const { id } = existingRepo.importedProject;
+
+ return axios
+ .post(cancelImportPath, {
+ project_id: id,
+ })
+ .then(() => {
+ commit(types.CANCEL_IMPORT_SUCCESS, {
+ repoId,
+ });
+ })
+ .catch((e) => {
+ const serverErrorMessage = e?.response?.data?.errors;
+ const flashMessage = serverErrorMessage
+ ? sprintf(
+ s__('ImportProjects|Cancelling project import failed: %{reason}'),
+ {
+ reason: serverErrorMessage,
+ },
+ false,
+ )
+ : s__('ImportProjects|Cancelling project import failed');
+
+ createAlert({
+ message: flashMessage,
+ });
+ });
+};
+
export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, dispatch }) => {
if (eTagPoll) {
stopJobsPolling();
@@ -176,22 +232,6 @@ export const fetchJobsFactory = (jobsPath = isRequired()) => ({ state, commit, d
});
};
-const fetchNamespacesFactory = (namespacesPath = isRequired()) => ({ commit }) => {
- commit(types.REQUEST_NAMESPACES);
- axios
- .get(namespacesPath)
- .then(({ data }) =>
- commit(types.RECEIVE_NAMESPACES_SUCCESS, convertObjectPropsToCamelCase(data, { deep: true })),
- )
- .catch(() => {
- createAlert({
- message: s__('ImportProjects|Requesting namespaces failed'),
- });
-
- commit(types.RECEIVE_NAMESPACES_ERROR);
- });
-};
-
const setFilter = ({ commit, dispatch }, filter) => {
commit(types.SET_FILTER, filter);
@@ -207,6 +247,6 @@ export default ({ endpoints = isRequired() }) => ({
importAll,
fetchRepos: fetchReposFactory({ reposPath: endpoints.reposPath }),
fetchImport: fetchImportFactory(endpoints.importPath),
+ cancelImport: cancelImportFactory(endpoints.cancelPath),
fetchJobs: fetchJobsFactory(endpoints.jobsPath),
- fetchNamespaces: fetchNamespacesFactory(endpoints.namespacesPath),
});
diff --git a/app/assets/javascripts/import_entities/import_projects/store/getters.js b/app/assets/javascripts/import_entities/import_projects/store/getters.js
index ef01a67ec94..31ddffd4eb4 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/getters.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/getters.js
@@ -1,7 +1,5 @@
import { isProjectImportable, isIncompatible, isImporting } from '../utils';
-export const isLoading = (state) => state.isLoadingRepos || state.isLoadingNamespaces;
-
export const importingRepoCount = (state) => state.repositories.filter(isImporting).length;
export const isImportingAnyRepo = (state) => state.repositories.some(isImporting);
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
index 6adf5e59cff..74832a03ac1 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
@@ -2,14 +2,12 @@ export const REQUEST_REPOS = 'REQUEST_REPOS';
export const RECEIVE_REPOS_SUCCESS = 'RECEIVE_REPOS_SUCCESS';
export const RECEIVE_REPOS_ERROR = 'RECEIVE_REPOS_ERROR';
-export const REQUEST_NAMESPACES = 'REQUEST_NAMESPACES';
-export const RECEIVE_NAMESPACES_SUCCESS = 'RECEIVE_NAMESPACES_SUCCESS';
-export const RECEIVE_NAMESPACES_ERROR = 'RECEIVE_NAMESPACES_ERROR';
-
export const REQUEST_IMPORT = 'REQUEST_IMPORT';
export const RECEIVE_IMPORT_SUCCESS = 'RECEIVE_IMPORT_SUCCESS';
export const RECEIVE_IMPORT_ERROR = 'RECEIVE_IMPORT_ERROR';
+export const CANCEL_IMPORT_SUCCESS = 'CANCEL_IMPORT_SUCCESS';
+
export const RECEIVE_JOBS_SUCCESS = 'RECEIVE_JOBS_SUCCESS';
export const SET_FILTER = 'SET_FILTER';
@@ -18,4 +16,4 @@ export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
export const SET_PAGE = 'SET_PAGE';
-export const SET_PAGE_INFO = 'SET_PAGE_INFO';
+export const SET_PAGE_CURSORS = 'SET_PAGE_CURSORS';
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index 163a19976de..8b2e0364d7a 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -36,7 +36,12 @@ export default {
[types.SET_FILTER](state, filter) {
state.filter = filter;
state.repositories = [];
- state.pageInfo.page = 0;
+ state.pageInfo = {
+ page: 0,
+ startCursor: null,
+ endCursor: null,
+ hasNextPage: true,
+ };
},
[types.REQUEST_REPOS](state) {
@@ -51,7 +56,9 @@ export default {
// https://gitlab.com/gitlab-org/gitlab/-/issues/27370#note_379034091
const newImportedProjects = processLegacyEntries({
- newRepositories: repositories.importedProjects,
+ newRepositories: repositories.importedProjects.filter(
+ (p) => p.importStatus !== STATUSES.CANCELED,
+ ),
existingRepositories: state.repositories,
factory: makeNewImportedProject,
});
@@ -122,17 +129,9 @@ export default {
});
},
- [types.REQUEST_NAMESPACES](state) {
- state.isLoadingNamespaces = true;
- },
-
- [types.RECEIVE_NAMESPACES_SUCCESS](state, namespaces) {
- state.isLoadingNamespaces = false;
- state.namespaces = namespaces;
- },
-
- [types.RECEIVE_NAMESPACES_ERROR](state) {
- state.isLoadingNamespaces = false;
+ [types.CANCEL_IMPORT_SUCCESS](state, { repoId }) {
+ const existingRepo = state.repositories.find((r) => r.importSource.id === repoId);
+ existingRepo.importedProject.importStatus = STATUSES.CANCELED;
},
[types.SET_IMPORT_TARGET](state, { repoId, importTarget }) {
@@ -151,4 +150,9 @@ export default {
[types.SET_PAGE](state, page) {
state.pageInfo.page = page;
},
+
+ [types.SET_PAGE_CURSORS](state, pageInfo) {
+ const { startCursor, endCursor, hasNextPage } = pageInfo;
+ state.pageInfo = { ...state.pageInfo, startCursor, endCursor, hasNextPage };
+ },
};
diff --git a/app/assets/javascripts/import_entities/import_projects/store/state.js b/app/assets/javascripts/import_entities/import_projects/store/state.js
index ecd93561d52..c384848f0a0 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/state.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/state.js
@@ -1,13 +1,14 @@
export default () => ({
provider: '',
repositories: [],
- namespaces: [],
customImportTargets: {},
isLoadingRepos: false,
- isLoadingNamespaces: false,
ciCdOnly: false,
filter: '',
pageInfo: {
page: 0,
+ startCursor: null,
+ endCursor: null,
+ hasNextPage: true,
},
});
diff --git a/app/assets/javascripts/import_entities/import_projects/utils.js b/app/assets/javascripts/import_entities/import_projects/utils.js
index 38bd529321a..c4c9e544c1e 100644
--- a/app/assets/javascripts/import_entities/import_projects/utils.js
+++ b/app/assets/javascripts/import_entities/import_projects/utils.js
@@ -9,7 +9,10 @@ export function getImportStatus(project) {
}
export function isProjectImportable(project) {
- return !isIncompatible(project) && getImportStatus(project) === STATUSES.NONE;
+ return (
+ !isIncompatible(project) &&
+ [STATUSES.NONE, STATUSES.CANCELED].includes(getImportStatus(project))
+ );
}
export function isImporting(repo) {
diff --git a/app/assets/javascripts/incidents/components/incidents_list.vue b/app/assets/javascripts/incidents/components/incidents_list.vue
index dbd2225167a..14ab7b2dc1e 100644
--- a/app/assets/javascripts/incidents/components/incidents_list.vue
+++ b/app/assets/javascripts/incidents/components/incidents_list.vue
@@ -14,7 +14,7 @@ import {
import { isValidSlaDueAt } from 'ee_else_ce/vue_shared/components/incidents/utils';
import { visitUrl, mergeUrlParams, joinPaths } from '~/lib/utils/url_utility';
import { s__, n__ } from '~/locale';
-import { INCIDENT_SEVERITY } from '~/sidebar/components/severity/constants';
+import { INCIDENT_SEVERITY } from '~/sidebar/constants';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
import Tracking from '~/tracking';
import {
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
index 93baa54956a..d3850114350 100644
--- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { ERROR_MSG } from './constants';
@@ -22,7 +22,7 @@ export default class IncidentsSettingsService {
.catch(({ response }) => {
const message = response?.data?.message || '';
- createFlash({
+ createAlert({
message: `${ERROR_MSG} ${message}`,
});
});
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index fe687ea9767..904e5639cac 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -1,14 +1,8 @@
<script>
-import {
- GlFormGroup,
- GlFormCheckbox,
- GlFormInput,
- GlFormSelect,
- GlFormTextarea,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
import { capitalize, lowerCase, isEmpty } from 'lodash';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'DynamicField',
@@ -80,7 +74,7 @@ export default {
};
},
computed: {
- ...mapGetters(['isInheriting']),
+ ...mapGetters(['isInheriting', 'propsSource']),
isCheckbox() {
return this.type === 'checkbox';
},
@@ -122,11 +116,18 @@ export default {
name: this.fieldName,
state: this.valid,
readonly: this.isInheriting,
+ disabled: this.isDisabled,
};
},
valid() {
return !this.required || !isEmpty(this.model) || this.isNonEmptyPassword || !this.isValidated;
},
+ isInheritingOrDisabled() {
+ return this.isInheriting || this.isDisabled;
+ },
+ isDisabled() {
+ return !this.propsSource.editable;
+ },
},
created() {
if (this.isNonEmptyPassword) {
@@ -149,7 +150,7 @@ export default {
<template v-if="isCheckbox">
<input :name="fieldName" type="hidden" :value="model || false" />
- <gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
+ <gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheritingOrDisabled">
{{ checkboxLabel || humanizedTitle }}
<template #help>
<span v-safe-html="help"></span>
@@ -158,7 +159,12 @@ export default {
</template>
<template v-else-if="isSelect">
<input type="hidden" :name="fieldName" :value="model" />
- <gl-form-select :id="fieldId" v-model="model" :options="options" :disabled="isInheriting" />
+ <gl-form-select
+ :id="fieldId"
+ v-model="model"
+ :options="options"
+ :disabled="isInheritingOrDisabled"
+ />
</template>
<gl-form-textarea
v-else-if="isTextarea"
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 4bf2b8d4468..d86e6326f64 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,22 +1,15 @@
<script>
-import {
- GlAlert,
- GlBadge,
- GlButton,
- GlModalDirective,
- GlSafeHtmlDirective as SafeHtml,
- GlForm,
-} from '@gitlab/ui';
+import { GlAlert, GlBadge, GlButton, GlForm } from '@gitlab/ui';
import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
- integrationLevels,
integrationFormSectionComponents,
billingPlanNames,
} from '~/integrations/constants';
@@ -25,11 +18,10 @@ import csrf from '~/lib/utils/csrf';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
-import ConfirmationModal from './confirmation_modal.vue';
import DynamicField from './dynamic_field.vue';
import OverrideDropdown from './override_dropdown.vue';
-import ResetConfirmationModal from './reset_confirmation_modal.vue';
import TriggerFields from './trigger_fields.vue';
+import IntegrationFormActions from './integration_form_actions.vue';
export default {
name: 'IntegrationForm',
@@ -38,8 +30,7 @@ export default {
ActiveCheckbox,
TriggerFields,
DynamicField,
- ConfirmationModal,
- ResetConfirmationModal,
+ IntegrationFormActions,
IntegrationSectionConfiguration: () =>
import(
/* webpackChunkName: 'integrationSectionConfiguration' */ '~/integrations/edit/components/sections/configuration.vue'
@@ -66,7 +57,6 @@ export default {
GlForm,
},
directives: {
- GlModal: GlModalDirective,
SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
@@ -78,10 +68,10 @@ export default {
data() {
return {
integrationActive: false,
- isTesting: false,
+ isValidated: false,
isSaving: false,
+ isTesting: false,
isResetting: false,
- isValidated: false,
};
},
computed: {
@@ -90,21 +80,6 @@ export default {
isEditable() {
return this.propsSource.editable;
},
- isInstanceOrGroupLevel() {
- return (
- this.customState.integrationLevel === integrationLevels.INSTANCE ||
- this.customState.integrationLevel === integrationLevels.GROUP
- );
- },
- showResetButton() {
- return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
- },
- showTestButton() {
- return this.propsSource.canTest;
- },
- disableButtons() {
- return Boolean(this.isSaving || this.isResetting || this.isTesting);
- },
hasSections() {
if (this.hasSlackNotificationsDisabled) {
return false;
@@ -134,6 +109,14 @@ export default {
}
return !this.hasSections && this.helpHtml;
},
+ shouldUpgradeSlack() {
+ return (
+ this.isSlackIntegration &&
+ this.glFeatures.integrationSlackAppNotifications &&
+ this.customState.shouldUpgradeSlack &&
+ (this.hasFieldsWithoutSection || this.hasSections)
+ );
+ },
},
methods: {
...mapActions(['setOverride', 'requestJiraIssueTypes']),
@@ -148,7 +131,6 @@ export default {
},
onSaveClick() {
this.isSaving = true;
-
if (this.integrationActive && !this.form().checkValidity()) {
this.isSaving = false;
this.setIsValidated();
@@ -194,7 +176,6 @@ export default {
},
onResetClick() {
this.isResetting = true;
-
return axios
.post(this.propsSource.resetPath)
.then(() => {
@@ -227,7 +208,10 @@ export default {
billingPlanNames,
slackUpgradeInfo: {
title: s__(
- `SlackIntegration|Notifications only work if you're on the latest version of the GitLab for Slack app`,
+ `SlackIntegration|Update to the latest version of GitLab for Slack to get notifications`,
+ ),
+ text: s__(
+ `SlackIntegration|Update to the latest version to receive notifications from GitLab.`,
),
btnText: s__('SlackIntegration|Update to the latest version'),
},
@@ -284,16 +268,18 @@ export default {
</div>
</section>
+ <div v-if="shouldUpgradeSlack" class="gl-border-t">
+ <gl-alert
+ :dismissible="false"
+ :title="$options.slackUpgradeInfo.title"
+ :primary-button-link="customState.upgradeSlackUrl"
+ :primary-button-text="$options.slackUpgradeInfo.btnText"
+ class="gl-mb-8 gl-mt-5"
+ >{{ $options.slackUpgradeInfo.text }}</gl-alert
+ >
+ </div>
+
<template v-if="hasSections">
- <div v-if="customState.shouldUpgradeSlack && isSlackIntegration" class="gl-border-t">
- <gl-alert
- :title="$options.slackUpgradeInfo.title"
- variant="warning"
- :primary-button-link="customState.upgradeSlackUrl"
- :primary-button-text="$options.slackUpgradeInfo.btnText"
- class="gl-mb-8 gl-mt-5"
- />
- </div>
<div
v-for="(section, index) in customState.sections"
:key="section.type"
@@ -344,71 +330,16 @@ export default {
</div>
</section>
- <section v-if="isEditable" :class="!hasSections && 'gl-lg-display-flex gl-justify-content-end'">
- <div :class="!hasSections && 'gl-flex-basis-two-thirds'">
- <div
- class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
- >
- <div>
- <template v-if="isInstanceOrGroupLevel">
- <gl-button
- v-gl-modal.confirmSaveIntegration
- category="primary"
- variant="confirm"
- :loading="isSaving"
- :disabled="disableButtons"
- data-testid="save-button-instance-group"
- data-qa-selector="save_changes_button"
- >
- {{ __('Save changes') }}
- </gl-button>
- <confirmation-modal @submit="onSaveClick" />
- </template>
- <gl-button
- v-else
- category="primary"
- variant="confirm"
- type="submit"
- :loading="isSaving"
- :disabled="disableButtons"
- data-testid="save-button"
- data-qa-selector="save_changes_button"
- @click.prevent="onSaveClick"
- >
- {{ __('Save changes') }}
- </gl-button>
-
- <gl-button
- v-if="showTestButton"
- category="secondary"
- variant="confirm"
- :loading="isTesting"
- :disabled="disableButtons"
- data-testid="test-button"
- @click.prevent="onTestClick"
- >
- {{ __('Test settings') }}
- </gl-button>
-
- <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
- </div>
-
- <template v-if="showResetButton">
- <gl-button
- v-gl-modal.confirmResetIntegration
- category="tertiary"
- variant="danger"
- :loading="isResetting"
- :disabled="disableButtons"
- data-testid="reset-button"
- >
- {{ __('Reset') }}
- </gl-button>
-
- <reset-confirmation-modal @reset="onResetClick" />
- </template>
- </div>
- </div>
- </section>
+ <integration-form-actions
+ v-if="isEditable"
+ :has-sections="hasSections"
+ :class="{ 'gl-lg-display-flex gl-justify-content-end': !hasSections }"
+ :is-saving="isSaving"
+ :is-testing="isTesting"
+ :is-resetting="isResetting"
+ @save="onSaveClick"
+ @test="onTestClick"
+ @reset="onResetClick"
+ />
</gl-form>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
new file mode 100644
index 00000000000..e5ad5149cf7
--- /dev/null
+++ b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
@@ -0,0 +1,143 @@
+<script>
+import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { mapState, mapGetters } from 'vuex';
+import { integrationLevels } from '~/integrations/constants';
+import ConfirmationModal from './confirmation_modal.vue';
+import ResetConfirmationModal from './reset_confirmation_modal.vue';
+
+export default {
+ name: 'IntegrationFormActions',
+ components: {
+ GlButton,
+ ConfirmationModal,
+ ResetConfirmationModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
+ },
+ props: {
+ hasSections: {
+ type: Boolean,
+ required: true,
+ },
+ isSaving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isTesting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResetting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ ...mapGetters(['propsSource']),
+ ...mapState(['customState']),
+ isInstanceOrGroupLevel() {
+ return (
+ this.customState.integrationLevel === integrationLevels.INSTANCE ||
+ this.customState.integrationLevel === integrationLevels.GROUP
+ );
+ },
+ showResetButton() {
+ return this.isInstanceOrGroupLevel && this.propsSource.resetPath;
+ },
+ showTestButton() {
+ return this.propsSource.canTest;
+ },
+ disableButtons() {
+ return Boolean(this.isSaving || this.isResetting || this.isTesting);
+ },
+ },
+ methods: {
+ onSaveClick() {
+ this.$emit('save');
+ },
+ onTestClick() {
+ this.$emit('test');
+ },
+ onResetClick() {
+ this.$emit('reset');
+ },
+ },
+};
+</script>
+<template>
+ <section>
+ <div :class="{ 'gl-flex-basis-two-thirds': !hasSections }">
+ <div
+ class="footer-block row-content-block gl-lg-display-flex gl-justify-content-space-between"
+ >
+ <div>
+ <template v-if="isInstanceOrGroupLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="confirm"
+ :loading="isSaving"
+ :disabled="disableButtons"
+ data-testid="save-button"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
+ <gl-button
+ v-else
+ category="primary"
+ variant="confirm"
+ type="submit"
+ :loading="isSaving"
+ :disabled="disableButtons"
+ data-testid="save-button"
+ data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+
+ <gl-button
+ v-if="showTestButton"
+ category="secondary"
+ variant="confirm"
+ :loading="isTesting"
+ :disabled="disableButtons"
+ data-testid="test-button"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
+
+ <gl-button
+ :href="propsSource.cancelPath"
+ data-testid="cancel-button"
+ :disabled="disableButtons"
+ >{{ __('Cancel') }}</gl-button
+ >
+ </div>
+
+ <template v-if="showResetButton">
+ <gl-button
+ v-gl-modal.confirmResetIntegration
+ category="tertiary"
+ variant="danger"
+ :loading="isResetting"
+ :disabled="disableButtons"
+ data-testid="reset-button"
+ >
+ {{ __('Reset') }}
+ </gl-button>
+
+ <reset-confirmation-modal @reset="onResetClick" />
+ </template>
+ </div>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index f15ad5e052e..b53bcd50f16 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -108,6 +108,7 @@ export default function initIntegrationSettingsForm() {
const initialState = {
defaultState: null,
customState: customSettingsProps,
+ editable: customSettingsProps.editable && !customSettingsProps.shouldUpgradeSlack,
};
if (defaultSettingsEl) {
initialState.defaultState = Object.freeze(parseDatasetToProps(defaultSettingsEl.dataset));
diff --git a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
index 31b7fd4cc42..b4e9a3a1559 100644
--- a/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/import_project_members_modal.vue
@@ -5,6 +5,10 @@ import { importProjectMembers } from '~/api/projects_api';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
import eventHub from '../event_hub';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ProjectSelect from './project_select.vue';
export default {
@@ -24,6 +28,11 @@ export default {
type: String,
required: true,
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -59,6 +68,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openProjectMembersModal', () => {
this.openModal();
});
@@ -74,16 +87,22 @@ export default {
submitImport() {
this.isLoading = true;
return importProjectMembers(this.projectId, this.projectToBeImported.id)
- .then(this.showToastMessage)
+ .then(this.onInviteSuccess)
.catch(this.showErrorAlert)
.finally(() => {
this.isLoading = false;
this.projectToBeImported = {};
});
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showToastMessage();
+ }
+ },
showToastMessage() {
this.$toast.show(this.$options.i18n.successMessage, this.$options.toastOptions);
-
this.closeModal();
},
showErrorAlert() {
diff --git a/app/assets/javascripts/invite_members/components/invite_group_notification.vue b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
new file mode 100644
index 00000000000..767675cc64c
--- /dev/null
+++ b/app/assets/javascripts/invite_members/components/invite_group_notification.vue
@@ -0,0 +1,37 @@
+<script>
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { GROUP_MODAL_ALERT_BODY } from '../constants';
+
+const SHARE_GROUP_LINK =
+ 'https://docs.gitlab.com/ee/user/group/manage.html#share-a-group-with-another-group';
+
+export default {
+ SHARE_GROUP_LINK,
+ name: 'InviteGroupNotification',
+ components: { GlAlert, GlSprintf, GlLink },
+ inject: ['freeUsersLimit'],
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ },
+ i18n: {
+ body: GROUP_MODAL_ALERT_BODY,
+ },
+};
+</script>
+
+<template>
+ <gl-alert variant="warning" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="$options.SHARE_GROUP_LINK" target="_blank" class="gl-label-link">{{
+ content
+ }}</gl-link>
+ </template>
+
+ <template #count>{{ freeUsersLimit }}</template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index 2ad4bb1a11a..3be3b9df747 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -6,13 +6,19 @@ import InviteModalBase from 'ee_else_ce/invite_members/components/invite_modal_b
import { GROUP_FILTERS, GROUP_MODAL_LABELS } from '../constants';
import eventHub from '../event_hub';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import GroupSelect from './group_select.vue';
+import InviteGroupNotification from './invite_group_notification.vue';
export default {
name: 'InviteMembersModal',
components: {
GroupSelect,
InviteModalBase,
+ InviteGroupNotification,
},
props: {
id: {
@@ -31,6 +37,10 @@ export default {
type: String,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
accessLevels: {
type: Object,
required: true,
@@ -57,6 +67,15 @@ export default {
type: Array,
required: true,
},
+ freeUserCapEnabled: {
+ type: Boolean,
+ required: true,
+ },
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -85,6 +104,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openGroupModal', () => {
this.openModal();
});
@@ -114,7 +137,7 @@ export default {
expires_at: expiresAt,
})
.then(() => {
- this.showSuccessMessage();
+ this.onInviteSuccess();
})
.catch((e) => {
this.showInvalidFeedbackMessage(e);
@@ -128,6 +151,13 @@ export default {
this.isLoading = false;
this.groupToBeSharedWith = {};
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
this.closeModal();
@@ -155,9 +185,14 @@ export default {
:root-group-id="rootId"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
>
+ <template #alert>
+ <invite-group-notification v-if="freeUserCapEnabled" :name="name" />
+ </template>
+
<template #select>
<group-select
v-model="groupToBeSharedWith"
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 f61e822bf7e..fbb547c28ff 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -29,6 +29,10 @@ import eventHub from '../event_hub';
import { responseFromSuccess } from '../utils/response_message_parser';
import { memberName } from '../utils/member_utils';
import { getInvalidFeedbackMessage } from '../utils/get_invalid_feedback_message';
+import {
+ displaySuccessfulInvitationAlert,
+ reloadOnInvitationSuccess,
+} from '../utils/trigger_successful_invite_alert';
import ModalConfetti from './confetti.vue';
import MembersTokenSelect from './members_token_select.vue';
import UserLimitNotification from './user_limit_notification.vue';
@@ -98,11 +102,20 @@ export default {
type: Array,
required: true,
},
+ fullPath: {
+ type: String,
+ required: true,
+ },
usersLimitDataset: {
type: Object,
required: false,
default: () => ({}),
},
+ reloadPageOnSubmit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -119,7 +132,7 @@ export default {
selectedAccessLevel: undefined,
errorsLimit: 2,
isErrorsSectionExpanded: false,
- emptyInvitesError: false,
+ shouldShowEmptyInvitesAlert: false,
};
},
computed: {
@@ -204,12 +217,15 @@ export default {
count: this.errorsExpanded.length,
});
},
+ formGroupDescription() {
+ return this.invalidFeedbackMessage ? null : this.$options.labels.placeHolder;
+ },
},
watch: {
isEmptyInvites: {
handler(updatedValue) {
// nothing to do if the invites are **still** empty and the emptyInvites were never set from submit
- if (!updatedValue && !this.emptyInvitesError) {
+ if (!updatedValue && !this.shouldShowEmptyInvitesAlert) {
return;
}
@@ -218,6 +234,10 @@ export default {
},
},
mounted() {
+ if (this.reloadPageOnSubmit) {
+ displaySuccessfulInvitationAlert();
+ }
+
eventHub.$on('openModal', (options) => {
this.openModal(options);
if (this.isOnLearnGitlab) {
@@ -258,16 +278,17 @@ export default {
const tracking = new ExperimentTracking(experimentName);
tracking.event(eventName);
},
- showEmptyInvitesError() {
- this.invalidFeedbackMessage = this.$options.labels.emptyInvitesErrorText;
- this.emptyInvitesError = true;
+ showEmptyInvitesAlert() {
+ this.invalidFeedbackMessage = this.$options.labels.placeHolder;
+ this.shouldShowEmptyInvitesAlert = true;
+ this.$refs.alerts.focus();
},
sendInvite({ accessLevel, expiresAt }) {
this.isLoading = true;
this.clearValidation();
if (!this.isEmptyInvites) {
- this.showEmptyInvitesError();
+ this.showEmptyInvitesAlert();
return;
}
@@ -298,7 +319,7 @@ export default {
if (error) {
this.showMemberErrors(message);
} else {
- this.showSuccessMessage();
+ this.onInviteSuccess();
}
})
.catch((e) => this.showInvalidFeedbackMessage(e))
@@ -308,6 +329,7 @@ export default {
},
showMemberErrors(message) {
this.invalidMembers = message;
+ this.$refs.alerts.focus();
},
tokenName(username) {
// initial token creation hits this and nothing is found... so safe navigation
@@ -322,6 +344,7 @@ export default {
resetFields() {
this.clearValidation();
this.isLoading = false;
+ this.shouldShowEmptyInvitesAlert = false;
this.newUsersToInvite = [];
this.selectedTasksToBeDone = [];
[this.selectedTaskProject] = this.projects;
@@ -329,6 +352,13 @@ export default {
changeSelectedTaskProject(project) {
this.selectedTaskProject = project;
},
+ onInviteSuccess() {
+ if (this.reloadPageOnSubmit) {
+ reloadOnInvitationSuccess();
+ } else {
+ this.showSuccessMessage();
+ }
+ },
showSuccessMessage() {
if (this.isOnLearnGitlab) {
eventHub.$emit('showSuccessfulInvitationsAlert');
@@ -347,7 +377,7 @@ export default {
},
clearEmptyInviteError() {
this.invalidFeedbackMessage = '';
- this.emptyInvitesError = false;
+ this.shouldShowEmptyInvitesAlert = false;
},
removeToken(token) {
delete this.invalidMembers[memberName(token)];
@@ -370,12 +400,13 @@ export default {
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="$options.labels.searchField"
- :form-group-description="$options.labels.placeHolder"
+ :form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:users-limit-dataset="usersLimitDataset"
+ :full-path="fullPath"
@reset="resetFields"
@submit="sendInvite"
@access-level="onAccessLevelUpdate"
@@ -390,59 +421,77 @@ export default {
</template>
<template #alert>
- <gl-alert
- v-if="hasInvalidMembers"
- variant="danger"
- :dismissible="false"
- :title="memberErrorTitle"
- data-testid="alert-member-error"
- >
- {{ $options.labels.memberErrorListText }}
- <ul class="gl-pl-5 gl-mb-0">
- <li v-for="error in errorsLimited" :key="error.member" data-testid="errors-limited-item">
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- <template v-if="shouldErrorsSectionExpand">
- <gl-collapse v-model="isErrorsSectionExpanded">
- <ul class="gl-pl-5 gl-mb-0">
- <li
- v-for="error in errorsExpanded"
- :key="error.member"
- data-testid="errors-expanded-item"
- >
- <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
- </li>
- </ul>
- </gl-collapse>
- <gl-button
- class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
- data-testid="accordion-button"
- variant="link"
- @click="toggleErrorExpansion"
- >
- {{ errorCollapseText }}
- <gl-icon
- name="chevron-down"
- class="gl-transition-medium"
- :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
- />
- </gl-button>
- </template>
- </gl-alert>
- <user-limit-notification
- v-else-if="showUserLimitNotification"
- :limit-variant="limitVariant"
- :users-limit-dataset="usersLimitDataset"
- />
+ <div ref="alerts" tabindex="-1">
+ <gl-alert
+ v-if="shouldShowEmptyInvitesAlert"
+ id="empty-invites-alert"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ data-testid="empty-invites-alert"
+ >
+ {{ $options.labels.emptyInvitesAlertText }}
+ </gl-alert>
+ <gl-alert
+ v-if="hasInvalidMembers"
+ class="gl-mb-4"
+ variant="danger"
+ :dismissible="false"
+ :title="memberErrorTitle"
+ data-testid="alert-member-error"
+ >
+ {{ $options.labels.memberErrorListText }}
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsLimited"
+ :key="error.member"
+ data-testid="errors-limited-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ <template v-if="shouldErrorsSectionExpand">
+ <gl-collapse v-model="isErrorsSectionExpanded">
+ <ul class="gl-pl-5 gl-mb-0">
+ <li
+ v-for="error in errorsExpanded"
+ :key="error.member"
+ data-testid="errors-expanded-item"
+ >
+ <strong>{{ error.displayedMemberName }}:</strong> {{ error.message }}
+ </li>
+ </ul>
+ </gl-collapse>
+ <gl-button
+ class="gl-text-decoration-none! gl-shadow-none! gl-mt-3"
+ data-testid="accordion-button"
+ variant="link"
+ @click="toggleErrorExpansion"
+ >
+ {{ errorCollapseText }}
+ <gl-icon
+ name="chevron-down"
+ class="gl-transition-medium"
+ :class="{ 'gl-rotate-180': isErrorsSectionExpanded }"
+ />
+ </gl-button>
+ </template>
+ </gl-alert>
+ <user-limit-notification
+ v-else-if="showUserLimitNotification"
+ :limit-variant="limitVariant"
+ :users-limit-dataset="usersLimitDataset"
+ />
+ </div>
</template>
- <template #select="{ exceptionState, labelId }">
+ <template #select="{ exceptionState, inputId }">
<members-token-select
v-model="newUsersToInvite"
class="gl-mb-2"
+ aria-labelledby="empty-invites-alert"
+ :input-id="inputId"
:exception-state="exceptionState"
- :aria-labelledby="labelId"
:users-filter="usersFilter"
:filter-id="filterId"
:invalid-members="invalidMembers"
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 e3511a49fc5..2cbd681c67d 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -1,14 +1,5 @@
<script>
-import {
- GlFormGroup,
- GlModal,
- GlDropdown,
- GlDropdownItem,
- GlDatepicker,
- GlLink,
- GlSprintf,
- GlFormInput,
-} from '@gitlab/ui';
+import { GlFormGroup, GlFormSelect, GlModal, GlDatepicker, GlLink, GlSprintf } from '@gitlab/ui';
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -37,13 +28,11 @@ const DEFAULT_SLOTS = [
export default {
components: {
GlFormGroup,
+ GlFormSelect,
GlDatepicker,
GlLink,
GlModal,
- GlDropdown,
- GlDropdownItem,
GlSprintf,
- GlFormInput,
ContentTransition,
},
mixins: [Tracking.mixin()],
@@ -141,14 +130,23 @@ export default {
};
},
computed: {
+ accessLevelsOptions() {
+ return Object.entries(this.accessLevels).map(([text, value]) => ({ text, value }));
+ },
introText() {
return sprintf(this.labelIntroText, { name: this.name });
},
exceptionState() {
return this.invalidFeedbackMessage ? false : null;
},
- selectLabelId() {
- return `${this.modalId}_select`;
+ selectId() {
+ return `${this.modalId}_search`;
+ },
+ dropdownId() {
+ return `${this.modalId}_dropdown`;
+ },
+ datepickerId() {
+ return `${this.modalId}_expires_at`;
},
selectedRoleName() {
return Object.keys(this.accessLevels).find(
@@ -218,9 +216,6 @@ export default {
this.$emit('cancel');
},
- changeSelectedItem(item) {
- this.selectedAccessLevel = item;
- },
onSubmit(e) {
// We never want to hide when submitting
e.preventDefault();
@@ -279,64 +274,50 @@ export default {
<slot name="alert"></slot>
<gl-form-group
+ :label="labelSearchField"
+ :label-for="selectId"
:invalid-feedback="invalidFeedbackMessage"
:state="exceptionState"
:description="formGroupDescription"
data-testid="members-form-group"
>
- <label :id="selectLabelId" class="col-form-label">{{ labelSearchField }}</label>
- <slot name="select" v-bind="{ exceptionState, labelId: selectLabelId }"></slot>
+ <slot name="select" v-bind="{ exceptionState, inputId: selectId }"></slot>
</gl-form-group>
- <label class="gl-font-weight-bold">{{ $options.ACCESS_LEVEL }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-dropdown
- class="gl-shadow-none gl-w-full"
+ <gl-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_LEVEL"
+ :label-for="dropdownId"
+ >
+ <template #description>
+ <gl-sprintf :message="$options.READ_MORE_TEXT">
+ <template #link="{ content }">
+ <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-select
+ :id="dropdownId"
+ v-model="selectedAccessLevel"
data-qa-selector="access_level_dropdown"
- v-bind="$attrs"
- :text="selectedRoleName"
- >
- <template v-for="(key, item) in accessLevels">
- <gl-dropdown-item
- :key="key"
- active-class="is-active"
- is-check-item
- :is-checked="key === selectedAccessLevel"
- @click="changeSelectedItem(key)"
- >
- <div>{{ item }}</div>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
- </div>
-
- <div class="gl-mt-2 gl-w-half gl-xs-w-full">
- <gl-sprintf :message="$options.READ_MORE_TEXT">
- <template #link="{ content }">
- <gl-link :href="helpLink" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </div>
+ :options="accessLevelsOptions"
+ />
+ </gl-form-group>
- <label class="gl-mt-5 gl-display-block" for="expires_at">{{
- $options.ACCESS_EXPIRE_DATE
- }}</label>
- <div class="gl-mt-2 gl-w-half gl-xs-w-full gl-display-inline-block">
+ <gl-form-group
+ class="gl-w-half gl-xs-w-full"
+ :label="$options.ACCESS_EXPIRE_DATE"
+ :label-for="datepickerId"
+ >
<gl-datepicker
v-model="selectedDate"
- class="gl-display-inline!"
+ :input-id="datepickerId"
+ class="gl-display-block!"
:min-date="minDate"
:target="null"
- >
- <template #default="{ formattedDate }">
- <gl-form-input
- class="gl-w-full"
- :value="formattedDate"
- :placeholder="__(`YYYY-MM-DD`)"
- />
- </template>
- </gl-datepicker>
- </div>
+ />
+ </gl-form-group>
+
<slot name="form-after"></slot>
</template>
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 2ddb04e1eeb..68602068699 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -49,6 +49,11 @@ export default {
type: Object,
required: true,
},
+ inputId: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -84,6 +89,13 @@ export default {
hasInvalidMembers() {
return !isEmpty(this.invalidMembers);
},
+ textInputAttrs() {
+ return {
+ 'data-testid': 'members-token-select-input',
+ 'data-qa-selector': 'members_token_select_input',
+ id: this.inputId,
+ };
+ },
},
watch: {
// We might not really want this to be *reactive* since we want the "class" state to be
@@ -183,10 +195,7 @@ export default {
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
- :text-input-attrs="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
- 'data-testid': 'members-token-select-input',
- 'data-qa-selector': 'members_token_select_input',
- } /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
+ :text-input-attrs="textInputAttrs"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index de7b1019782..a894eb24d38 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -9,6 +9,7 @@ export const INVITE_MEMBERS_FOR_TASK = {
view: 'modal_opened_from_email',
submit: 'submit',
};
+export const TOAST_MESSAGE_LOCALSTORAGE_KEY = 'members_invited_successfully';
export const GROUP_FILTERS = {
ALL: 'all',
@@ -57,6 +58,10 @@ export const GROUP_MODAL_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
"InviteMembersModal|You're inviting a group to the %{strongStart}%{name}%{strongEnd} project.",
);
+export const GROUP_MODAL_ALERT_BODY = s__(
+ 'InviteMembersModal| Inviting a group %{linkStart}adds its members to your group%{linkEnd}, including members who join after the invite. This might put your group over the free %{count} user limit.',
+);
+
export const GROUP_SEARCH_FIELD = s__('InviteMembersModal|Select a group to invite');
export const GROUP_PLACEHOLDER = s__('InviteMembersModal|Search for a group to invite');
@@ -77,9 +82,7 @@ export const MEMBER_ERROR_LIST_TEXT = s__(
);
export const COLLAPSED_ERRORS = s__('InviteMembersModal|Show more (%{count})');
export const EXPANDED_ERRORS = s__('InviteMembersModal|Show less');
-export const EMPTY_INVITES_ERROR_TEXT = s__(
- 'InviteMembersModal|Please select members or type email addresses to invite',
-);
+export const EMPTY_INVITES_ALERT_TEXT = s__('InviteMembersModal|Please add members to invite');
export const MEMBER_MODAL_LABELS = {
modal: {
@@ -117,7 +120,7 @@ export const MEMBER_MODAL_LABELS = {
memberErrorListText: MEMBER_ERROR_LIST_TEXT,
collapsedErrors: COLLAPSED_ERRORS,
expandedErrors: EXPANDED_ERRORS,
- emptyInvitesErrorText: EMPTY_INVITES_ERROR_TEXT,
+ emptyInvitesAlertText: EMPTY_INVITES_ALERT_TEXT,
};
export const GROUP_MODAL_LABELS = {
diff --git a/app/assets/javascripts/invite_members/init_import_project_members_modal.js b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
index daaa1315884..227d8395250 100644
--- a/app/assets/javascripts/invite_members/init_import_project_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_import_project_members_modal.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import ImportProjectMembersModal from '~/invite_members/components/import_project_members_modal.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
export default function initImportProjectMembersModal() {
const el = document.querySelector('.js-import-project-members-modal');
@@ -8,7 +9,7 @@ export default function initImportProjectMembersModal() {
return false;
}
- const { projectId, projectName } = el.dataset;
+ const { projectId, projectName, reloadPageOnSubmit } = el.dataset;
return new Vue({
el,
@@ -17,6 +18,7 @@ export default function initImportProjectMembersModal() {
props: {
projectId,
projectName,
+ reloadPageOnSubmit: parseBoolean(reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/init_invite_groups_modal.js b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
index be1576ad0b0..53b756b610f 100644
--- a/app/assets/javascripts/invite_members/init_invite_groups_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_groups_modal.js
@@ -28,6 +28,9 @@ export default function initInviteGroupsModal() {
return new Vue({
el,
+ provide: {
+ freeUsersLimit: parseInt(el.dataset.freeUsersLimit, 10),
+ },
render: (createElement) =>
createElement(InviteGroupsModal, {
props: {
@@ -38,6 +41,8 @@ export default function initInviteGroupsModal() {
groupSelectFilter: el.dataset.groupsFilter,
groupSelectParentId: parseInt(el.dataset.parentId, 10),
invalidGroups: JSON.parse(el.dataset.invalidGroups || '[]'),
+ freeUserCapEnabled: parseBoolean(el.dataset.freeUserCapEnabled),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
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 a4be3f205a3..842ab07f368 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -41,6 +41,7 @@ export default (function initInviteMembersModal() {
usersLimitDataset: convertObjectPropsToCamelCase(
JSON.parse(el.dataset.usersLimitDataset || '{}'),
),
+ reloadPageOnSubmit: parseBoolean(el.dataset.reloadPageOnSubmit),
},
}),
});
diff --git a/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
new file mode 100644
index 00000000000..4d3a7951265
--- /dev/null
+++ b/app/assets/javascripts/invite_members/utils/trigger_successful_invite_alert.js
@@ -0,0 +1,23 @@
+import { createAlert } from '~/flash';
+import AccessorUtilities from '~/lib/utils/accessor';
+
+import { TOAST_MESSAGE_LOCALSTORAGE_KEY, TOAST_MESSAGE_SUCCESSFUL } from '../constants';
+
+export function displaySuccessfulInvitationAlert() {
+ if (!AccessorUtilities.canUseLocalStorage()) {
+ return;
+ }
+
+ const showAlert = Boolean(localStorage.getItem(TOAST_MESSAGE_LOCALSTORAGE_KEY));
+ if (showAlert) {
+ localStorage.removeItem(TOAST_MESSAGE_LOCALSTORAGE_KEY);
+ createAlert({ message: TOAST_MESSAGE_SUCCESSFUL, variant: 'info' });
+ }
+}
+
+export function reloadOnInvitationSuccess() {
+ if (AccessorUtilities.canUseLocalStorage()) {
+ localStorage.setItem(TOAST_MESSAGE_LOCALSTORAGE_KEY, 'true');
+ }
+ window.location.reload();
+}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
deleted file mode 100644
index 68133ceb3c7..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/constants.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { __ } from '~/locale';
-
-export const statusDropdownOptions = [
- {
- text: __('Open'),
- value: 'reopen',
- },
- {
- text: __('Closed'),
- value: 'close',
- },
-];
-
-export const subscriptionsDropdownOptions = [
- {
- text: __('Subscribe'),
- value: 'subscribe',
- },
- {
- text: __('Unsubscribe'),
- value: 'unsubscribe',
- },
-];
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
deleted file mode 100644
index b7cb805ee37..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { gqlClient } from '../../issues/list/graphql';
-import StatusDropdown from './components/status_dropdown.vue';
-import SubscriptionsDropdown from './components/subscriptions_dropdown.vue';
-import MoveIssuesButton from './components/move_issues_button.vue';
-import issuableBulkUpdateActions from './issuable_bulk_update_actions';
-import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
-
-export function initBulkUpdateSidebar(prefixId) {
- const el = document.querySelector('.issues-bulk-update');
-
- if (!el) {
- return;
- }
-
- issuableBulkUpdateActions.init({ prefixId });
- new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
-}
-
-export function initStatusDropdown() {
- const el = document.querySelector('.js-status-dropdown');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'StatusDropdownRoot',
- render: (createElement) => createElement(StatusDropdown),
- });
-}
-
-export function initSubscriptionsDropdown() {
- const el = document.querySelector('.js-subscriptions-dropdown');
-
- if (!el) {
- return null;
- }
-
- return new Vue({
- el,
- name: 'SubscriptionsDropdownRoot',
- render: (createElement) => createElement(SubscriptionsDropdown),
- });
-}
-
-export function initMoveIssuesButton() {
- const el = document.querySelector('.js-move-issues');
-
- if (!el) {
- return null;
- }
-
- const { dataset } = el;
-
- Vue.use(VueApollo);
- const apolloProvider = new VueApollo({
- defaultClient: gqlClient,
- });
-
- return new Vue({
- el,
- name: 'MoveIssuesRoot',
- apolloProvider,
- render: (createElement) =>
- createElement(MoveIssuesButton, {
- props: {
- projectFullPath: dataset.projectFullPath,
- projectsFetchPath: dataset.projectsFetchPath,
- },
- }),
- });
-}
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 254248ef1d4..fd55f05e955 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -1,13 +1,7 @@
<script>
import '~/commons/bootstrap';
-import {
- GlIcon,
- GlLink,
- GlTooltip,
- GlTooltipDirective,
- GlButton,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlLink, GlTooltip, GlTooltipDirective, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index 10dbefce503..ed336deb2ed 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -1,12 +1,25 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
-import IssuableContext from '~/issuable/issuable_context';
import { parseBoolean } from '~/lib/utils/common_utils';
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';
+
+export function initBulkUpdateSidebar(prefixId) {
+ const el = document.querySelector('.issues-bulk-update');
+
+ if (!el) {
+ return;
+ }
+
+ issuableBulkUpdateActions.init({ prefixId });
+ new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
+}
export function initCsvImportExportButtons() {
const el = document.querySelector('.js-csv-import-export-buttons');
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable/issuable_bulk_update_actions.js
index 14824820c0d..c386267501a 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable/issuable_bulk_update_actions.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { difference, intersection, union } from 'lodash';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -32,7 +32,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return createFlash({
+ return createAlert({
message: __('Issue update failed'),
});
},
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
index b46a95c7dfa..095da60a583 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/issuable_bulk_update_sidebar.js
@@ -3,7 +3,12 @@
import $ from 'jquery';
import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
-import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
+import {
+ mountMilestoneDropdown,
+ mountMoveIssuesButton,
+ mountStatusDropdown,
+ mountSubscriptionsDropdown,
+} from '~/sidebar/mount_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
const HIDDEN_CLASS = 'hidden';
@@ -56,6 +61,9 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
mountMilestoneDropdown();
+ mountMoveIssuesButton();
+ mountStatusDropdown();
+ mountSubscriptionsDropdown();
// Checking IS_EE and using ee_else_ce is odd, but we do it here to satisfy
// the import/no-unresolved lint rule when FOSS_ONLY=1, even though at
diff --git a/app/assets/javascripts/issuable/issuable_label_selector.js b/app/assets/javascripts/issuable/issuable_label_selector.js
new file mode 100644
index 00000000000..ad8bbf04d6f
--- /dev/null
+++ b/app/assets/javascripts/issuable/issuable_label_selector.js
@@ -0,0 +1,56 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import {
+ DropdownVariant,
+ LabelType,
+} from '~/sidebar/components/labels/labels_select_widget/constants';
+import { WorkspaceType } from '~/issues/constants';
+import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export default () => {
+ const el = document.querySelector('.js-issuable-form-label-selector');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ fieldName,
+ fullPath,
+ initialLabels,
+ issuableType,
+ labelsFilterBasePath,
+ labelsManagePath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ allowLabelCreate: true,
+ allowLabelEdit: true,
+ allowLabelRemove: true,
+ allowScopedLabels: true,
+ attrWorkspacePath: fullPath,
+ fieldName,
+ fullPath,
+ initialLabels: JSON.parse(initialLabels),
+ issuableType,
+ labelType: LabelType.project,
+ labelsFilterBasePath,
+ labelsManagePath,
+ variant: DropdownVariant.Embedded,
+ workspaceType: WorkspaceType.project,
+ },
+ render(createElement) {
+ return createElement(IssuableLabelSelector);
+ },
+ });
+};
diff --git a/app/assets/javascripts/issues/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index 92ff7f21eff..977a505437d 100644
--- a/app/assets/javascripts/issues/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -7,7 +7,7 @@ import {
import confidentialMergeRequestState from '~/confidential_merge_request/state';
import DropLab from '~/filtered_search/droplab/drop_lab_deprecated';
import ISetter from '~/filtered_search/droplab/plugins/input_setter';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -141,7 +141,7 @@ export default class CreateMergeRequestDropdown {
.catch(() => {
this.unavailable();
this.disable();
- createFlash({
+ createAlert({
message: __('Failed to check related branches.'),
});
});
@@ -162,7 +162,7 @@ export default class CreateMergeRequestDropdown {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to create a branch for this issue. Please try again.'),
}),
);
@@ -293,7 +293,7 @@ export default class CreateMergeRequestDropdown {
}
this.unavailable();
this.disable();
- createFlash({
+ createAlert({
message: __('Failed to get ref.'),
});
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 29f6aecca03..b9d876ef72f 100644
--- a/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
+++ b/app/assets/javascripts/issues/dashboard/components/issues_dashboard_app.vue
@@ -1,13 +1,50 @@
<script>
-import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlTooltipDirective } from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
+import getIssuesQuery from 'ee_else_ce/issues/dashboard/queries/get_issues.query.graphql';
+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 { IssuableStatus } from '~/issues/constants';
+import {
+ CREATED_DESC,
+ PAGE_SIZE,
+ PARAM_STATE,
+ UPDATED_DESC,
+ urlSortParams,
+} from '~/issues/list/constants';
+import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
+import {
+ convertToApiParams,
+ convertToSearchQuery,
+ convertToUrlParams,
+ getFilterTokens,
+ getInitialPageParams,
+ getSortKey,
+ getSortOptions,
+ isSortKey,
+} from '~/issues/list/utils';
+import axios from '~/lib/utils/axios_utils';
+import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import {
+ TOKEN_TITLE_ASSIGNEE,
+ TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
+} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/constants';
+const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
+
export default {
i18n: {
calendarButtonText: __('Subscribe to calendar'),
+ closed: __('CLOSED'),
+ closedMoved: __('CLOSED (MOVED)'),
emptyStateTitle: __('Please select at least one filter to see results'),
+ errorFetchingIssues: __('An error occurred while loading issues'),
rssButtonText: __('Subscribe to RSS feed'),
searchInputPlaceholder: __('Search or filter results...'),
},
@@ -16,29 +53,237 @@ export default {
GlButton,
GlEmptyState,
IssuableList,
+ IssueCardStatistics,
+ IssueCardTimeInfo,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
- inject: ['calendarPath', 'emptyStateSvgPath', 'isSignedIn', 'rssPath'],
+ inject: [
+ 'calendarPath',
+ 'emptyStateSvgPath',
+ 'hasBlockedIssuesFeature',
+ 'hasIssuableHealthStatusFeature',
+ 'hasIssueWeightsFeature',
+ 'hasScopedLabelsFeature',
+ 'initialSort',
+ 'isPublicVisibilityRestricted',
+ 'isSignedIn',
+ 'rssPath',
+ ],
data() {
+ const state = getParameterByName(PARAM_STATE);
+
+ const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
+ const dashboardSortKey = getSortKey(this.initialSort);
+ const graphQLSortKey =
+ isSortKey(this.initialSort?.toUpperCase()) && this.initialSort.toUpperCase();
+
+ // The initial sort is an old enum value when it is saved on the dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ const sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
+
return {
+ filterTokens: getFilterTokens(window.location.search),
issues: [],
- searchTokens: [],
- sortOptions: [],
- state: IssuableStates.Opened,
+ issuesError: null,
+ pageInfo: {},
+ pageParams: getInitialPageParams(),
+ sortKey,
+ state: state || IssuableStates.Opened,
};
},
+ apollo: {
+ issues: {
+ query: getIssuesQuery,
+ variables() {
+ return {
+ hideUsers: this.isPublicVisibilityRestricted && !this.isSignedIn,
+ isSignedIn: this.isSignedIn,
+ search: this.searchQuery,
+ sort: this.sortKey,
+ state: this.state,
+ ...this.pageParams,
+ ...this.apiFilterParams,
+ };
+ },
+ update(data) {
+ return data.issues.nodes ?? [];
+ },
+ result({ data }) {
+ this.pageInfo = data?.issues.pageInfo ?? {};
+ },
+ error(error) {
+ this.issuesError = this.$options.i18n.errorFetchingIssues;
+ Sentry.captureException(error);
+ },
+ debounce: 200,
+ },
+ },
+ computed: {
+ apiFilterParams() {
+ return convertToApiParams(this.filterTokens);
+ },
+ searchQuery() {
+ return convertToSearchQuery(this.filterTokens);
+ },
+ searchTokens() {
+ const preloadedUsers = [];
+
+ if (gon.current_user_id) {
+ preloadedUsers.push({
+ id: gon.current_user_id,
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatar_url: gon.current_user_avatar_url,
+ });
+ }
+
+ const tokens = [
+ {
+ type: TOKEN_TYPE_ASSIGNEE,
+ title: TOKEN_TITLE_ASSIGNEE,
+ icon: 'user',
+ token: UserToken,
+ fetchUsers: this.fetchUsers,
+ preloadedUsers,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-assignee',
+ },
+ {
+ type: TOKEN_TYPE_AUTHOR,
+ title: TOKEN_TITLE_AUTHOR,
+ icon: 'pencil',
+ token: UserToken,
+ fetchUsers: this.fetchUsers,
+ defaultUsers: [],
+ preloadedUsers,
+ recentSuggestionsStorageKey: 'dashboard-issues-recent-tokens-author',
+ },
+ ];
+
+ return tokens;
+ },
+ showPaginationControls() {
+ return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
+ },
+ sortOptions() {
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
+ },
+ urlFilterParams() {
+ return convertToUrlParams(this.filterTokens);
+ },
+ urlParams() {
+ return {
+ search: this.searchQuery,
+ sort: urlSortParams[this.sortKey],
+ state: this.state,
+ ...this.urlFilterParams,
+ };
+ },
+ },
+ methods: {
+ fetchUsers(search) {
+ return axios.get('/-/autocomplete/users.json', { params: { active: true, search } });
+ },
+ getStatus(issue) {
+ if (issue.state === IssuableStatus.Closed && issue.moved) {
+ return this.$options.i18n.closedMoved;
+ }
+ if (issue.state === IssuableStatus.Closed) {
+ return this.$options.i18n.closed;
+ }
+ return undefined;
+ },
+ handleClickTab(state) {
+ if (this.state === state) {
+ return;
+ }
+ this.state = state;
+ this.pageParams = getInitialPageParams();
+ },
+ handleDismissAlert() {
+ this.issuesError = null;
+ },
+ handleFilter(tokens) {
+ this.filterTokens = tokens;
+ this.pageParams = getInitialPageParams();
+ },
+ handleNextPage() {
+ this.pageParams = {
+ afterCursor: this.pageInfo.endCursor,
+ firstPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handlePreviousPage() {
+ this.pageParams = {
+ beforeCursor: this.pageInfo.startCursor,
+ lastPageSize: PAGE_SIZE,
+ };
+ scrollUp();
+ },
+ handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
+ this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams();
+
+ if (this.isSignedIn) {
+ this.saveSortPreference(sortKey);
+ }
+ },
+ saveSortPreference(sortKey) {
+ this.$apollo
+ .mutate({
+ mutation: setSortPreferenceMutation,
+ variables: { input: { issuesSort: sortKey } },
+ })
+ .then(({ data }) => {
+ if (data.userPreferencesUpdate.errors.length) {
+ throw new Error(data.userPreferencesUpdate.errors);
+ }
+ })
+ .catch((error) => {
+ Sentry.captureException(error);
+ });
+ },
+ },
};
</script>
<template>
<issuable-list
+ :current-tab="state"
+ :error="issuesError"
+ :has-next-page="pageInfo.hasNextPage"
+ :has-previous-page="pageInfo.hasPreviousPage"
+ :has-scoped-labels-feature="hasScopedLabelsFeature"
+ :initial-filter-value="filterTokens"
+ :initial-sort-by="sortKey"
+ :issuables="issues"
+ :issuables-loading="$apollo.queries.issues.loading"
namespace="dashboard"
recent-searches-storage-key="issues"
:search-input-placeholder="$options.i18n.searchInputPlaceholder"
:search-tokens="searchTokens"
+ :show-pagination-controls="showPaginationControls"
+ show-work-item-type-icon
:sort-options="sortOptions"
- :issuables="issues"
:tabs="$options.IssuableListTabs"
- :current-tab="state"
+ :url-params="urlParams"
+ use-keyset-pagination
+ @click-tab="handleClickTab"
+ @dismiss-alert="handleDismissAlert"
+ @filter="handleFilter"
+ @next-page="handleNextPage"
+ @previous-page="handlePreviousPage"
+ @sort="handleSort"
>
<template #nav-actions>
<gl-button :href="rssPath" icon="rss">
@@ -49,6 +294,18 @@ export default {
</gl-button>
</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 #empty-state>
<gl-empty-state :svg-path="emptyStateSvgPath" :title="$options.i18n.emptyStateTitle" />
</template>
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index a1ae3b93f7d..e3e5cc614cb 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -1,4 +1,6 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
import IssuesDashboardApp from './components/issues_dashboard_app.vue';
@@ -9,14 +11,36 @@ export function mountIssuesDashboardApp() {
return null;
}
- const { calendarPath, emptyStateSvgPath, isSignedIn, rssPath } = el.dataset;
+ Vue.use(VueApollo);
+
+ const {
+ calendarPath,
+ emptyStateSvgPath,
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+ hasScopedLabelsFeature,
+ initialSort,
+ isPublicVisibilityRestricted,
+ isSignedIn,
+ rssPath,
+ } = el.dataset;
return new Vue({
el,
name: 'IssuesDashboardRoot',
+ apolloProvider: new VueApollo({
+ defaultClient: createDefaultClient(),
+ }),
provide: {
calendarPath,
emptyStateSvgPath,
+ hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ initialSort,
+ isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
rssPath,
},
diff --git a/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
new file mode 100644
index 00000000000..8ffcb456755
--- /dev/null
+++ b/app/assets/javascripts/issues/dashboard/queries/get_issues.query.graphql
@@ -0,0 +1,36 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/issues/list/queries/issue.fragment.graphql"
+
+query getDashboardIssues(
+ $hideUsers: Boolean = false
+ $isSignedIn: Boolean = false
+ $search: String
+ $sort: IssueSort
+ $state: IssuableState
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $afterCursor: String
+ $beforeCursor: String
+ $firstPageSize: Int
+ $lastPageSize: Int
+) {
+ issues(
+ search: $search
+ sort: $sort
+ state: $state
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ after: $afterCursor
+ before: $beforeCursor
+ first: $firstPageSize
+ last: $lastPageSize
+ ) {
+ nodes {
+ ...IssueFragment
+ reference(full: true)
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index a785790169d..e3716d0e111 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
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 GLForm from '~/gl_form';
@@ -39,6 +40,7 @@ export function initFilteredSearchServiceDesk() {
export function initForm() {
new GLForm($('.issue-form')); // eslint-disable-line no-new
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
+ IssuableLabelSelector();
new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new
new LabelsSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index a9321cf200d..de1c689e590 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -68,7 +68,7 @@ export default class Issue {
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
} else {
- createFlash({
+ createAlert({
message: issueFailMessage,
});
}
@@ -105,7 +105,7 @@ export default class Issue {
}
})
.catch(() =>
- createFlash({
+ createAlert({
message: __('Failed to load related branches'),
}),
);
diff --git a/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
new file mode 100644
index 00000000000..8aece24de0c
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/empty_state_with_any_issues.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlButton,
+ GlEmptyState,
+ },
+ inject: ['emptyStateSvgPath', 'newIssuePath', 'showNewIssueLink'],
+ props: {
+ hasSearch: {
+ type: Boolean,
+ required: true,
+ },
+ isOpenTab: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="hasSearch"
+ :description="$options.i18n.noSearchResultsDescription"
+ :title="$options.i18n.noSearchResultsTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+
+ <gl-empty-state
+ v-else-if="isOpenTab"
+ :description="$options.i18n.noOpenIssuesDescription"
+ :title="$options.i18n.noOpenIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ >
+ <template #actions>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ </template>
+ </gl-empty-state>
+
+ <gl-empty-state v-else :title="$options.i18n.noClosedIssuesTitle" :svg-path="emptyStateSvgPath" />
+</template>
diff --git a/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
new file mode 100644
index 00000000000..5a37751410a
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/empty_state_without_any_issues.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlButton, GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
+import { i18n } from '../constants';
+import NewIssueDropdown from './new_issue_dropdown.vue';
+
+export default {
+ i18n,
+ issuesHelpPagePath: helpPagePath('user/project/issues/index'),
+ components: {
+ CsvImportExportButtons,
+ GlButton,
+ GlEmptyState,
+ GlLink,
+ GlSprintf,
+ NewIssueDropdown,
+ },
+ inject: [
+ 'canCreateProjects',
+ 'emptyStateSvgPath',
+ 'isSignedIn',
+ 'jiraIntegrationPath',
+ 'newIssuePath',
+ 'newProjectPath',
+ 'showNewIssueLink',
+ 'signInPath',
+ ],
+ props: {
+ currentTabCount: {
+ type: Number,
+ required: false,
+ default: undefined,
+ },
+ exportCsvPathWithQuery: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ showCsvButtons: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showNewIssueDropdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="isSignedIn">
+ <gl-empty-state :title="$options.i18n.noIssuesTitle" :svg-path="emptyStateSvgPath">
+ <template #description>
+ <gl-link :href="$options.issuesHelpPagePath">
+ {{ $options.i18n.noIssuesDescription }}
+ </gl-link>
+ <p v-if="canCreateProjects">
+ <strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
+ </p>
+ </template>
+ <template #actions>
+ <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
+ {{ $options.i18n.newProjectLabel }}
+ </gl-button>
+ <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ {{ $options.i18n.newIssueLabel }}
+ </gl-button>
+ <csv-import-export-buttons
+ v-if="showCsvButtons"
+ class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
+ :export-csv-path="exportCsvPathWithQuery"
+ :issuable-count="currentTabCount"
+ />
+ <new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
+ </template>
+ </gl-empty-state>
+ <hr />
+ <p class="gl-text-center gl-font-weight-bold gl-mb-0">
+ {{ $options.i18n.jiraIntegrationTitle }}
+ </p>
+ <p class="gl-text-center gl-mb-0">
+ <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
+ <template #jiraDocsLink="{ content }">
+ <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <p class="gl-text-center gl-text-secondary">
+ {{ $options.i18n.jiraIntegrationSecondaryMessage }}
+ </p>
+ </div>
+
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.noIssuesTitle"
+ :svg-path="emptyStateSvgPath"
+ :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
+ :primary-button-link="signInPath"
+ >
+ <template #description>
+ <gl-link :href="$options.issuesHelpPagePath">
+ {{ $options.i18n.noIssuesDescription }}
+ </gl-link>
+ </template>
+ </gl-empty-state>
+</template>
diff --git a/app/assets/javascripts/issues/list/components/issue_card_statistics.vue b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue
new file mode 100644
index 00000000000..2d00c3e549d
--- /dev/null
+++ b/app/assets/javascripts/issues/list/components/issue_card_statistics.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { i18n } from '../constants';
+
+export default {
+ i18n,
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ issue: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <ul class="gl-display-contents">
+ <li
+ v-if="issue.mergeRequestsCount"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.relatedMergeRequests"
+ data-testid="merge-requests"
+ >
+ <gl-icon name="merge-request" />
+ {{ issue.mergeRequestsCount }}
+ </li>
+ <li
+ v-if="issue.upvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.upvotes"
+ data-testid="issuable-upvotes"
+ >
+ <gl-icon name="thumb-up" />
+ {{ issue.upvotes }}
+ </li>
+ <li
+ v-if="issue.downvotes"
+ v-gl-tooltip
+ class="gl-display-none gl-sm-display-block gl-mr-3"
+ :title="$options.i18n.downvotes"
+ data-testid="issuable-downvotes"
+ >
+ <gl-icon name="thumb-down" />
+ {{ issue.downvotes }}
+ </li>
+ <slot></slot>
+ </ul>
+</template>
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 64de4b1947b..12a83f06453 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -1,19 +1,12 @@
<script>
-import {
- GlButton,
- GlEmptyState,
- GlFilteredSearchToken,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlButton, GlFilteredSearchToken, GlTooltipDirective } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
+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 getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { ITEM_TYPE } from '~/groups/constants';
@@ -24,11 +17,11 @@ import axios from '~/lib/utils/axios_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
-import { helpPagePath } from '~/helpers/help_page_helper';
import {
- DEFAULT_NONE_ANY,
FILTERED_SEARCH_TERM,
- OPERATOR_IS_ONLY,
+ OPERATORS_IS,
+ OPERATORS_IS_NOT,
+ OPERATORS_IS_NOT_OR,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
TOKEN_TITLE_CONFIDENTIAL,
@@ -38,9 +31,8 @@ import {
TOKEN_TITLE_MY_REACTION,
TOKEN_TITLE_ORGANIZATION,
TOKEN_TITLE_RELEASE,
+ TOKEN_TITLE_SEARCH_WITHIN,
TOKEN_TITLE_TYPE,
- OPERATOR_IS_NOT_OR,
- OPERATOR_IS_AND_IS_NOT,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -50,6 +42,7 @@ import {
TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_ORGANIZATION,
TOKEN_TYPE_RELEASE,
+ TOKEN_TYPE_SEARCH_WITHIN,
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
@@ -70,11 +63,9 @@ import {
PARAM_SORT,
PARAM_STATE,
RELATIVE_POSITION_ASC,
- TYPE_TOKEN_TASK_OPTION,
UPDATED_DESC,
urlSortParams,
} from '../constants';
-
import eventHub from '../eventhub';
import reorderIssuesMutation from '../queries/reorder_issues.mutation.graphql';
import searchLabelsQuery from '../queries/search_labels.query.graphql';
@@ -91,10 +82,11 @@ import {
getSortOptions,
isSortKey,
} from '../utils';
+import EmptyStateWithAnyIssues from './empty_state_with_any_issues.vue';
+import EmptyStateWithoutAnyIssues from './empty_state_without_any_issues.vue';
import NewIssueDropdown from './new_issue_dropdown.vue';
-const AuthorToken = () =>
- import('~/vue_shared/components/filtered_search_bar/tokens/author_token.vue');
+const UserToken = () => import('~/vue_shared/components/filtered_search_bar/tokens/user_token.vue');
const EmojiToken = () =>
import('~/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue');
const LabelToken = () =>
@@ -113,13 +105,12 @@ export default {
IssuableListTabs,
components: {
CsvImportExportButtons,
+ EmptyStateWithAnyIssues,
+ EmptyStateWithoutAnyIssues,
GlButton,
- GlEmptyState,
- GlIcon,
- GlLink,
- GlSprintf,
IssuableByEmail,
IssuableList,
+ IssueCardStatistics,
IssueCardTimeInfo,
NewIssueDropdown,
},
@@ -131,15 +122,14 @@ export default {
'autocompleteAwardEmojisPath',
'calendarPath',
'canBulkUpdate',
- 'canCreateProjects',
'canReadCrmContact',
'canReadCrmOrganization',
- 'emptyStateSvgPath',
'exportCsvPath',
'fullPath',
'hasAnyIssues',
'hasAnyProjects',
'hasBlockedIssuesFeature',
+ 'hasIssuableHealthStatusFeature',
'hasIssueWeightsFeature',
'hasScopedLabelsFeature',
'initialEmail',
@@ -149,13 +139,10 @@ export default {
'isProject',
'isPublicVisibilityRestricted',
'isSignedIn',
- 'jiraIntegrationPath',
'newIssuePath',
- 'newProjectPath',
'releasesPath',
'rssPath',
'showNewIssueLink',
- 'signInPath',
],
props: {
eeSearchTokens: {
@@ -163,6 +150,21 @@ export default {
required: false,
default: () => [],
},
+ eeTypeTokenOptions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ eeWorkItemTypes: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ eeIsOkrsEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -189,10 +191,7 @@ export default {
return data[this.namespace]?.issues.nodes ?? [];
},
result({ data }) {
- if (!data) {
- return;
- }
- this.pageInfo = data[this.namespace]?.issues.pageInfo ?? {};
+ this.pageInfo = data?.[this.namespace]?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
@@ -239,24 +238,27 @@ export default {
state: this.state,
...this.pageParams,
...this.apiFilterParams,
- types: this.apiFilterParams.types || defaultWorkItemTypes,
+ types: this.apiFilterParams.types || this.defaultWorkItemTypes,
};
},
namespace() {
return this.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
},
+ defaultWorkItemTypes() {
+ return [...defaultWorkItemTypes, ...this.eeWorkItemTypes];
+ },
typeTokenOptions() {
- return defaultTypeTokenOptions.concat(TYPE_TOKEN_TASK_OPTION);
+ return [...defaultTypeTokenOptions, ...this.eeTypeTokenOptions];
},
hasOrFeature() {
return this.glFeatures.orIssuableQueries;
},
hasSearch() {
- return (
+ return Boolean(
this.searchQuery ||
- Object.keys(this.urlFilterParams).length ||
- this.pageParams.afterCursor ||
- this.pageParams.beforeCursor
+ Object.keys(this.urlFilterParams).length ||
+ this.pageParams.afterCursor ||
+ this.pageParams.beforeCursor,
);
},
isBulkEditButtonDisabled() {
@@ -284,13 +286,13 @@ export default {
return convertToUrlParams(this.filterTokens);
},
searchQuery() {
- return convertToSearchQuery(this.filterTokens) || undefined;
+ return convertToSearchQuery(this.filterTokens);
},
searchTokens() {
- const preloadedAuthors = [];
+ const preloadedUsers = [];
if (gon.current_user_id) {
- preloadedAuthors.push({
+ preloadedUsers.push({
id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
name: gon.current_user_fullname,
username: gon.current_username,
@@ -300,28 +302,41 @@ export default {
const tokens = [
{
+ type: TOKEN_TYPE_SEARCH_WITHIN,
+ title: TOKEN_TITLE_SEARCH_WITHIN,
+ icon: 'search',
+ token: GlFilteredSearchToken,
+ unique: true,
+ operators: OPERATORS_IS,
+ options: [
+ { icon: 'title', value: 'TITLE', title: this.$options.i18n.titles },
+ {
+ icon: 'text-description',
+ value: 'DESCRIPTION',
+ title: this.$options.i18n.descriptions,
+ },
+ ],
+ },
+ {
type: TOKEN_TYPE_AUTHOR,
title: TOKEN_TITLE_AUTHOR,
icon: 'pencil',
- token: AuthorToken,
- dataType: 'user',
- unique: true,
- defaultAuthors: [],
- fetchAuthors: this.fetchUsers,
+ token: UserToken,
+ defaultUsers: [],
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
+ fetchUsers: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
- preloadedAuthors,
+ preloadedUsers,
},
{
type: TOKEN_TYPE_ASSIGNEE,
title: TOKEN_TITLE_ASSIGNEE,
icon: 'user',
- token: AuthorToken,
- dataType: 'user',
- defaultAuthors: DEFAULT_NONE_ANY,
- operators: this.hasOrFeature ? OPERATOR_IS_NOT_OR : OPERATOR_IS_AND_IS_NOT,
- fetchAuthors: this.fetchUsers,
+ token: UserToken,
+ operators: this.hasOrFeature ? OPERATORS_IS_NOT_OR : OPERATORS_IS_NOT,
+ fetchUsers: this.fetchUsers,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
- preloadedAuthors,
+ preloadedUsers,
},
{
type: TOKEN_TYPE_MILESTONE,
@@ -337,7 +352,6 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
- defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-label`,
},
@@ -378,7 +392,7 @@ export default {
icon: 'eye-slash',
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ icon: 'eye-slash', value: 'yes', title: this.$options.i18n.confidentialYes },
{ icon: 'eye', value: 'no', title: this.$options.i18n.confidentialNo },
@@ -394,9 +408,8 @@ export default {
token: CrmContactToken,
fullPath: this.fullPath,
isProject: this.isProject,
- defaultContacts: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-crm-contacts`,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
});
}
@@ -409,9 +422,8 @@ export default {
token: CrmOrganizationToken,
fullPath: this.fullPath,
isProject: this.isProject,
- defaultOrganizations: DEFAULT_NONE_ANY,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-crm-organizations`,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
unique: true,
});
}
@@ -428,11 +440,14 @@ export default {
return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
},
showPageSizeControls() {
- /** only show page size controls when the tab count is greater than the default/minimum page size control i.e 20 in this case */
return this.currentTabCount > PAGE_SIZE;
},
sortOptions() {
- return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
+ return getSortOptions({
+ hasBlockedIssuesFeature: this.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: this.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: this.hasIssueWeightsFeature,
+ });
},
tabCounts() {
const { openedIssues, closedIssues, allIssues } = this.issuesCounts;
@@ -457,10 +472,7 @@ export default {
page_before: this.pageParams.beforeCursor ?? undefined,
};
},
- issuesHelpPagePath() {
- return helpPagePath('user/project/issues/index');
- },
- shouldDisableSomeFilters() {
+ shouldDisableTextSearch() {
return this.isAnonymousSearchDisabled && !this.isSignedIn;
},
},
@@ -482,18 +494,17 @@ export default {
eventHub.$off('issuables:toggleBulkEdit', this.toggleBulkEditSidebar);
},
methods: {
- fetchWithCache(path, cacheName, searchKey, search, wrapData = false) {
+ fetchWithCache(path, cacheName, searchKey, search) {
if (this.cache[cacheName]) {
const data = search
? fuzzaldrinPlus.filter(this.cache[cacheName], search, { key: searchKey })
: this.cache[cacheName].slice(0, MAX_LIST_SIZE);
- return wrapData ? Promise.resolve({ data }) : Promise.resolve(data);
+ return Promise.resolve(data);
}
return axios.get(path).then(({ data }) => {
this.cache[cacheName] = data;
- const result = data.slice(0, MAX_LIST_SIZE);
- return wrapData ? { data: result } : result;
+ return data.slice(0, MAX_LIST_SIZE);
});
},
fetchEmojis(search) {
@@ -554,14 +565,10 @@ export default {
},
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
- const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
+ const bulkUpdateSidebar = await import('~/issuable');
bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
- bulkUpdateSidebar.initStatusDropdown();
- bulkUpdateSidebar.initSubscriptionsDropdown();
- bulkUpdateSidebar.initMoveIssuesButton();
- const usersSelect = await import('~/users_select');
- const UsersSelect = usersSelect.default;
+ const UsersSelect = (await import('~/users_select')).default;
new UsersSelect(); // eslint-disable-line no-new
this.hasInitBulkEdit = true;
@@ -570,19 +577,20 @@ export default {
eventHub.$emit('issuables:enableBulkEdit');
},
handleClickTab(state) {
- if (this.state !== state) {
- this.pageParams = getInitialPageParams(this.pageSize);
+ if (this.state === state) {
+ return;
}
+
this.state = state;
+ this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
},
handleDismissAlert() {
this.issuesError = null;
},
- handleFilter(filter) {
- this.setFilterTokens(filter);
-
+ handleFilter(tokens) {
+ this.setFilterTokens(tokens);
this.pageParams = getInitialPageParams(this.pageSize);
this.$router.push({ query: this.urlParams });
@@ -642,15 +650,17 @@ export default {
});
},
handleSort(sortKey) {
+ if (this.sortKey === sortKey) {
+ return;
+ }
+
if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
this.showIssueRepositioningMessage();
return;
}
- if (this.sortKey !== sortKey) {
- this.pageParams = getInitialPageParams(this.pageSize);
- }
this.sortKey = sortKey;
+ this.pageParams = getInitialPageParams(this.pageSize);
if (this.isSignedIn) {
this.saveSortPreference(sortKey);
@@ -673,49 +683,36 @@ export default {
Sentry.captureException(error);
});
},
- setFilterTokens(filtersArg) {
- const filters = this.removeDisabledSearchTerms(filtersArg);
+ setFilterTokens(tokens) {
+ this.filterTokens = this.removeDisabledSearchTerms(tokens);
- this.filterTokens = filters;
-
- // If we filtered something out, let's show a warning message
- if (filters.length < filtersArg.length) {
+ if (this.filterTokens.length < tokens.length) {
this.showAnonymousSearchingMessage();
}
},
removeDisabledSearchTerms(filters) {
- // If we shouldn't disable anything, let's return the same thing
- if (!this.shouldDisableSomeFilters) {
- return filters;
- }
-
- const filtersWithoutSearchTerms = filters.filter(
- (token) => !(token.type === FILTERED_SEARCH_TERM && token.value?.data),
- );
-
- return filtersWithoutSearchTerms;
+ return this.shouldDisableTextSearch
+ ? filters.filter((token) => !(token.type === FILTERED_SEARCH_TERM && token.value?.data))
+ : filters;
},
showAnonymousSearchingMessage() {
- createFlash({
+ createAlert({
message: this.$options.i18n.anonymousSearchingMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
},
showIssueRepositioningMessage() {
- createFlash({
+ createAlert({
message: this.$options.i18n.issueRepositioningMessage,
- type: FLASH_TYPES.NOTICE,
+ variant: VARIANT_INFO,
});
},
toggleBulkEditSidebar(showBulkEditSidebar) {
this.showBulkEditSidebar = showBulkEditSidebar;
},
handlePageSizeChange(newPageSize) {
- /** make sure the page number is preserved so that the current context is not lost* */
- const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
- const pageNumberSize = lastPageSize ? 'lastPageSize' : 'firstPageSize';
- /** depending upon what page or page size we are dynamically set pageParams * */
- this.pageParams[pageNumberSize] = newPageSize;
+ const pageParam = getParameterByName(PARAM_LAST_PAGE_SIZE) ? 'lastPageSize' : 'firstPageSize';
+ this.pageParams[pageParam] = newPageSize;
this.pageSize = newPageSize;
scrollUp();
@@ -724,16 +721,14 @@ export default {
updateData(sortValue) {
const firstPageSize = getParameterByName(PARAM_FIRST_PAGE_SIZE);
const lastPageSize = getParameterByName(PARAM_LAST_PAGE_SIZE);
- const pageAfter = getParameterByName(PARAM_PAGE_AFTER);
- const pageBefore = getParameterByName(PARAM_PAGE_BEFORE);
const state = getParameterByName(PARAM_STATE);
const defaultSortKey = state === IssuableStates.Closed ? UPDATED_DESC : CREATED_DESC;
const dashboardSortKey = getSortKey(sortValue);
const graphQLSortKey = isSortKey(sortValue?.toUpperCase()) && sortValue.toUpperCase();
- // The initial sort is an old enum value when it is saved on the dashboard issues page.
- // The initial sort is a GraphQL enum value when it is saved on the Vue issues list page.
+ // The initial sort is an old enum value when it is saved on the Haml dashboard issues page.
+ // The initial sort is a GraphQL enum value when it is saved on the Vue group/project issues page.
let sortKey = dashboardSortKey || graphQLSortKey || defaultSortKey;
if (this.isIssueRepositioningDisabled && sortKey === RELATIVE_POSITION_ASC) {
@@ -741,15 +736,15 @@ export default {
sortKey = defaultSortKey;
}
- this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.setFilterTokens(getFilterTokens(window.location.search));
+ this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.pageParams = getInitialPageParams(
this.pageSize,
isPositiveInteger(firstPageSize) ? parseInt(firstPageSize, 10) : undefined,
isPositiveInteger(lastPageSize) ? parseInt(lastPageSize, 10) : undefined,
- pageAfter,
- pageBefore,
+ getParameterByName(PARAM_PAGE_AFTER),
+ getParameterByName(PARAM_PAGE_BEFORE),
);
this.sortKey = sortKey;
this.state = state || IssuableStates.Opened;
@@ -827,9 +822,14 @@ export default {
>
{{ $options.i18n.editIssues }}
</gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
+ <gl-button
+ v-if="showNewIssueLink && !eeIsOkrsEnabled"
+ :href="newIssuePath"
+ variant="confirm"
+ >
{{ $options.i18n.newIssueLabel }}
</gl-button>
+ <slot name="new-objective-button"></slot>
<new-issue-dropdown v-if="showNewIssueDropdown" />
</template>
@@ -842,129 +842,25 @@ export default {
</template>
<template #statistics="{ issuable = {} }">
- <li
- v-if="issuable.mergeRequestsCount"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
- :title="$options.i18n.relatedMergeRequests"
- data-testid="merge-requests"
- >
- <gl-icon name="merge-request" />
- {{ issuable.mergeRequestsCount }}
- </li>
- <li
- v-if="issuable.upvotes"
- v-gl-tooltip
- class="issuable-upvotes gl-display-none gl-sm-display-block"
- :title="$options.i18n.upvotes"
- data-testid="issuable-upvotes"
- >
- <gl-icon name="thumb-up" />
- {{ issuable.upvotes }}
- </li>
- <li
- v-if="issuable.downvotes"
- v-gl-tooltip
- class="issuable-downvotes gl-display-none gl-sm-display-block"
- :title="$options.i18n.downvotes"
- data-testid="issuable-downvotes"
- >
- <gl-icon name="thumb-down" />
- {{ issuable.downvotes }}
- </li>
- <slot :issuable="issuable"></slot>
+ <issue-card-statistics :issue="issuable" />
</template>
<template #empty-state>
- <gl-empty-state
- v-if="hasSearch"
- :description="$options.i18n.noSearchResultsDescription"
- :title="$options.i18n.noSearchResultsTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
-
- <gl-empty-state
- v-else-if="isOpenTab"
- :description="$options.i18n.noOpenIssuesDescription"
- :title="$options.i18n.noOpenIssuesTitle"
- :svg-path="emptyStateSvgPath"
- >
- <template #actions>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- </template>
- </gl-empty-state>
-
- <gl-empty-state
- v-else
- :title="$options.i18n.noClosedIssuesTitle"
- :svg-path="emptyStateSvgPath"
- />
+ <empty-state-with-any-issues :has-search="hasSearch" :is-open-tab="isOpenTab" />
+ </template>
+
+ <template #list-body>
+ <slot name="list-body"></slot>
</template>
</issuable-list>
- <template v-else-if="isSignedIn">
- <gl-empty-state :title="$options.i18n.noIssuesSignedInTitle" :svg-path="emptyStateSvgPath">
- <template #description>
- <gl-link :href="issuesHelpPagePath" target="_blank">{{
- $options.i18n.noIssuesSignedInDescription
- }}</gl-link>
- <p v-if="canCreateProjects">
- <strong>{{ $options.i18n.noGroupIssuesSignedInDescription }}</strong>
- </p>
- </template>
- <template #actions>
- <gl-button v-if="canCreateProjects" :href="newProjectPath" variant="confirm">
- {{ $options.i18n.newProjectLabel }}
- </gl-button>
- <gl-button v-if="showNewIssueLink" :href="newIssuePath" variant="confirm">
- {{ $options.i18n.newIssueLabel }}
- </gl-button>
- <csv-import-export-buttons
- v-if="showCsvButtons"
- class="gl-w-full gl-sm-w-auto gl-sm-mr-3"
- :export-csv-path="exportCsvPathWithQuery"
- :issuable-count="currentTabCount"
- />
- <new-issue-dropdown v-if="showNewIssueDropdown" class="gl-align-self-center" />
- </template>
- </gl-empty-state>
- <hr />
- <p class="gl-text-center gl-font-weight-bold gl-mb-0">
- {{ $options.i18n.jiraIntegrationTitle }}
- </p>
- <p class="gl-text-center gl-mb-0">
- <gl-sprintf :message="$options.i18n.jiraIntegrationMessage">
- <template #jiraDocsLink="{ content }">
- <gl-link :href="jiraIntegrationPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <p class="gl-text-center gl-text-gray-500">
- {{ $options.i18n.jiraIntegrationSecondaryMessage }}
- </p>
- </template>
-
- <gl-empty-state
+ <empty-state-without-any-issues
v-else
- :title="$options.i18n.noIssuesSignedOutTitle"
- :svg-path="emptyStateSvgPath"
- :primary-button-text="$options.i18n.noIssuesSignedOutButtonText"
- :primary-button-link="signInPath"
- >
- <template #description>
- <gl-link :href="issuesHelpPagePath" target="_blank">{{
- $options.i18n.noIssuesSignedOutDescription
- }}</gl-link>
- </template>
- </gl-empty-state>
+ :current-tab-count="currentTabCount"
+ :export-csv-path-with-query="exportCsvPathWithQuery"
+ :show-csv-buttons="showCsvButtons"
+ :show-new-issue-dropdown="showNewIssueDropdown"
+ />
<issuable-by-email v-if="showIssuableByEmail" class="gl-text-center gl-pt-5 gl-pb-7" />
</div>
diff --git a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
index 666e80dfd4b..e420c21a11f 100644
--- a/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
@@ -6,7 +6,7 @@ import {
GlLoadingIcon,
GlSearchBoxByType,
} from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -45,7 +45,7 @@ export default {
},
update: ({ group }) => group.projects.nodes ?? [],
error(error) {
- createFlash({
+ createAlert({
message: __('An error occurred while loading projects.'),
captureError: true,
error,
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 5ed9ceea856..49a953cad43 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -6,7 +6,7 @@ import {
FILTER_STARTED,
FILTER_UPCOMING,
OPERATOR_IS,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
@@ -22,6 +22,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
+ TOKEN_TYPE_SEARCH_WITHIN,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
WORK_ITEM_TYPE_ENUM_INCIDENT,
@@ -30,6 +31,50 @@ import {
WORK_ITEM_TYPE_ENUM_TASK,
} from '~/work_items/constants';
+export const ISSUE_REFERENCE = /^#\d+$/;
+export const MAX_LIST_SIZE = 10;
+export const PAGE_SIZE = 20;
+export const PARAM_ASSIGNEE_ID = 'assignee_id';
+export const PARAM_FIRST_PAGE_SIZE = 'first_page_size';
+export const PARAM_LAST_PAGE_SIZE = 'last_page_size';
+export const PARAM_PAGE_AFTER = 'page_after';
+export const PARAM_PAGE_BEFORE = 'page_before';
+export const PARAM_SORT = 'sort';
+export const PARAM_STATE = 'state';
+export const RELATIVE_POSITION = 'relative_position';
+
+export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
+export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
+export const CLOSED_AT_ASC = 'CLOSED_AT_ASC';
+export const CLOSED_AT_DESC = 'CLOSED_AT_DESC';
+export const CREATED_ASC = 'CREATED_ASC';
+export const CREATED_DESC = 'CREATED_DESC';
+export const DUE_DATE_ASC = 'DUE_DATE_ASC';
+export const DUE_DATE_DESC = 'DUE_DATE_DESC';
+export const HEALTH_STATUS_ASC = 'HEALTH_STATUS_ASC';
+export const HEALTH_STATUS_DESC = 'HEALTH_STATUS_DESC';
+export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
+export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
+export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
+export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
+export const POPULARITY_ASC = 'POPULARITY_ASC';
+export const POPULARITY_DESC = 'POPULARITY_DESC';
+export const PRIORITY_ASC = 'PRIORITY_ASC';
+export const PRIORITY_DESC = 'PRIORITY_DESC';
+export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
+export const TITLE_ASC = 'TITLE_ASC';
+export const TITLE_DESC = 'TITLE_DESC';
+export const UPDATED_ASC = 'UPDATED_ASC';
+export const UPDATED_DESC = 'UPDATED_DESC';
+export const WEIGHT_ASC = 'WEIGHT_ASC';
+export const WEIGHT_DESC = 'WEIGHT_DESC';
+
+export const API_PARAM = 'apiParam';
+export const URL_PARAM = 'urlParam';
+export const NORMAL_FILTER = 'normalFilter';
+export const SPECIAL_FILTER = 'specialFilter';
+export const ALTERNATIVE_FILTER = 'alternativeFilter';
+
export const i18n = {
anonymousSearchingMessage: __('You must sign in to search for specific terms.'),
calendarLabel: __('Subscribe to calendar'),
@@ -57,11 +102,9 @@ export const i18n = {
),
noOpenIssuesDescription: __('To keep this project going, create a new issue'),
noOpenIssuesTitle: __('There are no open issues'),
- noIssuesSignedInDescription: __('Learn more about issues.'),
- noIssuesSignedInTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
+ noIssuesDescription: __('Learn more about issues.'),
+ noIssuesTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noIssuesSignedOutButtonText: __('Register / Sign In'),
- noIssuesSignedOutDescription: __('Learn more about issues.'),
- noIssuesSignedOutTitle: __('Use issues to collaborate on ideas, solve problems, and plan work'),
noSearchResultsDescription: __('To widen your search, change or remove filters above'),
noSearchResultsTitle: __('Sorry, your filter produced no results'),
relatedMergeRequests: __('Related merge requests'),
@@ -69,45 +112,10 @@ export const i18n = {
rssLabel: __('Subscribe to RSS feed'),
searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
+ titles: __('Titles'),
+ descriptions: __('Descriptions'),
};
-export const ISSUE_REFERENCE = /^#\d+$/;
-export const MAX_LIST_SIZE = 10;
-export const PAGE_SIZE = 20;
-export const PAGE_SIZE_MANUAL = 100;
-export const PARAM_ASSIGNEE_ID = 'assignee_id';
-export const PARAM_FIRST_PAGE_SIZE = 'first_page_size';
-export const PARAM_LAST_PAGE_SIZE = 'last_page_size';
-export const PARAM_PAGE_AFTER = 'page_after';
-export const PARAM_PAGE_BEFORE = 'page_before';
-export const PARAM_SORT = 'sort';
-export const PARAM_STATE = 'state';
-export const RELATIVE_POSITION = 'relative_position';
-
-export const BLOCKING_ISSUES_ASC = 'BLOCKING_ISSUES_ASC';
-export const BLOCKING_ISSUES_DESC = 'BLOCKING_ISSUES_DESC';
-export const CREATED_ASC = 'CREATED_ASC';
-export const CREATED_DESC = 'CREATED_DESC';
-export const DUE_DATE_ASC = 'DUE_DATE_ASC';
-export const DUE_DATE_DESC = 'DUE_DATE_DESC';
-export const LABEL_PRIORITY_ASC = 'LABEL_PRIORITY_ASC';
-export const LABEL_PRIORITY_DESC = 'LABEL_PRIORITY_DESC';
-export const MILESTONE_DUE_ASC = 'MILESTONE_DUE_ASC';
-export const MILESTONE_DUE_DESC = 'MILESTONE_DUE_DESC';
-export const POPULARITY_ASC = 'POPULARITY_ASC';
-export const POPULARITY_DESC = 'POPULARITY_DESC';
-export const PRIORITY_ASC = 'PRIORITY_ASC';
-export const PRIORITY_DESC = 'PRIORITY_DESC';
-export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
-export const TITLE_ASC = 'TITLE_ASC';
-export const TITLE_DESC = 'TITLE_DESC';
-export const UPDATED_ASC = 'UPDATED_ASC';
-export const UPDATED_DESC = 'UPDATED_DESC';
-export const WEIGHT_ASC = 'WEIGHT_ASC';
-export const WEIGHT_DESC = 'WEIGHT_DESC';
-export const CLOSED_ASC = 'CLOSED_AT_ASC';
-export const CLOSED_DESC = 'CLOSED_AT_DESC';
-
export const urlSortParams = {
[PRIORITY_ASC]: 'priority',
[PRIORITY_DESC]: 'priority_desc',
@@ -115,8 +123,8 @@ export const urlSortParams = {
[CREATED_DESC]: 'created_date',
[UPDATED_ASC]: 'updated_asc',
[UPDATED_DESC]: 'updated_desc',
- [CLOSED_ASC]: 'closed_asc',
- [CLOSED_DESC]: 'closed_desc',
+ [CLOSED_AT_ASC]: 'closed_at',
+ [CLOSED_AT_DESC]: 'closed_at_desc',
[MILESTONE_DUE_ASC]: 'milestone',
[MILESTONE_DUE_DESC]: 'milestone_due_desc',
[DUE_DATE_ASC]: 'due_date',
@@ -126,20 +134,16 @@ export const urlSortParams = {
[LABEL_PRIORITY_ASC]: 'label_priority',
[LABEL_PRIORITY_DESC]: 'label_priority_desc',
[RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
+ [TITLE_ASC]: 'title_asc',
+ [TITLE_DESC]: 'title_desc',
+ [HEALTH_STATUS_ASC]: 'health_status_asc',
+ [HEALTH_STATUS_DESC]: 'health_status_desc',
[WEIGHT_ASC]: 'weight',
[WEIGHT_DESC]: 'weight_desc',
[BLOCKING_ISSUES_ASC]: 'blocking_issues_asc',
[BLOCKING_ISSUES_DESC]: 'blocking_issues_desc',
- [TITLE_ASC]: 'title_asc',
- [TITLE_DESC]: 'title_desc',
};
-export const API_PARAM = 'apiParam';
-export const URL_PARAM = 'urlParam';
-export const NORMAL_FILTER = 'normalFilter';
-export const SPECIAL_FILTER = 'specialFilter';
-export const ALTERNATIVE_FILTER = 'alternativeFilter';
-
export const specialFilterValues = [
FILTER_NONE,
FILTER_ANY,
@@ -148,7 +152,17 @@ export const specialFilterValues = [
FILTER_STARTED,
];
-export const TYPE_TOKEN_TASK_OPTION = { icon: 'issue-type-task', title: 'task', value: 'task' };
+export const TYPE_TOKEN_OBJECTIVE_OPTION = {
+ icon: 'issue-type-objective',
+ title: 'objective',
+ value: 'objective',
+};
+
+export const TYPE_TOKEN_KEY_RESULT_OPTION = {
+ icon: 'issue-type-key-result',
+ title: 'key_result',
+ value: 'key_result',
+};
// This should be consistent with Issue::TYPES_FOR_LIST in the backend
// https://gitlab.com/gitlab-org/gitlab/-/blob/1379c2d7bffe2a8d809f23ac5ef9b4114f789c07/app/models/issue.rb#L48
@@ -163,20 +177,35 @@ export const defaultTypeTokenOptions = [
{ icon: 'issue-type-issue', title: 'issue', value: 'issue' },
{ icon: 'issue-type-incident', title: 'incident', value: 'incident' },
{ icon: 'issue-type-test-case', title: 'test_case', value: 'test_case' },
+ { icon: 'issue-type-task', title: 'task', value: 'task' },
];
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: {
[NORMAL_FILTER]: 'authorUsername',
+ [ALTERNATIVE_FILTER]: 'authorUsernames',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'author_username',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[author_username]',
},
+ [OPERATOR_OR]: {
+ [ALTERNATIVE_FILTER]: 'or[author_username]',
+ },
+ },
+ },
+ [TOKEN_TYPE_SEARCH_WITHIN]: {
+ [API_PARAM]: {
+ [NORMAL_FILTER]: 'in',
+ },
+ [URL_PARAM]: {
+ [OPERATOR_IS]: {
+ [NORMAL_FILTER]: 'in',
+ },
},
},
[TOKEN_TYPE_ASSIGNEE]: {
@@ -190,7 +219,7 @@ export const filters = {
[SPECIAL_FILTER]: 'assignee_id',
[ALTERNATIVE_FILTER]: 'assignee_username',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[assignee_username][]',
},
[OPERATOR_OR]: {
@@ -208,7 +237,7 @@ export const filters = {
[NORMAL_FILTER]: 'milestone_title',
[SPECIAL_FILTER]: 'milestone_title',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[milestone_title]',
[SPECIAL_FILTER]: 'not[milestone_title]',
},
@@ -225,7 +254,7 @@ export const filters = {
[SPECIAL_FILTER]: 'label_name[]',
[ALTERNATIVE_FILTER]: 'label_name',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
},
},
@@ -238,7 +267,7 @@ export const filters = {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'type[]',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[type][]',
},
},
@@ -253,7 +282,7 @@ export const filters = {
[NORMAL_FILTER]: 'release_tag',
[SPECIAL_FILTER]: 'release_tag',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[release_tag]',
},
},
@@ -268,7 +297,7 @@ export const filters = {
[NORMAL_FILTER]: 'my_reaction_emoji',
[SPECIAL_FILTER]: 'my_reaction_emoji',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[my_reaction_emoji]',
},
},
@@ -293,7 +322,7 @@ export const filters = {
[NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[iteration_id]',
[SPECIAL_FILTER]: 'not[iteration_id]',
},
@@ -309,7 +338,7 @@ export const filters = {
[NORMAL_FILTER]: 'epic_id',
[SPECIAL_FILTER]: 'epic_id',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[epic_id]',
},
},
@@ -324,7 +353,7 @@ export const filters = {
[NORMAL_FILTER]: 'weight',
[SPECIAL_FILTER]: 'weight',
},
- [OPERATOR_IS_NOT]: {
+ [OPERATOR_NOT]: {
[NORMAL_FILTER]: 'not[weight]',
},
},
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 5e04dd1971c..7b68b7432c9 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -2,16 +2,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
-import createDefaultClient from '~/lib/graphql';
import { parseBoolean } from '~/lib/utils/common_utils';
-import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
+import JiraIssuesImportStatusApp from './components/jira_issues_import_status_app.vue';
import { gqlClient } from './graphql';
export function mountJiraIssuesListApp() {
- const el = document.querySelector('.js-jira-issues-import-status');
+ const el = document.querySelector('.js-jira-issues-import-status-root');
if (!el) {
- return false;
+ return null;
}
const { issuesPath, projectPath } = el.dataset;
@@ -19,21 +18,19 @@ export function mountJiraIssuesListApp() {
const isJiraConfigured = parseBoolean(el.dataset.isJiraConfigured);
if (!isJiraConfigured || !canEdit) {
- return false;
+ return null;
}
Vue.use(VueApollo);
- const defaultClient = createDefaultClient();
- const apolloProvider = new VueApollo({
- defaultClient,
- });
return new Vue({
el,
name: 'JiraIssuesImportStatusRoot',
- apolloProvider,
+ apolloProvider: new VueApollo({
+ defaultClient: gqlClient,
+ }),
render(createComponent) {
- return createComponent(JiraIssuesImportStatusRoot, {
+ return createComponent(JiraIssuesImportStatusApp, {
props: {
canEdit,
isJiraConfigured,
@@ -46,10 +43,10 @@ export function mountJiraIssuesListApp() {
}
export function mountIssuesListApp() {
- const el = document.querySelector('.js-issues-list');
+ const el = document.querySelector('.js-issues-list-root');
if (!el) {
- return false;
+ return null;
}
Vue.use(VueApollo);
@@ -77,6 +74,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature,
hasIterationsFeature,
hasScopedLabelsFeature,
+ hasOkrsFeature,
importCsvIssuesPath,
initialEmail,
initialSort,
@@ -127,6 +125,7 @@ export function mountIssuesListApp() {
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
+ hasOkrsFeature: parseBoolean(hasOkrsFeature),
initialSort,
isAnonymousSearchDisabled: parseBoolean(isAnonymousSearchDisabled),
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
diff --git a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
index b447289b425..ee97fb6edca 100644
--- a/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
@@ -10,6 +10,7 @@ query getIssues(
$search: String
$sort: IssueSort
$state: IssuableState
+ $in: [IssuableSearchableField!]
$assigneeId: String
$assigneeUsernames: [String!]
$authorUsername: String
@@ -38,6 +39,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
@@ -72,6 +74,7 @@ query getIssues(
search: $search
sort: $sort
state: $state
+ in: $in
assigneeId: $assigneeId
assigneeUsernames: $assigneeUsernames
authorUsername: $authorUsername
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 2f9ab9d62ee..b566e08731c 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -4,9 +4,10 @@ import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
FILTERED_SEARCH_TERM,
- OPERATOR_IS_NOT,
+ OPERATOR_NOT,
OPERATOR_OR,
TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_MILESTONE,
@@ -14,14 +15,19 @@ import {
TOKEN_TYPE_TYPE,
} from '~/vue_shared/components/filtered_search_bar/constants';
import {
+ ALTERNATIVE_FILTER,
API_PARAM,
BLOCKING_ISSUES_ASC,
BLOCKING_ISSUES_DESC,
+ CLOSED_AT_ASC,
+ CLOSED_AT_DESC,
CREATED_ASC,
CREATED_DESC,
DUE_DATE_ASC,
DUE_DATE_DESC,
filters,
+ HEALTH_STATUS_ASC,
+ HEALTH_STATUS_DESC,
LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
@@ -44,8 +50,6 @@ import {
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
- CLOSED_ASC,
- CLOSED_DESC,
} from './constants';
export const getInitialPageParams = (
@@ -66,7 +70,11 @@ export const getSortKey = (sort) =>
export const isSortKey = (sort) => Object.keys(urlSortParams).includes(sort);
-export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature) => {
+export const getSortOptions = ({
+ hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature,
+}) => {
const sortOptions = [
{
id: 1,
@@ -96,8 +104,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 4,
title: __('Closed date'),
sortDirection: {
- ascending: CLOSED_ASC,
- descending: CLOSED_DESC,
+ ascending: CLOSED_AT_ASC,
+ descending: CLOSED_AT_DESC,
},
},
{
@@ -150,6 +158,17 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
];
+ if (hasIssuableHealthStatusFeature) {
+ sortOptions.push({
+ id: sortOptions.length + 1,
+ title: __('Health'),
+ sortDirection: {
+ ascending: HEALTH_STATUS_ASC,
+ descending: HEALTH_STATUS_DESC,
+ },
+ });
+ }
+
if (hasIssueWeightsFeature) {
sortOptions.push({
id: sortOptions.length + 1,
@@ -223,13 +242,24 @@ export const getFilterTokens = (locationSearch) => {
return tokens.length ? tokens : [createTerm()];
};
-const getFilterType = (data, tokenType = '') => {
+const isSpecialFilter = (type, data) => {
const isAssigneeIdParam =
- tokenType === TOKEN_TYPE_ASSIGNEE &&
+ type === TOKEN_TYPE_ASSIGNEE &&
isPositiveInteger(data) &&
getParameterByName(PARAM_ASSIGNEE_ID) === data;
+ return specialFilterValues.includes(data) || isAssigneeIdParam;
+};
+
+const getFilterType = ({ type, value: { data, operator } }) => {
+ const isUnionedAuthor = type === TOKEN_TYPE_AUTHOR && operator === OPERATOR_OR;
- return specialFilterValues.includes(data) || isAssigneeIdParam ? SPECIAL_FILTER : NORMAL_FILTER;
+ if (isUnionedAuthor) {
+ return ALTERNATIVE_FILTER;
+ }
+ if (isSpecialFilter(type, data)) {
+ return SPECIAL_FILTER;
+ }
+ return NORMAL_FILTER;
};
const wildcardTokens = [TOKEN_TYPE_ITERATION, TOKEN_TYPE_MILESTONE, TOKEN_TYPE_RELEASE];
@@ -258,10 +288,10 @@ export const convertToApiParams = (filterTokens) => {
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.forEach((token) => {
- const filterType = getFilterType(token.value.data, token.type);
- const field = filters[token.type][API_PARAM][filterType];
+ const filterType = getFilterType(token);
+ const apiField = filters[token.type][API_PARAM][filterType];
let obj;
- if (token.value.operator === OPERATOR_IS_NOT) {
+ if (token.value.operator === OPERATOR_NOT) {
obj = not;
} else if (token.value.operator === OPERATOR_OR) {
obj = or;
@@ -270,7 +300,7 @@ export const convertToApiParams = (filterTokens) => {
}
const data = formatData(token);
Object.assign(obj, {
- [field]: obj[field] ? [obj[field], data].flat() : data,
+ [apiField]: obj[apiField] ? [obj[apiField], data].flat() : data,
});
});
@@ -289,10 +319,10 @@ export const convertToUrlParams = (filterTokens) =>
filterTokens
.filter((token) => token.type !== FILTERED_SEARCH_TERM)
.reduce((acc, token) => {
- const filterType = getFilterType(token.value.data, token.type);
- const param = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
+ const filterType = getFilterType(token);
+ const urlParam = filters[token.type][URL_PARAM][token.value.operator]?.[filterType];
return Object.assign(acc, {
- [param]: acc[param] ? [acc[param], token.value.data].flat() : token.value.data,
+ [urlParam]: acc[urlParam] ? [acc[urlParam], token.value.data].flat() : token.value.data,
});
}, {});
@@ -300,4 +330,4 @@ export const convertToSearchQuery = (filterTokens) =>
filterTokens
.filter((token) => token.type === FILTERED_SEARCH_TERM && token.value.data)
.map((token) => token.value.data)
- .join(' ');
+ .join(' ') || undefined;
diff --git a/app/assets/javascripts/issues/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index bc1cffef943..1bb53dfd50d 100644
--- a/app/assets/javascripts/issues/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
@@ -1,5 +1,5 @@
import Sortable from 'sortablejs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
import { getSortableDefaultOptions, sortableStart } from '~/sortable/utils';
@@ -11,7 +11,7 @@ const updateIssue = (url, { move_before_id, move_after_id }) =>
move_after_id,
})
.catch(() => {
- createFlash({
+ createAlert({
message: s__("ManualOrdering|Couldn't save the order of the issues"),
});
});
diff --git a/app/assets/javascripts/issues/related_merge_requests/store/actions.js b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
index 94abb50de89..4c81f1d9bc1 100644
--- a/app/assets/javascripts/issues/related_merge_requests/store/actions.js
+++ b/app/assets/javascripts/issues/related_merge_requests/store/actions.js
@@ -1,4 +1,4 @@
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { normalizeHeaders } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -29,7 +29,7 @@ export const fetchMergeRequests = ({ state, dispatch }) => {
})
.catch(() => {
dispatch('receiveDataError');
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching related merge requests.'),
});
});
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 0daf77e03dc..e5428f87095 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlBadge, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import {
IssuableStatus,
IssuableStatusText,
@@ -327,7 +327,7 @@ export default {
this.store.updateState(data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: this.defaultErrorMessage,
});
});
@@ -362,7 +362,7 @@ export default {
this.updateAndShowForm(res.data);
})
.catch(() => {
- createFlash({
+ createAlert({
message: this.defaultErrorMessage,
});
this.updateAndShowForm();
@@ -429,7 +429,7 @@ export default {
errMsg += `. ${message}`;
}
- this.flashContainer = createFlash({
+ this.flashContainer = createAlert({
message: errMsg,
});
})
diff --git a/app/assets/javascripts/issues/show/components/description.vue b/app/assets/javascripts/issues/show/components/description.vue
index 5c2a154362f..78e729b97da 100644
--- a/app/assets/javascripts/issues/show/components/description.vue
+++ b/app/assets/javascripts/issues/show/components/description.vue
@@ -1,11 +1,12 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
+import { GlToast, GlTooltip, GlModalDirective } from '@gitlab/ui';
import $ from 'jquery';
import Sortable from 'sortablejs';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isMetaKey } from '~/lib/utils/common_utils';
import { isPositiveInteger } from '~/lib/utils/number_utils';
@@ -27,6 +28,7 @@ import {
TASK_TYPE_NAME,
WIDGET_TYPE_DESCRIPTION,
} from '~/work_items/constants';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import animateMixin from '../mixins/animate';
import { convertDescriptionWithNewSort } from '../utils';
@@ -165,7 +167,7 @@ export default {
this.renderGFM();
this.updateTaskStatusText();
- if (this.workItemId) {
+ if (this.workItemId && this.workItemsEnabled) {
const taskLink = this.$el.querySelector(
`.gfm-issue[data-issue="${getIdFromGraphQLId(this.workItemId)}"]`,
);
@@ -177,7 +179,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
if (this.canUpdate) {
// eslint-disable-next-line no-new
@@ -283,7 +285,7 @@ export default {
},
taskListUpdateError() {
- createFlash({
+ createAlert({
message: sprintf(
__(
'Someone edited this %{issueType} at the same time you did. The description has been updated and you will need to make your changes again.',
@@ -467,7 +469,7 @@ export default {
this.workItemId = newWorkItem.id;
this.openWorkItemDetailModal(el);
} catch (error) {
- createFlash({
+ createAlert({
message: sprintfWorkItem(I18N_WORK_ITEM_ERROR_CREATING, workItemTypes.TASK),
error,
captureError: true,
diff --git a/app/assets/javascripts/issues/show/components/fields/description.vue b/app/assets/javascripts/issues/show/components/fields/description.vue
index 180dea77003..04c5007dbec 100644
--- a/app/assets/javascripts/issues/show/components/fields/description.vue
+++ b/app/assets/javascripts/issues/show/components/fields/description.vue
@@ -67,6 +67,7 @@ export default {
:quick-actions-docs-path="quickActionsDocsPath"
:enable-autocomplete="enableAutocomplete"
supports-quick-actions
+ use-bottom-toolbar
autofocus
@input="$emit('input', $event)"
@keydown.meta.enter="updateIssuable"
diff --git a/app/assets/javascripts/issues/show/components/form.vue b/app/assets/javascripts/issues/show/components/form.vue
index 0c6b61fb893..b56c91d7983 100644
--- a/app/assets/javascripts/issues/show/components/form.vue
+++ b/app/assets/javascripts/issues/show/components/form.vue
@@ -164,7 +164,7 @@ export default {
<template>
<form data-testid="issuable-form">
- <locked-warning v-if="showLockedWarning" />
+ <locked-warning v-if="showLockedWarning" :issuable-type="issuableType" />
<gl-alert
v-if="showOutdatedDescriptionWarning"
class="gl-mb-5"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index c01de63ced9..983e2e6530e 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -10,7 +10,7 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import createFlash, { FLASH_TYPES } from '~/flash';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import { IssuableStatus, IssueType } from '~/issues/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
@@ -40,6 +40,7 @@ export default {
promoteSuccessMessage: __(
'The issue was successfully promoted to an epic. Redirecting to epic...',
),
+ reportAbuse: __('Report abuse to administrator'),
},
components: {
DeleteIssueModal,
@@ -191,7 +192,7 @@ export default {
// Dispatch event which updates open/close state, shared among the issue show page
document.dispatchEvent(new CustomEvent(EVENT_ISSUABLE_VUE_APP_CHANGE, payload));
})
- .catch(() => createFlash({ message: __('Error occurred while updating the issue status') }))
+ .catch(() => createAlert({ message: __('Error occurred while updating the issue status') }))
.finally(() => {
this.toggleStateButtonLoading(false);
});
@@ -214,14 +215,14 @@ export default {
throw new Error();
}
- createFlash({
+ createAlert({
message: this.$options.i18n.promoteSuccessMessage,
- type: FLASH_TYPES.SUCCESS,
+ variant: VARIANT_SUCCESS,
});
visitUrl(data.promoteToEpic.epic.webPath);
})
- .catch(() => createFlash({ message: this.$options.i18n.promoteErrorMessage }))
+ .catch(() => createAlert({ message: this.$options.i18n.promoteErrorMessage }))
.finally(() => {
this.toggleStateButtonLoading(false);
});
@@ -255,7 +256,7 @@ export default {
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
- {{ __('Report abuse') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
@@ -314,7 +315,7 @@ export default {
{{ __('Promote to epic') }}
</gl-dropdown-item>
<gl-dropdown-item v-if="!isIssueAuthor" :href="reportAbusePath">
- {{ __('Report abuse') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="canReportSpam"
diff --git a/app/assets/javascripts/issues/show/components/incidents/constants.js b/app/assets/javascripts/issues/show/components/incidents/constants.js
index db846009409..22db19610c1 100644
--- a/app/assets/javascripts/issues/show/components/incidents/constants.js
+++ b/app/assets/javascripts/issues/show/components/incidents/constants.js
@@ -14,6 +14,7 @@ export const timelineFormI18n = Object.freeze({
areaPlaceholder: s__('Incident|Timeline text...'),
save: __('Save'),
cancel: __('Cancel'),
+ delete: __('Delete'),
description: __('Description'),
hint: __('You can enter up to 280 characters'),
textRemaining: (count) => n__('%d character remaining', '%d characters remaining', count),
diff --git a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
index 60fa8cb949b..8cdd62ca9ef 100644
--- a/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/edit_timeline_event.vue
@@ -40,8 +40,10 @@ export default {
:is-event-processed="editTimelineEventActive"
:previous-occurred-at="event.occurredAt"
:previous-note="event.note"
+ show-delete
@save-event="saveEvent"
@cancel="$emit('hide-edit')"
+ @delete="$emit('delete')"
/>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
index f1fc27dcb2a..4a8786b04b1 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/create_timeline_event.mutation.graphql
@@ -7,6 +7,12 @@ mutation CreateTimelineEvent($input: TimelineEventCreateInput!) {
action
occurredAt
createdAt
+ timelineEventTags {
+ nodes {
+ id
+ name
+ }
+ }
}
errors
}
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
index d88633f2ae9..e057267b006 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_alert.graphql
@@ -4,6 +4,7 @@ query getAlert($iid: String!, $fullPath: ID!) {
issue(iid: $iid) {
id
alertManagementAlert {
+ id
iid
title
detailsUrl
diff --git a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
index bc4e8414bfc..baeb81745ab 100644
--- a/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
+++ b/app/assets/javascripts/issues/show/components/incidents/graphql/queries/get_timeline_events.query.graphql
@@ -9,6 +9,12 @@ query GetTimelineEvents($fullPath: ID!, $incidentId: IssueID!) {
action
occurredAt
createdAt
+ timelineEventTags {
+ nodes {
+ id
+ name
+ }
+ }
}
}
}
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 5725d0f8d6a..53956fcb4b2 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -1,16 +1,29 @@
<script>
import { GlTab, GlTabs } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DescriptionComponent from '../description.vue';
import getAlert from './graphql/queries/get_alert.graphql';
import HighlightBar from './highlight_bar.vue';
import TimelineTab from './timeline_events_tab.vue';
+export const incidentTabsI18n = Object.freeze({
+ summaryTitle: s__('Incident|Summary'),
+ metricsTitle: s__('Incident|Metrics'),
+ alertsTitle: s__('Incident|Alert details'),
+ timelineTitle: s__('Incident|Timeline'),
+});
+
+export const TAB_NAMES = Object.freeze({
+ SUMMARY: '',
+ ALERTS: 'alerts',
+ METRICS: 'metrics',
+ TIMELINE: 'timeline',
+});
+
export default {
components: {
AlertDetailsTable,
@@ -22,8 +35,8 @@ export default {
IncidentMetricTab: () =>
import('ee_component/issues/show/components/incidents/incident_metric_tab.vue'),
},
- mixins: [glFeatureFlagsMixin()],
- inject: ['fullPath', 'iid'],
+ inject: ['fullPath', 'iid', 'uploadMetricsFeatureAvailable'],
+ i18n: incidentTabsI18n,
apollo: {
alert: {
query: getAlert,
@@ -37,7 +50,7 @@ export default {
return data?.project?.issue?.alertManagementAlert;
},
error() {
- createFlash({
+ createAlert({
message: s__('Incident|There was an issue loading alert data. Please try again.'),
});
},
@@ -46,12 +59,44 @@ export default {
data() {
return {
alert: null,
+ activeTabIndex: 0,
};
},
computed: {
loading() {
return this.$apollo.queries.alert.loading;
},
+ tabMapping() {
+ const availableTabs = [TAB_NAMES.SUMMARY];
+
+ if (this.uploadMetricsFeatureAvailable) {
+ availableTabs.push(TAB_NAMES.METRICS);
+ }
+ if (this.alert) {
+ availableTabs.push(TAB_NAMES.ALERTS);
+ }
+
+ availableTabs.push(TAB_NAMES.TIMELINE);
+
+ const tabNamesToIndex = {};
+ const tabIndexToName = {};
+
+ availableTabs.forEach((item, index) => {
+ tabNamesToIndex[item] = index;
+ tabIndexToName[index] = item;
+ });
+
+ return { tabNamesToIndex, tabIndexToName };
+ },
+ currentTabIndex: {
+ get() {
+ return this.activeTabIndex;
+ },
+ set(index) {
+ this.handleTabChange(index);
+ this.activeTabIndex = index;
+ },
+ },
},
mounted() {
this.trackPageViews();
@@ -91,25 +136,33 @@ export default {
<template>
<div>
<gl-tabs
+ v-model="currentTabIndex"
content-class="gl-reset-line-height"
class="gl-mt-n3"
data-testid="incident-tabs"
- @input="handleTabChange"
>
- <gl-tab :title="s__('Incident|Summary')">
+ <gl-tab :title="$options.i18n.summaryTitle" data-testid="summary-tab">
<highlight-bar :alert="alert" />
<description-component v-bind="$attrs" v-on="$listeners" />
</gl-tab>
- <incident-metric-tab />
+ <gl-tab
+ v-if="uploadMetricsFeatureAvailable"
+ :title="$options.i18n.metricsTitle"
+ data-testid="metrics-tab"
+ >
+ <incident-metric-tab />
+ </gl-tab>
<gl-tab
v-if="alert"
class="alert-management-details"
- :title="s__('Incident|Alert details')"
+ :title="$options.i18n.alertsTitle"
data-testid="alert-details-tab"
>
<alert-details-table :alert="alert" :loading="loading" />
</gl-tab>
- <timeline-tab />
+ <gl-tab :title="$options.i18n.timelineTitle" data-testid="timeline-tab">
+ <timeline-tab />
+ </gl-tab>
</gl-tabs>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
index 72dfccca467..f1a3aebc990 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_form.vue
@@ -1,7 +1,6 @@
<script>
import { GlDatepicker, GlFormInput, GlFormGroup, GlButton } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
import { MAX_TEXT_LENGTH, timelineFormI18n } from './constants';
import { getUtcShiftedDate } from './utils';
@@ -27,15 +26,17 @@ export default {
},
i18n: timelineFormI18n,
MAX_TEXT_LENGTH,
- directives: {
- autofocusonshow,
- },
props: {
showSaveAndAdd: {
type: Boolean,
required: false,
default: false,
},
+ showDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
isEventProcessed: {
type: Boolean,
required: true,
@@ -97,7 +98,7 @@ export default {
this.timelineText = '';
},
focusDate() {
- this.$refs.datepicker.$el.querySelector('input').focus();
+ this.$refs.datepicker.$el.querySelector('input')?.focus();
},
handleSave(addAnotherEvent) {
const event = {
@@ -185,32 +186,42 @@ export default {
</gl-form-group>
</div>
<gl-form-group class="gl-mb-0">
- <gl-button
- variant="confirm"
- category="primary"
- class="gl-mr-3"
- data-testid="save-button"
- :disabled="!isTimelineTextValid"
- :loading="isEventProcessed"
- @click="handleSave(false)"
- >
- {{ $options.i18n.save }}
- </gl-button>
- <gl-button
- v-if="showSaveAndAdd"
- variant="confirm"
- category="secondary"
- class="gl-mr-3 gl-ml-n2"
- data-testid="save-and-add-button"
- :disabled="!isTimelineTextValid"
- :loading="isEventProcessed"
- @click="handleSave(true)"
- >
- {{ $options.i18n.saveAndAdd }}
- </gl-button>
- <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
- {{ $options.i18n.cancel }}
- </gl-button>
+ <div class="gl-display-flex">
+ <gl-button
+ variant="confirm"
+ category="primary"
+ class="gl-mr-3"
+ data-testid="save-button"
+ :disabled="!isTimelineTextValid"
+ :loading="isEventProcessed"
+ @click="handleSave(false)"
+ >
+ {{ $options.i18n.save }}
+ </gl-button>
+ <gl-button
+ v-if="showSaveAndAdd"
+ variant="confirm"
+ category="secondary"
+ class="gl-mr-3 gl-ml-n2"
+ data-testid="save-and-add-button"
+ :disabled="!isTimelineTextValid"
+ :loading="isEventProcessed"
+ @click="handleSave(true)"
+ >
+ {{ $options.i18n.saveAndAdd }}
+ </gl-button>
+ <gl-button class="gl-ml-n2" :disabled="isEventProcessed" @click="$emit('cancel')">
+ {{ $options.i18n.cancel }}
+ </gl-button>
+ <gl-button
+ v-if="showDelete"
+ class="gl-ml-auto btn-danger"
+ :disabled="isEventProcessed"
+ @click="$emit('delete')"
+ >
+ {{ $options.i18n.delete }}
+ </gl-button>
+ </div>
<div class="timeline-event-bottom-border"></div>
</gl-form-group>
</form>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
index cbf3c387fa3..90ee4351e39 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlSafeHtmlDirective, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlSprintf, GlBadge } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { formatDate } from '~/lib/utils/datetime_utility';
import { timelineItemI18n } from './constants';
import { getEventIcon } from './utils';
@@ -12,9 +13,10 @@ export default {
GlDropdownItem,
GlIcon,
GlSprintf,
+ GlBadge,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
inject: ['canUpdateTimelineEvent'],
props: {
@@ -30,6 +32,11 @@ export default {
type: String,
required: true,
},
+ eventTag: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
computed: {
time() {
@@ -42,41 +49,41 @@ export default {
};
</script>
<template>
- <div class="gl-display-flex gl-align-items-start">
+ <div class="timeline-event gl-display-grid">
<div
class="gl-display-flex gl-align-items-center gl-justify-content-center gl-bg-white gl-text-gray-200 gl-border-gray-100 gl-border-1 gl-border-solid gl-rounded-full gl-mt-2 gl-mr-3 gl-w-8 gl-h-8 gl-p-3 gl-z-index-1"
>
<gl-icon :name="getEventIcon(action)" class="note-icon" />
</div>
- <div
- class="timeline-event-note timeline-event-border gl-w-full gl-display-flex gl-flex-direction-row"
- data-testid="event-text-container"
- >
- <div>
+ <div class="timeline-event-note timeline-event-border" data-testid="event-text-container">
+ <div class="gl-display-flex gl-align-items-center gl-mb-3">
<strong class="gl-font-lg" data-testid="event-time">
<gl-sprintf :message="$options.i18n.timeUTC">
<template #time>{{ time }}</template>
</gl-sprintf>
</strong>
- <div v-safe-html="noteHtml"></div>
+ <gl-badge v-if="eventTag" variant="muted" icon="tag" class="gl-ml-3">
+ {{ eventTag }}
+ </gl-badge>
</div>
- <gl-dropdown
- v-if="canUpdateTimelineEvent"
- right
- class="event-note-actions gl-ml-auto gl-align-self-start"
- icon="ellipsis_v"
- text-sr-only
- :text="$options.i18n.moreActions"
- category="tertiary"
- no-caret
- >
- <gl-dropdown-item @click="$emit('edit')">
- {{ $options.i18n.edit }}
- </gl-dropdown-item>
- <gl-dropdown-item @click="$emit('delete')">
- {{ $options.i18n.delete }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <div v-safe-html="noteHtml" class="md"></div>
</div>
+ <gl-dropdown
+ v-if="canUpdateTimelineEvent"
+ right
+ class="event-note-actions gl-ml-auto gl-align-self-start"
+ icon="ellipsis_v"
+ text-sr-only
+ :text="$options.i18n.moreActions"
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item @click="$emit('edit')">
+ {{ $options.i18n.edit }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('delete')">
+ {{ $options.i18n.delete }}
+ </gl-dropdown-item>
+ </gl-dropdown>
</div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
index 321b7ccc14a..c6b93201c97 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_list.vue
@@ -50,6 +50,9 @@ export default {
},
},
methods: {
+ getFirstTag(eventTag) {
+ return eventTag.nodes?.[0]?.name;
+ },
handleEditSelection(event) {
this.eventToEdit = event.id;
this.$emit('hide-new-incident-timeline-event-form');
@@ -153,6 +156,7 @@ export default {
:edit-timeline-event-active="editTimelineEventActive"
@handle-save-edit="handleSaveEdit"
@hide-edit="hideEdit()"
+ @delete="handleDelete(event)"
/>
<incident-timeline-event-item
v-else
@@ -160,6 +164,7 @@ export default {
:action="event.action"
:occurred-at="event.occurredAt"
:note-html="event.noteHtml"
+ :event-tag="getFirstTag(event.timelineEventTags)"
@delete="handleDelete(event)"
@edit="handleEditSelection(event)"
/>
diff --git a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
index 5f70d9acac9..c8237766505 100644
--- a/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/timeline_events_tab.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlEmptyState, GlLoadingIcon, GlTab } from '@gitlab/ui';
+import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { fetchPolicies } from '~/lib/graphql';
@@ -15,7 +15,6 @@ export default {
GlButton,
GlEmptyState,
GlLoadingIcon,
- GlTab,
CreateTimelineEvent,
IncidentTimelineEventsList,
},
@@ -77,7 +76,7 @@ export default {
</script>
<template>
- <gl-tab :title="$options.i18n.title">
+ <div>
<gl-loading-icon v-if="timelineEventLoading" size="lg" color="dark" class="gl-mt-5" />
<gl-empty-state
v-else-if="showEmptyState"
@@ -106,5 +105,5 @@ export default {
>
{{ $options.i18n.addEventButton }}
</gl-button>
- </gl-tab>
+ </div>
</template>
diff --git a/app/assets/javascripts/issues/show/components/locked_warning.vue b/app/assets/javascripts/issues/show/components/locked_warning.vue
index 12feacb027b..4414e693ed0 100644
--- a/app/assets/javascripts/issues/show/components/locked_warning.vue
+++ b/app/assets/javascripts/issues/show/components/locked_warning.vue
@@ -1,29 +1,44 @@
<script>
import { GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
+import { IssuableType } from '~/issues/constants';
-const alertMessage = __(
- 'Someone edited the issue at the same time you did. Please check out %{linkStart}the issue%{linkEnd} and make sure your changes will not unintentionally remove theirs.',
-);
+export const i18n = Object.freeze({
+ alertMessage: __(
+ "Someone edited the %{issuableType} at the same time you did. Review %{linkStart}the %{issuableType}%{linkEnd} and make sure you don't unintentionally overwrite their changes.",
+ ),
+});
export default {
- alertMessage,
components: {
GlSprintf,
GlLink,
GlAlert,
},
+ props: {
+ issuableType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return Object.values(IssuableType).includes(value);
+ },
+ },
+ },
computed: {
currentPath() {
return window.location.pathname;
},
+ alertMessage() {
+ return sprintf(this.$options.i18n.alertMessage, { issuableType: this.issuableType });
+ },
},
+ i18n,
};
</script>
<template>
<gl-alert variant="danger" class="gl-mb-5" :dismissible="false">
- <gl-sprintf :message="$options.alertMessage">
+ <gl-sprintf :message="alertMessage">
<template #link="{ content }">
<gl-link :href="currentPath" target="_blank" rel="nofollow">
{{ content }}
diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue
index 307d9f9f69a..6978f730e1d 100644
--- a/app/assets/javascripts/issues/show/components/title.vue
+++ b/app/assets/javascripts/issues/show/components/title.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlTooltipDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import eventHub from '../event_hub';
import animateMixin from '../mixins/animate';
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 0e2d8821f36..dac807dceb0 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
@@ -1,5 +1,6 @@
<script>
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
import { __ } from '~/locale';
import { BRANCHES_PER_PAGE } from '../constants';
import getProjectQuery from '../graphql/queries/get_project.query.graphql';
@@ -7,10 +8,7 @@ import getProjectQuery from '../graphql/queries/get_project.query.graphql';
export default {
BRANCHES_PER_PAGE,
components: {
- GlDropdown,
- GlDropdownItem,
- GlSearchBoxByType,
- GlLoadingIcon,
+ GlCollapsibleListbox,
},
props: {
selectedProject: {
@@ -26,7 +24,6 @@ export default {
},
data() {
return {
- sourceBranchSearchQuery: '',
initialSourceBranchNamesLoading: false,
sourceBranchNamesLoading: false,
sourceBranchNames: [],
@@ -59,6 +56,9 @@ export default {
onSourceBranchSelect(branchName) {
this.$emit('change', branchName);
},
+ onSearch: debounce(function debouncedSearch(branchSearchQuery) {
+ this.onSourceBranchSearchQuery(branchSearchQuery);
+ }, 250),
onSourceBranchSearchQuery(branchSearchQuery) {
this.branchSearchQuery = branchSearchQuery;
this.fetchSourceBranchNames({
@@ -83,7 +83,10 @@ export default {
});
const { branchNames, rootRef } = data?.project.repository || {};
- this.sourceBranchNames = branchNames || [];
+ this.sourceBranchNames =
+ branchNames.map((value) => {
+ return { text: value, value };
+ }) || [];
// Use root ref as the default selection
if (rootRef && !this.hasSelectedSourceBranch) {
@@ -102,33 +105,15 @@ export default {
</script>
<template>
- <gl-dropdown
- :text="branchDropdownText"
- :loading="initialSourceBranchNamesLoading"
- :disabled="!hasSelectedProject"
+ <gl-collapsible-listbox
:class="{ 'gl-font-monospace': hasSelectedSourceBranch }"
- >
- <template #header>
- <gl-search-box-by-type
- :debounce="250"
- :value="sourceBranchSearchQuery"
- @input="onSourceBranchSearchQuery"
- />
- </template>
-
- <gl-loading-icon v-show="sourceBranchNamesLoading" />
- <template v-if="!sourceBranchNamesLoading">
- <gl-dropdown-item
- v-for="branchName in sourceBranchNames"
- v-show="!sourceBranchNamesLoading"
- :key="branchName"
- :is-checked="branchName === selectedBranchName"
- is-check-item
- class="gl-font-monospace"
- @click="onSourceBranchSelect(branchName)"
- >
- {{ branchName }}
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
+ :disabled="!hasSelectedProject"
+ :items="sourceBranchNames"
+ :loading="initialSourceBranchNamesLoading"
+ :searchable="true"
+ :searching="sourceBranchNamesLoading"
+ :toggle-text="branchDropdownText"
+ @search="onSearch"
+ @select="onSourceBranchSelect"
+ />
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index fc365746b54..01bc5dfc66b 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -38,7 +38,7 @@ export const INTEGRATIONS_DOC_LINK = helpPagePath('integration/jira/development_
anchor: 'use-the-integration',
});
export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('integration/jira/connect-app', {
- anchor: 'install-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances',
+ anchor: 'connect-the-gitlabcom-for-jira-cloud-app-for-self-managed-instances',
});
export const GITLAB_COM_BASE_PATH = 'https://gitlab.com';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
index 5ff75e19425..7c6ff002014 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/index.vue
@@ -5,10 +5,14 @@ import { s__ } from '~/locale';
import { reloadPage, persistBaseUrl, retrieveBaseUrl } from '~/jira_connect/subscriptions/utils';
import { updateInstallation, setApiBaseURL } from '~/jira_connect/subscriptions/api';
-import { I18N_UPDATE_INSTALLATION_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
+import {
+ GITLAB_COM_BASE_PATH,
+ I18N_UPDATE_INSTALLATION_ERROR_MESSAGE,
+} from '~/jira_connect/subscriptions/constants';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import SignInOauthButton from '../../../components/sign_in_oauth_button.vue';
+import SetupInstructions from './setup_instructions.vue';
import VersionSelectForm from './version_select_form.vue';
export default {
@@ -16,12 +20,14 @@ export default {
components: {
GlButton,
SignInOauthButton,
+ SetupInstructions,
VersionSelectForm,
},
data() {
return {
gitlabBasePath: null,
loadingVersionSelect: false,
+ showSetupInstructions: false,
};
},
computed: {
@@ -37,6 +43,9 @@ export default {
mounted() {
this.gitlabBasePath = retrieveBaseUrl();
setApiBaseURL(this.gitlabBasePath);
+ if (this.gitlabBasePath !== GITLAB_COM_BASE_PATH) {
+ this.showSetupInstructions = true;
+ }
},
methods: {
...mapMutations({
@@ -61,6 +70,9 @@ export default {
this.loadingVersionSelect = false;
});
},
+ onSetupNext() {
+ this.showSetupInstructions = false;
+ },
onSignInError() {
this.$emit('error');
},
@@ -88,19 +100,23 @@ export default {
@submit="onVersionSelect"
/>
- <div v-else class="gl-text-center">
- <sign-in-oauth-button
- class="gl-mb-5"
- :gitlab-base-path="gitlabBasePath"
- @sign-in="$emit('sign-in-oauth', $event)"
- @error="onSignInError"
- />
+ <template v-else>
+ <setup-instructions v-if="showSetupInstructions" @next="onSetupNext" />
+
+ <div v-else class="gl-text-center">
+ <sign-in-oauth-button
+ class="gl-mb-5"
+ :gitlab-base-path="gitlabBasePath"
+ @sign-in="$emit('sign-in-oauth', $event)"
+ @error="onSignInError"
+ />
- <div>
- <gl-button category="tertiary" variant="confirm" @click="resetGitlabBasePath">
- {{ $options.i18n.changeVersionButtonText }}
- </gl-button>
+ <div>
+ <gl-button category="tertiary" variant="confirm" @click="resetGitlabBasePath">
+ {{ $options.i18n.changeVersionButtonText }}
+ </gl-button>
+ </div>
</div>
- </div>
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
new file mode 100644
index 00000000000..00fa739b518
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -0,0 +1,35 @@
+<script>
+import { GlButton, GlLink } from '@gitlab/ui';
+import { OAUTH_SELF_MANAGED_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+
+export default {
+ components: {
+ GlButton,
+ GlLink,
+ },
+ OAUTH_SELF_MANAGED_DOC_LINK,
+};
+</script>
+
+<template>
+ <div class="gl-max-w-62 gl-mx-auto gl-mt-7">
+ <h3>{{ s__('JiraService|Continue setup in GitLab') }}</h3>
+ <p>
+ {{
+ s__(
+ 'JiraService|In order to complete the set up, you’ll need to complete a few steps in GitLab.',
+ )
+ }}
+ <gl-link
+ class="gl-reset-font-size!"
+ :href="$options.OAUTH_SELF_MANAGED_DOC_LINK"
+ target="_blank"
+ >{{ __('Learn more') }}</gl-link
+ >
+ </p>
+
+ <gl-button variant="confirm" @click="$emit('next')">
+ {{ __('Next') }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
index 6b32225ed11..37a65946b3f 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
@@ -55,7 +55,6 @@ export default {
},
radioOptions: RADIO_OPTIONS,
i18n: {
- title: s__('JiraService|Welcome to GitLab for Jira'),
saasRadioLabel: __('GitLab.com (SaaS)'),
saasRadioHelp: __('Most common'),
selfManagedRadioLabel: __('GitLab (self-managed)'),
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
index e498a735898..67cdca6aa0a 100644
--- a/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
+++ b/app/assets/javascripts/jobs/components/filtered_search/jobs_filtered_search.vue
@@ -1,13 +1,13 @@
<script>
import { GlFilteredSearch } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+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 {
- tokenTypes: {
- status: 'status',
- },
components: {
GlFilteredSearch,
},
@@ -22,12 +22,12 @@ export default {
tokens() {
return [
{
- type: this.$options.tokenTypes.status,
+ type: TOKEN_TYPE_STATUS,
icon: 'status',
- title: s__('Jobs|Status'),
+ title: TOKEN_TITLE_STATUS,
unique: true,
token: JobStatusToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
];
},
@@ -35,7 +35,7 @@ export default {
if (this.queryString?.statuses) {
return [
{
- type: 'status',
+ type: TOKEN_TYPE_STATUS,
value: {
data: this.queryString?.statuses,
operator: '=',
diff --git a/app/assets/javascripts/jobs/components/job/empty_state.vue b/app/assets/javascripts/jobs/components/job/empty_state.vue
index 65b9600e664..d0a39025807 100644
--- a/app/assets/javascripts/jobs/components/job/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/job/empty_state.vue
@@ -1,16 +1,12 @@
<script>
import { GlLink } from '@gitlab/ui';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import LegacyManualVariablesForm from '~/jobs/components/job/legacy_manual_variables_form.vue';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
export default {
components: {
GlLink,
- LegacyManualVariablesForm,
ManualVariablesForm,
},
- mixins: [glFeatureFlagsMixin()],
props: {
illustrationPath: {
type: String,
@@ -20,6 +16,14 @@ export default {
type: String,
required: true,
},
+ isRetryable: {
+ type: Boolean,
+ required: true,
+ },
+ jobId: {
+ type: Number,
+ required: true,
+ },
title: {
type: String,
required: true,
@@ -54,9 +58,6 @@ export default {
},
},
computed: {
- isGraphQL() {
- return this.glFeatures?.graphqlJobApp;
- },
shouldRenderManualVariables() {
return this.playable && !this.scheduled;
},
@@ -77,14 +78,14 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <template v-if="isGraphQL">
- <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
- </template>
- <template v-else>
- <legacy-manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
- </template>
- <div class="text-content">
- <div v-if="action && !shouldRenderManualVariables" class="text-center">
+ <manual-variables-form
+ v-if="shouldRenderManualVariables"
+ :is-retryable="isRetryable"
+ :job-id="jobId"
+ @hideManualVariablesForm="$emit('hideManualVariablesForm')"
+ />
+ <div v-if="action && !shouldRenderManualVariables" class="text-content">
+ <div class="text-center">
<gl-link
:href="action.path"
:data-method="action.method"
diff --git a/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
new file mode 100644
index 00000000000..2b79892a072
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql
@@ -0,0 +1,16 @@
+mutation retryJobWithVariables($id: CiBuildID!, $variables: [CiVariableInput!]) {
+ jobRetry(input: { id: $id, variables: $variables }) {
+ job {
+ id
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ webPath
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
new file mode 100644
index 00000000000..aaf1dec8e0f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job/graphql/queries/get_job.query.graphql
@@ -0,0 +1,17 @@
+query getJob($fullPath: ID!, $id: JobID!) {
+ project(fullPath: $fullPath) {
+ id
+ job(id: $id) {
+ id
+ manualJob
+ manualVariables {
+ nodes {
+ id
+ key
+ value
+ }
+ }
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job/job_app.vue b/app/assets/javascripts/jobs/components/job/job_app.vue
index 81b65d175a7..c6d900ef13e 100644
--- a/app/assets/javascripts/jobs/components/job/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job/job_app.vue
@@ -1,8 +1,9 @@
<script>
-import { GlLoadingIcon, GlIcon, GlSafeHtmlDirective as SafeHtml, GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlAlert } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { throttle, isEmpty } from 'lodash';
import { mapGetters, mapState, mapActions } from 'vuex';
+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';
@@ -71,6 +72,7 @@ export default {
data() {
return {
searchResults: [],
+ showUpdateVariablesState: false,
};
},
computed: {
@@ -121,6 +123,10 @@ export default {
return this.shouldRenderCalloutMessage && !this.hasUnmetPrerequisitesFailure;
},
+ isJobRetryable() {
+ return Boolean(this.job.retry_path);
+ },
+
itemName() {
return sprintf(__('Job %{jobName}'), { jobName: this.job.name });
},
@@ -168,10 +174,16 @@ export default {
'toggleScrollButtons',
'toggleScrollAnimation',
]),
+ onHideManualVariablesForm() {
+ this.showUpdateVariablesState = false;
+ },
onResize() {
this.updateSidebar();
this.updateScroll();
},
+ onUpdateVariables() {
+ this.showUpdateVariablesState = true;
+ },
updateSidebar() {
const breakpoint = bp.getBreakpointSize();
if (breakpoint === 'xs' || breakpoint === 'sm') {
@@ -271,14 +283,12 @@ export default {
</div>
<!-- job log -->
<div
- v-if="hasJobLog"
+ v-if="hasJobLog && !showUpdateVariablesState"
class="build-log-container gl-relative"
:class="{ 'gl-mt-3': !job.archived }"
>
<log-top-bar
:class="{
- 'sidebar-expanded': isSidebarOpen,
- 'sidebar-collapsed': !isSidebarOpen,
'has-archived-block': job.archived,
}"
:size="jobLogSize"
@@ -299,14 +309,17 @@ export default {
<!-- empty state -->
<empty-state
- v-if="!hasJobLog"
+ v-if="!hasJobLog || showUpdateVariablesState"
:illustration-path="emptyStateIllustration.image"
:illustration-size-class="emptyStateIllustration.size"
+ :is-retryable="isJobRetryable"
+ :job-id="job.id"
:title="emptyStateTitle"
:content="emptyStateIllustration.content"
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
+ @hideManualVariablesForm="onHideManualVariablesForm()"
/>
<!-- EO empty state -->
@@ -320,9 +333,9 @@ export default {
'right-sidebar-expanded': isSidebarOpen,
'right-sidebar-collapsed': !isSidebarOpen,
}"
- :erase-path="job.erase_path"
:artifact-help-url="artifactHelpUrl"
data-testid="job-sidebar"
+ @updateVariables="onUpdateVariables()"
/>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
deleted file mode 100644
index 1898e02c94e..00000000000
--- a/app/assets/javascripts/jobs/components/job/legacy_manual_variables_form.vue
+++ /dev/null
@@ -1,192 +0,0 @@
-<script>
-import {
- GlFormInputGroup,
- GlInputGroupText,
- GlFormInput,
- GlButton,
- GlLink,
- GlSprintf,
-} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
-import { mapActions } from 'vuex';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-
-export default {
- name: 'ManualVariablesForm',
- components: {
- GlFormInputGroup,
- GlInputGroupText,
- GlFormInput,
- GlButton,
- GlLink,
- GlSprintf,
- },
- props: {
- action: {
- type: Object,
- required: false,
- default: null,
- validator(value) {
- return (
- value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'path') &&
- Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'button_title'))
- );
- },
- },
- },
- inputTypes: {
- key: 'key',
- value: 'value',
- },
- i18n: {
- header: s__('CiVariables|Variables'),
- keyLabel: s__('CiVariables|Key'),
- valueLabel: s__('CiVariables|Value'),
- keyPlaceholder: s__('CiVariables|Input variable key'),
- valuePlaceholder: s__('CiVariables|Input variable value'),
- formHelpText: s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
- },
- data() {
- return {
- variables: [
- {
- key: '',
- secretValue: '',
- id: uniqueId(),
- },
- ],
- triggerBtnDisabled: false,
- };
- },
- computed: {
- variableSettings() {
- return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
- },
- preparedVariables() {
- // we need to ensure no empty variables are passed to the API
- // and secretValue should be snake_case when passed to the API
- return this.variables
- .filter((variable) => variable.key !== '')
- .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
- },
- },
- methods: {
- ...mapActions(['triggerManualJob']),
- addEmptyVariable() {
- const lastVar = this.variables[this.variables.length - 1];
-
- if (lastVar.key === '') {
- return;
- }
-
- this.variables.push({
- key: '',
- secret_value: '',
- id: uniqueId(),
- });
- },
- canRemove(index) {
- return index < this.variables.length - 1;
- },
- deleteVariable(id) {
- this.variables.splice(
- this.variables.findIndex((el) => el.id === id),
- 1,
- );
- },
- inputRef(type, id) {
- return `${this.$options.inputTypes[type]}-${id}`;
- },
- trigger() {
- this.triggerBtnDisabled = true;
-
- this.triggerManualJob(this.preparedVariables);
- },
- },
-};
-</script>
-<template>
- <div class="row gl-justify-content-center">
- <div class="col-10" data-testid="manual-vars-form">
- <label>{{ $options.i18n.header }}</label>
-
- <div
- v-for="(variable, index) in variables"
- :key="variable.id"
- class="gl-display-flex gl-align-items-center gl-mb-4"
- data-testid="ci-variable-row"
- >
- <gl-form-input-group class="gl-mr-4 gl-flex-grow-1">
- <template #prepend>
- <gl-input-group-text>
- {{ $options.i18n.keyLabel }}
- </gl-input-group-text>
- </template>
- <gl-form-input
- :ref="inputRef('key', variable.id)"
- v-model="variable.key"
- :placeholder="$options.i18n.keyPlaceholder"
- data-testid="ci-variable-key"
- @change="addEmptyVariable"
- />
- </gl-form-input-group>
-
- <gl-form-input-group class="gl-flex-grow-2">
- <template #prepend>
- <gl-input-group-text>
- {{ $options.i18n.valueLabel }}
- </gl-input-group-text>
- </template>
- <gl-form-input
- :ref="inputRef('value', variable.id)"
- v-model="variable.secretValue"
- :placeholder="$options.i18n.valuePlaceholder"
- data-testid="ci-variable-value"
- />
- </gl-form-input-group>
-
- <gl-button
- v-if="canRemove(index)"
- class="gl-flex-grow-0 gl-flex-basis-0"
- category="tertiary"
- variant="danger"
- icon="clear"
- :aria-label="__('Delete variable')"
- data-testid="delete-variable-btn"
- @click="deleteVariable(variable.id)"
- />
-
- <!-- delete variable button placeholder to not break flex layout -->
- <div v-else class="gl-w-7 gl-mr-3" data-testid="delete-variable-btn-placeholder"></div>
- </div>
-
- <div class="gl-text-center gl-mt-5">
- <gl-sprintf :message="$options.i18n.formHelpText">
- <template #link="{ content }">
- <gl-link :href="variableSettings" target="_blank">
- {{ content }}
- </gl-link>
- </template>
- </gl-sprintf>
- </div>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
- <gl-button
- class="gl-mt-5"
- variant="confirm"
- category="primary"
- :aria-label="__('Trigger manual job')"
- :disabled="triggerBtnDisabled"
- data-testid="trigger-manual-job-btn"
- @click="trigger"
- >
- {{ action.button_title }}
- </gl-button>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
index 2f97301979c..d7bbd6daed2 100644
--- a/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/job/manual_variables_form.vue
@@ -5,15 +5,24 @@ import {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
+ GlTooltipDirective,
} from '@gitlab/ui';
-import { uniqueId } from 'lodash';
+import { cloneDeep, uniqueId } from 'lodash';
import { mapActions } from 'vuex';
+import { fetchPolicies } from '~/lib/graphql';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { JOB_GRAPHQL_ERRORS, GRAPHQL_ID_TYPES } from '~/jobs/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { redirectTo } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import GetJob from './graphql/queries/get_job.query.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
-// It is meant to fetch the job information via GraphQL instead of REST API.
+// It is meant to fetch/update the job information via GraphQL instead of REST API.
export default {
name: 'ManualVariablesForm',
@@ -23,59 +32,93 @@ export default {
GlFormInput,
GlButton,
GlLink,
+ GlLoadingIcon,
GlSprintf,
},
- props: {
- action: {
- type: Object,
- required: false,
- default: null,
- validator(value) {
- return (
- value === null ||
- (Object.prototype.hasOwnProperty.call(value, 'path') &&
- Object.prototype.hasOwnProperty.call(value, 'method') &&
- Object.prototype.hasOwnProperty.call(value, 'button_title'))
- );
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: ['projectPath'],
+ apollo: {
+ variables: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
+ return [...jobVariables.reverse(), ...this.variables];
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
},
},
},
+ props: {
+ isRetryable: {
+ type: Boolean,
+ required: true,
+ },
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ },
inputTypes: {
key: 'key',
value: 'value',
},
i18n: {
+ clearInputs: s__('CiVariables|Clear inputs'),
+ formHelpText: s__(
+ 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
+ ),
header: s__('CiVariables|Variables'),
keyLabel: s__('CiVariables|Key'),
- valueLabel: s__('CiVariables|Value'),
keyPlaceholder: s__('CiVariables|Input variable key'),
+ runAgainButtonText: s__('CiVariables|Run job again'),
+ triggerButtonText: s__('CiVariables|Trigger this manual action'),
+ valueLabel: s__('CiVariables|Value'),
valuePlaceholder: s__('CiVariables|Input variable value'),
- formHelpText: s__(
- 'CiVariables|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used as default',
- ),
+ },
+ variableValueKeys: {
+ rest: 'secret_value',
+ gql: 'value',
},
data() {
return {
+ job: {},
variables: [
{
- key: '',
- secretValue: '',
id: uniqueId(),
+ key: '',
+ value: '',
},
],
+ runAgainBtnDisabled: false,
triggerBtnDisabled: false,
};
},
computed: {
- variableSettings() {
- return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
- },
preparedVariables() {
- // we need to ensure no empty variables are passed to the API
- // and secretValue should be snake_case when passed to the API
+ // filtering out 'id' along with empty variables to send only key, value in the mutation.
+ // This will be removed in: https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+
return this.variables
.filter((variable) => variable.key !== '')
- .map(({ key, secretValue }) => ({ key, secret_value: secretValue }));
+ .map(({ key, value }) => ({ key, [this.valueKey]: value }));
+ },
+ valueKey() {
+ return this.isRetryable
+ ? this.$options.variableValueKeys.gql
+ : this.$options.variableValueKeys.rest;
+ },
+ variableSettings() {
+ return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
methods: {
@@ -88,9 +131,9 @@ export default {
}
this.variables.push({
- key: '',
- secret_value: '',
id: uniqueId(),
+ key: '',
+ value: '',
});
},
canRemove(index) {
@@ -105,7 +148,34 @@ export default {
inputRef(type, id) {
return `${this.$options.inputTypes[type]}-${id}`;
},
- trigger() {
+ navigateToRetriedJob(retryPath) {
+ redirectTo(retryPath);
+ },
+ async retryJob() {
+ try {
+ const { data } = await this.$apollo.mutate({
+ mutation: retryJobWithVariablesMutation,
+ variables: {
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, this.jobId),
+ // we need to ensure no empty variables are passed to the API
+ variables: this.preparedVariables,
+ },
+ });
+ if (data.jobRetry?.errors?.length) {
+ createAlert({ message: data.jobRetry.errors[0] });
+ } else {
+ this.navigateToRetriedJob(data.jobRetry?.job?.webPath);
+ }
+ } catch (error) {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.retryMutationErrorText });
+ }
+ },
+ runAgain() {
+ this.runAgainBtnDisabled = true;
+
+ this.retryJob();
+ },
+ triggerJob() {
this.triggerBtnDisabled = true;
this.triggerManualJob(this.preparedVariables);
@@ -114,7 +184,8 @@ export default {
};
</script>
<template>
- <div class="row gl-justify-content-center">
+ <gl-loading-icon v-if="$apollo.queries.variables.loading" class="gl-mt-9" size="lg" />
+ <div v-else class="row gl-justify-content-center">
<div class="col-10" data-testid="manual-vars-form">
<label>{{ $options.i18n.header }}</label>
@@ -147,7 +218,7 @@ export default {
</template>
<gl-form-input
:ref="inputRef('value', variable.id)"
- v-model="variable.secretValue"
+ v-model="variable.value"
:placeholder="$options.i18n.valuePlaceholder"
data-testid="ci-variable-value"
/>
@@ -155,11 +226,13 @@ export default {
<gl-button
v-if="canRemove(index)"
+ v-gl-tooltip
+ :aria-label="$options.i18n.clearInputs"
+ :title="$options.i18n.clearInputs"
class="gl-flex-grow-0 gl-flex-basis-0"
category="tertiary"
variant="danger"
icon="clear"
- :aria-label="__('Delete variable')"
data-testid="delete-variable-btn"
@click="deleteVariable(variable.id)"
/>
@@ -177,7 +250,27 @@ export default {
</template>
</gl-sprintf>
</div>
- <div class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <div v-if="isRetryable" class="gl-display-flex gl-justify-content-center gl-mt-5">
+ <gl-button
+ class="gl-mt-5"
+ :aria-label="__('Cancel')"
+ data-testid="cancel-btn"
+ @click="$emit('hideManualVariablesForm')"
+ >{{ __('Cancel') }}</gl-button
+ >
+ <gl-button
+ class="gl-mt-5"
+ variant="confirm"
+ category="primary"
+ :aria-label="__('Run manual job again')"
+ :disabled="runAgainBtnDisabled"
+ data-testid="run-manual-job-btn"
+ @click="runAgain"
+ >
+ {{ $options.i18n.runAgainButtonText }}
+ </gl-button>
+ </div>
+ <div v-else class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-button
class="gl-mt-5"
variant="confirm"
@@ -185,9 +278,9 @@ export default {
:aria-label="__('Trigger manual job')"
:disabled="triggerBtnDisabled"
data-testid="trigger-manual-job-btn"
- @click="trigger"
+ @click="triggerJob"
>
- {{ action.button_title }}
+ {{ $options.i18n.triggerButtonText }}
</gl-button>
</div>
</div>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
index dd620977f0c..7183a8b5d03 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/job_sidebar_retry_button.vue
@@ -1,15 +1,17 @@
<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlDropdown, GlDropdownItem, GlModalDirective } from '@gitlab/ui';
import { mapGetters } from 'vuex';
import { JOB_SIDEBAR_COPY } from '~/jobs/constants';
export default {
name: 'JobSidebarRetryButton',
i18n: {
- retryLabel: JOB_SIDEBAR_COPY.retry,
+ ...JOB_SIDEBAR_COPY,
},
components: {
GlButton,
+ GlDropdown,
+ GlDropdownItem,
},
directives: {
GlModal: GlModalDirective,
@@ -23,6 +25,10 @@ export default {
type: String,
required: true,
},
+ isManualJob: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -33,17 +39,30 @@ export default {
<gl-button
v-if="hasForwardDeploymentFailure"
v-gl-modal="modalId"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
data-testid="retry-job-button"
/>
-
+ <gl-dropdown
+ v-else-if="isManualJob"
+ icon="retry"
+ category="primary"
+ :right="true"
+ variant="confirm"
+ >
+ <gl-dropdown-item :href="href" data-method="post">
+ {{ $options.i18n.runAgainJobButtonLabel }}
+ </gl-dropdown-item>
+ <gl-dropdown-item @click="$emit('updateVariablesClicked')">
+ {{ $options.i18n.updateVariables }}
+ </gl-dropdown-item>
+ </gl-dropdown>
<gl-button
v-else
:href="href"
- :aria-label="$options.i18n.retryLabel"
+ :aria-label="$options.i18n.retryJobLabel"
category="primary"
variant="confirm"
icon="retry"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
deleted file mode 100644
index 64b497c3550..00000000000
--- a/app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue
+++ /dev/null
@@ -1,104 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { mapActions } from 'vuex';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
-import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
-
-export default {
- name: 'LegacySidebarHeader',
- i18n: {
- ...JOB_SIDEBAR_COPY,
- },
- forwardDeploymentFailureModalId,
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlButton,
- JobSidebarRetryButton,
- TooltipOnTruncate,
- },
- props: {
- job: {
- type: Object,
- required: true,
- default: () => ({}),
- },
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
- },
- computed: {
- retryButtonCategory() {
- return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
- },
- buttonTitle() {
- return this.job.status && this.job.status.text === 'passed'
- ? this.$options.i18n.runAgainJobButtonLabel
- : this.$options.i18n.retryJobButtonLabel;
- },
- },
- methods: {
- ...mapActions(['toggleSidebar']),
- },
-};
-</script>
-
-<template>
- <div class="gl-py-5 gl-display-flex gl-align-items-center">
- <tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
- </tooltip-on-truncate>
- <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
- <gl-button
- v-if="erasePath"
- v-gl-tooltip.left
- :title="$options.i18n.eraseLogButtonLabel"
- :aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
- :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"
- />
- <job-sidebar-retry-button
- v-if="job.retry_path"
- v-gl-tooltip.left
- :title="buttonTitle"
- :aria-label="buttonTitle"
- :category="retryButtonCategory"
- :href="job.retry_path"
- :modal-id="$options.forwardDeploymentFailureModalId"
- variant="confirm"
- data-qa-selector="retry_button"
- data-testid="retry-button"
- />
- <gl-button
- v-if="job.cancel_path"
- v-gl-tooltip.left
- :title="$options.i18n.cancelJobButtonLabel"
- :aria-label="$options.i18n.cancelJobButtonLabel"
- :href="job.cancel_path"
- variant="danger"
- icon="cancel"
- data-method="post"
- data-testid="cancel-button"
- rel="nofollow"
- />
- <gl-button
- :aria-label="$options.i18n.toggleSidebar"
- category="tertiary"
- class="gl-md-display-none gl-ml-2"
- icon="chevron-double-lg-right"
- @click="toggleSidebar"
- />
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
index aac6a0ad6d3..69271cc9022 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar.vue
@@ -2,14 +2,12 @@
import { GlButton, GlIcon } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import ArtifactsBlock from './artifacts_block.vue';
import CommitBlock from './commit_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';
-import ArtifactsBlock from './artifacts_block.vue';
-import LegacySidebarHeader from './legacy_sidebar_header.vue';
import SidebarHeader from './sidebar_header.vue';
import StagesDropdown from './stages_dropdown.vue';
import TriggerBlock from './trigger_block.vue';
@@ -29,23 +27,16 @@ export default {
JobsContainer,
JobRetryForwardDeploymentModal,
JobSidebarDetailsContainer,
- LegacySidebarHeader,
SidebarHeader,
StagesDropdown,
TriggerBlock,
},
- mixins: [glFeatureFlagsMixin()],
props: {
artifactHelpUrl: {
type: String,
required: false,
default: '',
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
...mapGetters(['hasForwardDeploymentFailure']),
@@ -57,9 +48,6 @@ export default {
hasTriggers() {
return !isEmpty(this.job.trigger);
},
- isGraphQL() {
- return this.glFeatures?.graphqlJobApp;
- },
commit() {
return this.job?.pipeline?.commit || {};
},
@@ -89,8 +77,11 @@ export default {
<aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
<div class="sidebar-container">
<div class="blocks-container">
- <sidebar-header v-if="isGraphQL" :erase-path="erasePath" :job="job" />
- <legacy-sidebar-header v-else :erase-path="erasePath" :job="job" />
+ <sidebar-header
+ :rest-job="job"
+ :job-id="job.id"
+ @updateVariables="$emit('updateVariables')"
+ />
<div
v-if="job.terminal_path || job.new_issue_path"
class="gl-py-5"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
index 523710598bf..40aec0b0536 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_header.vue
@@ -1,13 +1,19 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import { createAlert } from '~/flash';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
-import { JOB_SIDEBAR_COPY, forwardDeploymentFailureModalId } from '~/jobs/constants';
+import {
+ JOB_GRAPHQL_ERRORS,
+ GRAPHQL_ID_TYPES,
+ JOB_SIDEBAR_COPY,
+ forwardDeploymentFailureModalId,
+ PASSED_STATUS,
+} from '~/jobs/constants';
+import GetJob from '../graphql/queries/get_job.query.graphql';
import JobSidebarRetryButton from './job_sidebar_retry_button.vue';
-// This component is a port of ~/jobs/components/job/sidebar/legacy_sidebar_header.vue
-// It is meant to fetch the job information via GraphQL instead of REST API.
-
export default {
name: 'SidebarHeader',
i18n: {
@@ -22,21 +28,58 @@ export default {
JobSidebarRetryButton,
TooltipOnTruncate,
},
- props: {
+ inject: ['projectPath'],
+ apollo: {
job: {
+ query: GetJob,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ id: convertToGraphQLId(GRAPHQL_ID_TYPES.commitStatus, this.jobId),
+ };
+ },
+ update(data) {
+ const { name, manualJob } = data?.project?.job || {};
+ return {
+ name,
+ manualJob,
+ };
+ },
+ error() {
+ createAlert({ message: JOB_GRAPHQL_ERRORS.jobQueryErrorText });
+ },
+ },
+ },
+ props: {
+ jobId: {
+ type: Number,
+ required: true,
+ },
+ restJob: {
type: Object,
required: true,
default: () => ({}),
},
- erasePath: {
- type: String,
- required: false,
- default: null,
- },
+ },
+ data() {
+ return {
+ job: {},
+ };
},
computed: {
+ buttonTitle() {
+ return this.restJob.status?.text === PASSED_STATUS
+ ? this.$options.i18n.runAgainJobButtonLabel
+ : this.$options.i18n.retryJobLabel;
+ },
+ canShowJobRetryButton() {
+ return this.restJob.retry_path && !this.$apollo.queries.job.loading;
+ },
+ isManualJob() {
+ return this.job?.manualJob;
+ },
retryButtonCategory() {
- return this.job.status && this.job.recoverable ? 'primary' : 'secondary';
+ return this.restJob.status && this.restJob.recoverable ? 'primary' : 'secondary';
},
},
methods: {
@@ -48,17 +91,15 @@ export default {
<template>
<div class="gl-py-5 gl-display-flex gl-align-items-center">
<tooltip-on-truncate :title="job.name" truncate-target="child"
- ><h4 class="gl-my-0 gl-mr-3 gl-text-truncate">
- {{ job.name }}
- </h4>
+ ><h4 class="gl-my-0 gl-mr-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">
<gl-button
- v-if="erasePath"
+ v-if="restJob.erase_path"
v-gl-tooltip.left
:title="$options.i18n.eraseLogButtonLabel"
:aria-label="$options.i18n.eraseLogButtonLabel"
- :href="erasePath"
+ :href="restJob.erase_path"
:data-confirm="$options.i18n.eraseLogConfirmText"
class="gl-mr-2"
data-testid="job-log-erase-link"
@@ -67,23 +108,25 @@ export default {
icon="remove"
/>
<job-sidebar-retry-button
- v-if="job.retry_path"
+ v-if="canShowJobRetryButton"
v-gl-tooltip.left
- :title="$options.i18n.retryJobButtonLabel"
- :aria-label="$options.i18n.retryJobButtonLabel"
+ :title="buttonTitle"
+ :aria-label="buttonTitle"
+ :is-manual-job="isManualJob"
:category="retryButtonCategory"
- :href="job.retry_path"
+ :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="job.cancel_path"
+ v-if="restJob.cancel_path"
v-gl-tooltip.left
:title="$options.i18n.cancelJobButtonLabel"
:aria-label="$options.i18n.cancelJobButtonLabel"
- :href="job.cancel_path"
+ :href="restJob.cancel_path"
variant="danger"
icon="cancel"
data-method="post"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
index 3b1509e5be5..8300a22cb67 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/sidebar_job_details_container.vue
@@ -1,6 +1,7 @@
<script>
import { mapState } from 'vuex';
import { GlBadge } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -79,7 +80,9 @@ export default {
TAGS: __('Tags:'),
TIMEOUT: __('Timeout'),
},
- RUNNER_HELP_URL: 'https://docs.gitlab.com/runner/register/index.html',
+ TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
+ anchor: 'set-a-limit-for-how-long-jobs-can-run',
+ }),
};
</script>
@@ -96,7 +99,7 @@ export default {
<detail-row v-if="job.queued_duration" :value="queuedDuration" :title="$options.i18n.QUEUED" />
<detail-row
v-if="hasTimeout"
- :help-url="$options.RUNNER_HELP_URL"
+ :help-url="$options.TIMEOUT_HELP_URL"
:value="timeout"
data-testid="job-timeout"
:title="$options.i18n.TIMEOUT"
diff --git a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
index 1afc1c9a595..c9172fe0322 100644
--- a/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
+++ b/app/assets/javascripts/jobs/components/job/sidebar/trigger_block.vue
@@ -2,9 +2,7 @@
import { GlButton, GlTableLite } from '@gitlab/ui';
import { __ } from '~/locale';
-const DEFAULT_TD_CLASSES = 'gl-w-half gl-font-sm! gl-border-gray-200!';
-const DEFAULT_TH_CLASSES =
- 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-200! gl-border-b-1!';
+const DEFAULT_TD_CLASSES = 'gl-font-sm!';
export default {
fields: [
@@ -13,14 +11,12 @@ export default {
label: __('Key'),
tdAttr: { 'data-testid': 'trigger-build-key' },
tdClass: DEFAULT_TD_CLASSES,
- thClass: DEFAULT_TH_CLASSES,
},
{
key: 'value',
label: __('Value'),
tdAttr: { 'data-testid': 'trigger-build-value' },
tdClass: DEFAULT_TD_CLASSES,
- thClass: DEFAULT_TH_CLASSES,
},
],
components: {
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index e9475994e8b..405aea11181 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -5,6 +5,11 @@ const moreInfo = __('More information');
export const forwardDeploymentFailureModalId = 'forward-deployment-failure';
+export const GRAPHQL_ID_TYPES = {
+ commitStatus: 'CommitStatus',
+ ciBuild: 'Ci::Build',
+};
+
export const JOB_SIDEBAR_COPY = {
cancel,
cancelJobButtonLabel: s__('Job|Cancel'),
@@ -12,10 +17,15 @@ export const JOB_SIDEBAR_COPY = {
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'),
- retry: __('Retry'),
- retryJobButtonLabel: s__('Job|Retry'),
+ retryJobLabel: s__('Job|Retry'),
toggleSidebar: __('Toggle Sidebar'),
runAgainJobButtonLabel: s__('Job|Run again'),
+ updateVariables: s__('Job|Update CI/CD variables'),
+};
+
+export const JOB_GRAPHQL_ERRORS = {
+ retryMutationErrorText: __('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 = {
@@ -31,3 +41,4 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
};
export const SUCCESS_STATUS = 'SUCCESS';
+export const PASSED_STATUS = 'passed';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 9dd47f4046c..44bb1ffb1bc 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -1,10 +1,17 @@
import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import JobApp from './components/job/job_app.vue';
import createStore from './store';
+Vue.use(VueApollo);
Vue.use(GlToast);
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
const initializeJobPage = (element) => {
const store = createStore();
@@ -26,11 +33,13 @@ const initializeJobPage = (element) => {
return new Vue({
el: element,
+ apolloProvider,
store,
components: {
JobApp,
},
provide: {
+ projectPath,
retryOutdatedJobDocsUrl,
},
render(createElement) {
diff --git a/app/assets/javascripts/labels/labels_select.js b/app/assets/javascripts/labels/labels_select.js
index 65dda804a20..515b0a79a03 100644
--- a/app/assets/javascripts/labels/labels_select.js
+++ b/app/assets/javascripts/labels/labels_select.js
@@ -4,7 +4,7 @@
import $ from 'jquery';
import { difference, isEqual, escape, sortBy, template, union } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import IssuableBulkUpdateActions from '~/issuable/bulk_update_sidebar/issuable_bulk_update_actions';
+import IssuableBulkUpdateActions from '~/issuable/issuable_bulk_update_actions';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/language_switcher/components/app.vue b/app/assets/javascripts/language_switcher/components/app.vue
new file mode 100644
index 00000000000..71babe6c614
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/components/app.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { setCookie } from '~/lib/utils/common_utils';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from '../constants';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+ inject: {
+ locales: {
+ default: [],
+ },
+ preferredLocale: {
+ default: {},
+ },
+ },
+ data() {
+ return {
+ selected: this.preferredLocale.value,
+ };
+ },
+ methods: {
+ onLanguageSelected(code) {
+ setCookie(PREFERRED_LANGUAGE_COOKIE_KEY, code);
+ window.location.reload();
+ },
+ },
+};
+</script>
+<template>
+ <gl-collapsible-listbox
+ v-model="selected"
+ :toggle-text="preferredLocale.text"
+ :items="locales"
+ category="tertiary"
+ right
+ icon="earth"
+ size="small"
+ toggle-class="py-0 gl-h-6"
+ @select="onLanguageSelected"
+ >
+ <template #list-item="{ item: locale }">
+ <span :data-testid="`language_switcher_lang_${locale.value}`">
+ {{ locale.text }}
+ </span>
+ </template>
+ </gl-collapsible-listbox>
+</template>
diff --git a/app/assets/javascripts/language_switcher/constants.js b/app/assets/javascripts/language_switcher/constants.js
new file mode 100644
index 00000000000..b5c0613ac01
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/constants.js
@@ -0,0 +1 @@
+export const PREFERRED_LANGUAGE_COOKIE_KEY = 'preferred_language';
diff --git a/app/assets/javascripts/language_switcher/index.js b/app/assets/javascripts/language_switcher/index.js
new file mode 100644
index 00000000000..b224e2510bb
--- /dev/null
+++ b/app/assets/javascripts/language_switcher/index.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import { getCookie } from '~/lib/utils/common_utils';
+import LanguageSwitcher from './components/app.vue';
+import { PREFERRED_LANGUAGE_COOKIE_KEY } from './constants';
+
+export const initLanguageSwitcher = () => {
+ const el = document.querySelector('.js-language-switcher');
+ if (!el) return false;
+ const locales = JSON.parse(el.dataset.locales);
+ const preferredLangCode = getCookie(PREFERRED_LANGUAGE_COOKIE_KEY);
+ const preferredLocale = locales.find((locale) => locale.value === preferredLangCode);
+
+ return new Vue({
+ el,
+ provide: {
+ locales,
+ preferredLocale,
+ },
+ render(createElement) {
+ return createElement(LanguageSwitcher);
+ },
+ });
+};
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 27760e483aa..5372f6555d2 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -18,7 +18,7 @@ export const defaultConfig = {
'data-disable',
'data-turbo',
],
- FORBID_TAGS: ['style', 'mstyle'],
+ FORBID_TAGS: ['style', 'mstyle', 'form'],
ALLOW_UNKNOWN_PROTOCOLS: true,
};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index beced4f9144..4ce63d518a6 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -4,9 +4,9 @@
import { GlBreakpointInstance as breakpointInstance } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
-import { isFunction, defer } from 'lodash';
+import { isFunction, defer, escape } from 'lodash';
import Cookies from '~/lib/utils/cookies';
-import { SCOPED_LABEL_DELIMITER } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import { SCOPED_LABEL_DELIMITER } from '~/sidebar/components/labels/labels_select_widget/constants';
import { convertToCamelCase, convertToSnakeCase } from './text_utility';
import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
@@ -28,16 +28,12 @@ export const checkPageAndAction = (page, action) => {
export const isInIncidentPage = () => checkPageAndAction('incidents', 'show');
export const isInIssuePage = () => checkPageAndAction('issues', 'show');
export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
-export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInMRPage = () =>
+ checkPageAndAction('merge_requests', 'show') || checkPageAndAction('merge_requests', 'diffs');
export const isInEpicPage = () => checkPageAndAction('epics', 'show');
export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
-export const getCspNonceValue = () => {
- const metaTag = document.querySelector('meta[name=csp-nonce]');
- return metaTag && metaTag.content;
-};
-
export const rstrip = (val) => {
if (val) {
return val.replace(/\s+$/, '');
@@ -469,7 +465,7 @@ export const backOff = (fn, timeout = 60000) => {
export const spriteIcon = (icon, className = '') => {
const classAttribute = className.length > 0 ? `class="${className}"` : '';
- return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${icon}" /></svg>`;
+ return `<svg ${classAttribute}><use xlink:href="${gon.sprite_icons}#${escape(icon)}" /></svg>`;
};
/**
@@ -715,3 +711,16 @@ export const getFirstPropertyValue = (data) => {
return data[key];
};
+
+// TODO: remove when FF `new_fonts` is removed https://gitlab.com/gitlab-org/gitlab/-/issues/379147
+/**
+ * This method checks the FF `new_fonts`
+ * as well as a query parameter `new_fonts`.
+ * If either of them is enabled, new fonts will be applied.
+ *
+ * @returns Boolean Whether to apply new fonts
+ */
+export const useNewFonts = () => {
+ const hasQueryParam = new URLSearchParams(window.location.search).has('new_fonts');
+ return window?.gon.features?.newFonts || hasQueryParam;
+};
diff --git a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
index 3788d8ab20c..ea91ccec546 100644
--- a/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
+++ b/app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue
@@ -1,10 +1,11 @@
<script>
-import { GlModal, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
GlModal,
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 379c57f3945..2c8953237cf 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,6 +1,5 @@
export const BYTES_IN_KIB = 1024;
export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
-export const HIDDEN_CLASS = 'hidden';
export const THOUSAND = 1000;
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/create_and_submit_form.js b/app/assets/javascripts/lib/utils/create_and_submit_form.js
new file mode 100644
index 00000000000..fce4f898f2f
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/create_and_submit_form.js
@@ -0,0 +1,26 @@
+import csrf from '~/lib/utils/csrf';
+
+export const createAndSubmitForm = ({ url, data }) => {
+ const form = document.createElement('form');
+
+ form.action = url;
+ // For now we only support 'post'.
+ // `form.method` doesn't support other methods so we would need to
+ // use a hidden `_method` input, which is out of scope for now.
+ form.method = 'post';
+ form.style.display = 'none';
+
+ Object.entries(data)
+ .concat([['authenticity_token', csrf.token]])
+ .forEach(([key, value]) => {
+ const input = document.createElement('input');
+ input.type = 'hidden';
+ input.name = key;
+ input.value = value;
+
+ form.appendChild(input);
+ });
+
+ document.body.appendChild(form);
+ form.submit();
+};
diff --git a/app/assets/javascripts/lib/utils/dom_utils.js b/app/assets/javascripts/lib/utils/dom_utils.js
index cafee641174..317c401e404 100644
--- a/app/assets/javascripts/lib/utils/dom_utils.js
+++ b/app/assets/javascripts/lib/utils/dom_utils.js
@@ -118,3 +118,24 @@ export const getContentWrapperHeight = (contentWrapperClass) => {
const wrapperEl = document.querySelector(contentWrapperClass);
return wrapperEl ? `${wrapperEl.offsetTop}px` : '';
};
+
+/**
+ * Replaces comment nodes in a DOM tree with a different element
+ * containing the text of the comment.
+ *
+ * @param {*} el
+ * @param {*} tagName
+ */
+export const replaceCommentsWith = (el, tagName) => {
+ const iterator = document.createNodeIterator(el, NodeFilter.SHOW_COMMENT);
+ let commentNode = iterator.nextNode();
+
+ while (commentNode) {
+ const newNode = document.createElement(tagName);
+ newNode.textContent = commentNode.textContent;
+
+ commentNode.parentNode.replaceChild(newNode, commentNode);
+
+ commentNode = iterator.nextNode();
+ }
+};
diff --git a/app/assets/javascripts/lib/utils/http_status.js b/app/assets/javascripts/lib/utils/http_status.js
index c5190592bb6..ec0d8d433a5 100644
--- a/app/assets/javascripts/lib/utils/http_status.js
+++ b/app/assets/javascripts/lib/utils/http_status.js
@@ -1,45 +1,43 @@
-/**
- * exports HTTP status codes
- */
+export const HTTP_STATUS_ABORTED = 0;
+export const HTTP_STATUS_CREATED = 201;
+export const HTTP_STATUS_ACCEPTED = 202;
+export const HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION = 203;
+export const HTTP_STATUS_NO_CONTENT = 204;
+export const HTTP_STATUS_RESET_CONTENT = 205;
+export const HTTP_STATUS_PARTIAL_CONTENT = 206;
+export const HTTP_STATUS_MULTI_STATUS = 207;
+export const HTTP_STATUS_ALREADY_REPORTED = 208;
+export const HTTP_STATUS_IM_USED = 226;
+export const HTTP_STATUS_METHOD_NOT_ALLOWED = 405;
+export const HTTP_STATUS_CONFLICT = 409;
+export const HTTP_STATUS_GONE = 410;
+export const HTTP_STATUS_PAYLOAD_TOO_LARGE = 413;
+export const HTTP_STATUS_UNPROCESSABLE_ENTITY = 422;
+export const HTTP_STATUS_TOO_MANY_REQUESTS = 429;
+// TODO move the rest of the status codes to primitive constants
+// https://docs.gitlab.com/ee/development/fe_guide/style/javascript.html#export-constants-as-primitives
const httpStatusCodes = {
- ABORTED: 0,
OK: 200,
- CREATED: 201,
- ACCEPTED: 202,
- NON_AUTHORITATIVE_INFORMATION: 203,
- NO_CONTENT: 204,
- RESET_CONTENT: 205,
- PARTIAL_CONTENT: 206,
- MULTI_STATUS: 207,
- ALREADY_REPORTED: 208,
- IM_USED: 226,
- MULTIPLE_CHOICES: 300,
BAD_REQUEST: 400,
UNAUTHORIZED: 401,
FORBIDDEN: 403,
NOT_FOUND: 404,
- METHOD_NOT_ALLOWED: 405,
- CONFLICT: 409,
- GONE: 410,
- PAYLOAD_TOO_LARGE: 413,
- UNPROCESSABLE_ENTITY: 422,
- TOO_MANY_REQUESTS: 429,
INTERNAL_SERVER_ERROR: 500,
SERVICE_UNAVAILABLE: 503,
};
export const successCodes = [
httpStatusCodes.OK,
- httpStatusCodes.CREATED,
- httpStatusCodes.ACCEPTED,
- httpStatusCodes.NON_AUTHORITATIVE_INFORMATION,
- httpStatusCodes.NO_CONTENT,
- httpStatusCodes.RESET_CONTENT,
- httpStatusCodes.PARTIAL_CONTENT,
- httpStatusCodes.MULTI_STATUS,
- httpStatusCodes.ALREADY_REPORTED,
- httpStatusCodes.IM_USED,
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_NON_AUTHORITATIVE_INFORMATION,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_RESET_CONTENT,
+ HTTP_STATUS_PARTIAL_CONTENT,
+ HTTP_STATUS_MULTI_STATUS,
+ HTTP_STATUS_ALREADY_REPORTED,
+ HTTP_STATUS_IM_USED,
];
export default httpStatusCodes;
diff --git a/app/assets/javascripts/lib/utils/poll.js b/app/assets/javascripts/lib/utils/poll.js
index 71782c9a4ce..73add1e37ee 100644
--- a/app/assets/javascripts/lib/utils/poll.js
+++ b/app/assets/javascripts/lib/utils/poll.js
@@ -1,5 +1,5 @@
import { normalizeHeaders } from './common_utils';
-import httpStatusCodes, { successCodes } from './http_status';
+import { HTTP_STATUS_ABORTED, successCodes } from './http_status';
/**
* Polling utility for handling realtime updates.
@@ -108,7 +108,7 @@ export default class Poll {
})
.catch((error) => {
notificationCallback(false);
- if (error.status === httpStatusCodes.ABORTED) {
+ if (error.status === HTTP_STATUS_ABORTED) {
return;
}
errorCallback(error);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index b1a0baf8150..f33484f4192 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -86,7 +86,7 @@ export function cleanLeadingSeparator(path) {
return path.replace(PATH_SEPARATOR_LEADING_REGEX, '');
}
-function cleanEndingSeparator(path) {
+export function cleanEndingSeparator(path) {
return path.replace(PATH_SEPARATOR_ENDING_REGEX, '');
}
diff --git a/app/assets/javascripts/listbox/index.js b/app/assets/javascripts/listbox/index.js
index 7eacbf7fcdd..7e8fc4b637b 100644
--- a/app/assets/javascripts/listbox/index.js
+++ b/app/assets/javascripts/listbox/index.js
@@ -1,4 +1,4 @@
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
@@ -31,7 +31,7 @@ export function initListbox(el, { onChange } = {}) {
},
},
render(h) {
- return h(GlListbox, {
+ return h(GlCollapsibleListbox, {
props: {
items,
right,
diff --git a/app/assets/javascripts/listbox/redirect_behavior.js b/app/assets/javascripts/listbox/redirect_behavior.js
index 7e0ea2c4dfd..38d9d84f889 100644
--- a/app/assets/javascripts/listbox/redirect_behavior.js
+++ b/app/assets/javascripts/listbox/redirect_behavior.js
@@ -2,7 +2,7 @@ import { initListbox } from '~/listbox';
import { redirectTo } from '~/lib/utils/url_utility';
/**
- * Instantiates GlListbox components with redirect behavior for tags created
+ * Instantiates GlCollapsibleListbox components with redirect behavior for tags created
* with the `gl_redirect_listbox_tag` HAML helper.
*
* NOTE: Do not import this script explicitly. Using `gl_redirect_listbox_tag`
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 8e4ebd510aa..df3b55ed2ad 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -37,6 +37,7 @@ import initBroadcastNotifications from './broadcast_notification';
import { initTopNav } from './nav';
import { initCopyCodeButton } from './behaviors/copy_code';
import initHeaderSearch from './header_search/init';
+import initGitlabVersionCheck from './gitlab_version_check';
import 'ee_else_ce/main_ee';
import 'jh_else_ce/main_jh';
@@ -100,21 +101,7 @@ function deferredInitialisation() {
initDefaultTrackers();
initFeatureHighlight();
initCopyCodeButton();
-
- const helpToggle = document.querySelector('.header-help-dropdown-toggle');
- if (helpToggle) {
- helpToggle.addEventListener(
- 'click',
- () => {
- import(/* webpackChunkName: 'versionCheck' */ './gitlab_version_check')
- .then(({ default: initGitlabVersionCheck }) => {
- initGitlabVersionCheck();
- })
- .catch(() => {});
- },
- { once: true },
- );
- }
+ initGitlabVersionCheck();
addSelectOnFocusBehaviour('.js-select-on-focus');
diff --git a/app/assets/javascripts/members/components/avatars/user_avatar.vue b/app/assets/javascripts/members/components/avatars/user_avatar.vue
index ec59f0f681c..4260ee14a14 100644
--- a/app/assets/javascripts/members/components/avatars/user_avatar.vue
+++ b/app/assets/javascripts/members/components/avatars/user_avatar.vue
@@ -1,10 +1,6 @@
<script>
-import {
- GlAvatarLink,
- GlAvatarLabeled,
- GlBadge,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateBadges } from 'ee_else_ce/members/utils';
import { glEmojiTag } from '~/emoji';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
index cb7b963b698..76b286f94ad 100644
--- a/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
+++ b/app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue
@@ -7,11 +7,11 @@ import {
redirectTo,
} from '~/lib/utils/url_utility';
import {
- SEARCH_TOKEN_TYPE,
SORT_QUERY_PARAM_NAME,
ACTIVE_TAB_QUERY_PARAM_NAME,
AVAILABLE_FILTERED_SEARCH_TOKENS,
} from 'ee_else_ce/members/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
export default {
@@ -65,7 +65,7 @@ export default {
if (query[this.filteredSearchBar.searchParam]) {
tokens.push({
- type: SEARCH_TOKEN_TYPE,
+ type: FILTERED_SEARCH_TERM,
value: {
data: query[this.filteredSearchBar.searchParam],
},
@@ -83,7 +83,7 @@ export default {
return accumulator;
}
- if (type === SEARCH_TOKEN_TYPE) {
+ if (type === FILTERED_SEARCH_TERM) {
if (value.data !== '') {
const { searchParam } = this.filteredSearchBar;
const { [searchParam]: searchParamValue } = accumulator;
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index 3135ec602be..dab544c7cbc 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -1,7 +1,7 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
// Overridden in EE
export const EE_APP_OPTIONS = {};
@@ -117,7 +117,7 @@ export const FILTERED_SEARCH_TOKEN_TWO_FACTOR = {
title: s__('Members|2FA'),
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'enabled', title: s__('Members|Enabled') },
{ value: 'disabled', title: s__('Members|Disabled') },
@@ -131,7 +131,7 @@ export const FILTERED_SEARCH_TOKEN_WITH_INHERITED_PERMISSIONS = {
title: s__('Members|Membership'),
token: GlFilteredSearchToken,
unique: true,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
options: [
{ value: 'exclude', title: s__('Members|Direct') },
{ value: 'only', title: s__('Members|Inherited') },
@@ -187,8 +187,6 @@ export const LEAVE_MODAL_ID = 'member-leave-modal';
export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
-export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
-
export const SORT_QUERY_PARAM_NAME = 'sort';
export const ACTIVE_TAB_QUERY_PARAM_NAME = 'tab';
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
index 87eeb272659..6c431dc8af3 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
index 2c59e7bfa2f..f8a097a3a0f 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import syntaxHighlight from '~/syntax_highlight';
import { SYNTAX_HIGHLIGHT_CLASS } from '../constants';
import utilsMixin from '../mixins/line_conflict_utils';
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 57b5e9809d2..80eb94a5364 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -94,7 +94,11 @@ MergeRequest.prototype.initMRBtnListeners = function () {
.put(draftToggle.href, null, { params: { format: 'json' } })
.then(({ data }) => {
draftToggle.removeAttribute('disabled');
- eventHub.$emit('MRWidgetUpdateRequested');
+
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
+
MergeRequest.toggleDraftStatus(data.title, wipEvent === 'ready');
})
.catch(() => {
@@ -173,7 +177,7 @@ MergeRequest.toggleDraftStatus = function (title, isReady) {
);
draftToggle.setAttribute('href', url);
- draftToggle.querySelector('.gl-new-dropdown-item-text-wrapper').textContent = isReady
+ draftToggle.querySelector('.gl-dropdown-item-text-wrapper').textContent = isReady
? __('Mark as draft')
: __('Mark as ready');
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 0ddf5def8ee..5a1410ceeba 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -5,6 +5,7 @@ import { createAlert } from '~/flash';
import { getCookie, isMetaClick, parseBoolean, scrollToElement } from '~/lib/utils/common_utils';
import { parseUrlPathname } from '~/lib/utils/url_utility';
import createEventHub from '~/helpers/event_hub_factory';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
import { initDiffStatsDropdown } from './init_diff_stats_dropdown';
@@ -161,6 +162,23 @@ function toggleLoader(state) {
$('.mr-loading-status .loading').toggleClass('hide', !state);
}
+function getActionFromHref(href) {
+ let action = new URL(href).pathname.match(/\/(commits|diffs|pipelines).*$/);
+
+ if (action) {
+ action = action[0].replace(/(^\/|\.html)/g, '');
+ } else {
+ action = 'show';
+ }
+
+ return action;
+}
+
+const pageBundles = {
+ show: () => import(/* webpackPrefetch: true */ '~/mr_notes/init_notes'),
+ diffs: () => import(/* webpackPrefetch: true */ '~/diffs'),
+};
+
export default class MergeRequestTabs {
constructor({ action, setUrl, stubLocation } = {}) {
this.mergeRequestTabs = document.querySelector('.merge-request-tabs-container');
@@ -186,10 +204,10 @@ export default class MergeRequestTabs {
this.currentTab = null;
this.diffsLoaded = false;
- this.pipelinesLoaded = false;
this.commitsLoaded = false;
this.fixedLayoutPref = null;
this.eventHub = createEventHub();
+ this.loadedPages = { [action]: true };
this.setUrl = setUrl !== undefined ? setUrl : true;
this.setCurrentAction = this.setCurrentAction.bind(this);
@@ -206,12 +224,11 @@ export default class MergeRequestTabs {
bindEvents() {
$('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
- window.addEventListener('popstate', (event) => {
- if (event.state && event.state.action) {
- this.tabShown(event.state.action, event.target.location);
- this.currentAction = event.state.action;
- this.eventHub.$emit('MergeRequestTabChange', this.getCurrentAction());
- }
+ window.addEventListener('popstate', () => {
+ const action = getActionFromHref(location.href);
+
+ this.tabShown(action, location.href);
+ this.eventHub.$emit('MergeRequestTabChange', action);
});
}
@@ -252,17 +269,18 @@ export default class MergeRequestTabs {
} else if (action) {
const href = e.currentTarget.getAttribute('href');
this.tabShown(action, href);
-
- if (this.setUrl) {
- this.setCurrentAction(action);
- }
}
}
}
tabShown(action, href, shouldScroll = true) {
+ toggleLoader(false);
+
if (action !== this.currentTab && this.mergeRequestTabs) {
this.currentTab = action;
+ if (this.setUrl) {
+ this.setCurrentAction(action);
+ }
if (this.mergeRequestTabPanesAll) {
this.mergeRequestTabPanesAll.forEach((el) => {
@@ -282,6 +300,20 @@ export default class MergeRequestTabs {
const tab = this.mergeRequestTabs.querySelector(`.${action}-tab`);
if (tab) tab.classList.add('active');
+ if (!this.loadedPages[action] && action in pageBundles) {
+ toggleLoader(true);
+ pageBundles[action]()
+ .then(({ default: init }) => {
+ toggleLoader(false);
+ init();
+ this.loadedPages[action] = true;
+ })
+ .catch(() => {
+ toggleLoader(false);
+ createAlert({ message: __('MergeRequest|Failed to load the page') });
+ });
+ }
+
if (window.gon?.features?.movedMrSidebar) {
this.expandSidebar?.forEach((el) =>
el.classList.toggle('gl-display-none!', action !== 'show'),
@@ -334,7 +366,7 @@ export default class MergeRequestTabs {
this.commitPipelinesTable = destroyPipelines(this.commitPipelinesTable);
}
- $('.detail-page-description').renderGFM();
+ renderGFM(document.querySelector('.detail-page-description'));
if (shouldScroll) this.recallScroll(action);
} else if (action === this.currentAction) {
@@ -398,7 +430,7 @@ export default class MergeRequestTabs {
// Ensure parameters and hash come along for the ride
newState += location.search + location.hash;
- if (window.history.state && window.history.state.url && window.location.pathname !== newState) {
+ if (window.location.pathname !== newState) {
window.history.pushState(
{
url: newState,
@@ -477,8 +509,6 @@ export default class MergeRequestTabs {
return;
}
- toggleLoader(true);
-
loadDiffs({
// We extract pathname for the current Changes tab anchor href
// some pages like MergeRequestsController#new has query parameters on that anchor
@@ -496,9 +526,6 @@ export default class MergeRequestTabs {
createAlert({
message: __('An error occurred while fetching this tab.'),
});
- })
- .finally(() => {
- toggleLoader(false);
});
}
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index b7629ba001f..4a675cf7563 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -1,12 +1,7 @@
<script>
-import {
- GlIntersectionObserver,
- GlLink,
- GlSprintf,
- GlBadge,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlIntersectionObserver, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
import { mapGetters, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -28,7 +23,7 @@ export default {
ClipboardButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
inject: {
diff --git a/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue b/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue
new file mode 100644
index 00000000000..cd2e25793f4
--- /dev/null
+++ b/app/assets/javascripts/merge_requests/components/target_project_dropdown.vue
@@ -0,0 +1,87 @@
+<script>
+import { GlListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { createAlert } from '~/flash';
+import { __ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+export default {
+ components: {
+ GlListbox,
+ },
+ inject: {
+ targetProjectsPath: {
+ type: String,
+ required: true,
+ },
+ currentProject: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ currentProject: this.currentProject,
+ selected: this.currentProject.value,
+ isLoading: false,
+ projects: [],
+ };
+ },
+ methods: {
+ async fetchProjects(search = '') {
+ this.isLoading = true;
+
+ try {
+ const { data } = await axios.get(this.targetProjectsPath, {
+ params: { search },
+ });
+
+ this.projects = data.map((p) => ({
+ value: `${p.id}`,
+ text: p.full_path.replace(/^\//, ''),
+ refsUrl: p.refs_url,
+ }));
+ this.isLoading = false;
+ } catch {
+ createAlert({
+ message: __('Error fetching target projects. Please try again.'),
+ primaryButton: { text: __('Try again'), clickHandler: () => this.fetchProjects(search) },
+ });
+ }
+ },
+ searchProjects: debounce(function searchProjects(search) {
+ this.fetchProjects(search);
+ }, 500),
+ selectProject(projectId) {
+ this.currentProject = this.projects.find((p) => p.value === projectId);
+
+ this.$emit('project-selected', this.currentProject.refsUrl);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <input
+ id="merge_request_target_project_id"
+ type="hidden"
+ :value="currentProject.value"
+ name="merge_request[target_project_id]"
+ data-testid="target-project-input"
+ />
+ <gl-listbox
+ v-model="selected"
+ :items="projects"
+ :toggle-text="currentProject.text"
+ :header-text="__('Select target project')"
+ :searching="isLoading"
+ searchable
+ class="gl-w-full dropdown-target-project"
+ toggle-class="gl-align-items-flex-start! gl-justify-content-start! mr-compare-dropdown js-target-project"
+ @shown="fetchProjects"
+ @search="searchProjects"
+ @select="selectProject"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
deleted file mode 100644
index 73cdfbc44b0..00000000000
--- a/app/assets/javascripts/ml/experiment_tracking/components/experiment.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
-import { GlTable } from '@gitlab/ui';
-import IncubationAlert from './incubation_alert.vue';
-
-export default {
- name: 'ShowMlExperiment',
- components: {
- GlTable,
- IncubationAlert,
- },
- inject: ['candidates', 'metricNames', 'paramNames'],
- computed: {
- fields() {
- return [...this.paramNames, ...this.metricNames];
- },
- },
-};
-</script>
-
-<template>
- <div>
- <incubation-alert />
-
- <h3>
- {{ __('Experiment Candidates') }}
- </h3>
-
- <gl-table
- :fields="fields"
- :items="candidates"
- :empty-text="__('This Experiment has no logged Candidates')"
- show-empty
- class="gl-mt-0!"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
index 51c1e935677..42f6394ed68 100644
--- a/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/components/incubation_alert.vue
@@ -8,8 +8,8 @@ export default {
contentLabel: __(
'GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited',
),
- learnMoreLabel: __('Learn More'),
- feedbackLabel: __('Feedback and Updates'),
+ learnMoreLabel: __('Learn more'),
+ feedbackLabel: __('Feedback'),
},
name: 'MlopsIncubationAlert',
components: { GlAlert, GlLink },
@@ -37,7 +37,7 @@ export default {
:title="$options.i18n.titleLabel"
variant="warning"
:primary-button-text="$options.i18n.feedbackLabel"
- primary-button-link="https://gitlab.com/groups/gitlab-org/-/epics/8560"
+ primary-button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
@dismiss="dismissAlert"
>
{{ $options.i18n.contentLabel }}
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
new file mode 100644
index 00000000000..5f54f24e24c
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_candidate.vue
@@ -0,0 +1,94 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IncubationAlert from './incubation_alert.vue';
+
+export default {
+ name: 'MlCandidate',
+ components: {
+ IncubationAlert,
+ GlLink,
+ },
+ inject: ['candidate'],
+ i18n: {
+ titleLabel: __('Model candidate details'),
+ infoLabel: __('Info'),
+ idLabel: __('ID'),
+ statusLabel: __('Status'),
+ experimentLabel: __('Experiment'),
+ artifactsLabel: __('Artifacts'),
+ parametersLabel: __('Parameters'),
+ metricsLabel: __('Metrics'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-alert />
+
+ <h3>
+ {{ $options.i18n.titleLabel }}
+ </h3>
+
+ <table class="candidate-details">
+ <tbody>
+ <tr class="divider"></tr>
+
+ <tr>
+ <td class="gl-text-secondary gl-font-weight-bold">{{ $options.i18n.infoLabel }}</td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.idLabel }}</td>
+ <td>{{ candidate.info.iid }}</td>
+ </tr>
+
+ <tr>
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.statusLabel }}</td>
+ <td>{{ candidate.info.status }}</td>
+ </tr>
+
+ <tr>
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.experimentLabel }}</td>
+ <td>
+ <gl-link :href="candidate.info.path_to_experiment">{{
+ candidate.info.experiment_name
+ }}</gl-link>
+ </td>
+ </tr>
+
+ <tr v-if="candidate.info.path_to_artifact">
+ <td></td>
+ <td class="gl-font-weight-bold">{{ $options.i18n.artifactsLabel }}</td>
+ <td>
+ <gl-link :href="candidate.info.path_to_artifact">{{
+ $options.i18n.artifactsLabel
+ }}</gl-link>
+ </td>
+ </tr>
+
+ <tr class="divider"></tr>
+
+ <tr v-for="(param, index) in candidate.params" :key="param.name">
+ <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
+ {{ $options.i18n.parametersLabel }}
+ </td>
+ <td v-else></td>
+ <td class="gl-font-weight-bold">{{ param.name }}</td>
+ <td>{{ param.value }}</td>
+ </tr>
+
+ <tr class="divider"></tr>
+
+ <tr v-for="(metric, index) in candidate.metrics" :key="metric.name">
+ <td v-if="index == 0" class="gl-text-secondary gl-font-weight-bold">
+ {{ $options.i18n.metricsLabel }}
+ </td>
+ <td v-else></td>
+ <td class="gl-font-weight-bold">{{ metric.name }}</td>
+ <td>{{ metric.value }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
new file mode 100644
index 00000000000..f8e269d3b57
--- /dev/null
+++ b/app/assets/javascripts/ml/experiment_tracking/components/ml_experiment.vue
@@ -0,0 +1,59 @@
+<script>
+import { GlTable, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+import IncubationAlert from './incubation_alert.vue';
+
+export default {
+ name: 'MlExperiment',
+ components: {
+ GlTable,
+ GlLink,
+ IncubationAlert,
+ },
+ inject: ['candidates', 'metricNames', 'paramNames'],
+ computed: {
+ fields() {
+ return [
+ ...this.paramNames,
+ ...this.metricNames,
+ { key: 'details', label: '' },
+ { key: 'artifact', label: '' },
+ ];
+ },
+ },
+ i18n: {
+ titleLabel: __('Experiment candidates'),
+ emptyStateLabel: __('This experiment has no logged candidates'),
+ artifactsLabel: __('Artifacts'),
+ detailsLabel: __('Details'),
+ },
+};
+</script>
+
+<template>
+ <div>
+ <incubation-alert />
+
+ <h3>
+ {{ $options.i18n.titleLabel }}
+ </h3>
+
+ <gl-table
+ :fields="fields"
+ :items="candidates"
+ :empty-text="$options.i18n.emptyStateLabel"
+ show-empty
+ class="gl-mt-0!"
+ >
+ <template #cell(artifact)="data">
+ <gl-link v-if="data.value" :href="data.value" target="_blank">{{
+ $options.i18n.artifactsLabel
+ }}</gl-link>
+ </template>
+
+ <template #cell(details)="data">
+ <gl-link :href="data.value">{{ $options.i18n.detailsLabel }}</gl-link>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
index ae079da0b0b..da4c92df711 100644
--- a/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
+++ b/app/assets/javascripts/monitoring/components/charts/empty_chart.vue
@@ -1,11 +1,11 @@
<script>
import chartEmptyStateIllustration from '@gitlab/svgs/dist/illustrations/chart-empty-state.svg';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { chartHeight } from '../../constants';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
data() {
return {
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index b6ad2d21757..2c185794d17 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -391,11 +391,7 @@ export default {
};
</script>
<template>
- <div
- class="prometheus-graphs"
- data-qa-selector="prometheus_graphs_content"
- data-testid="prometheus-graphs"
- >
+ <div class="prometheus-graphs" data-testid="prometheus-graphs">
<div>
<gl-alert
v-if="!isDeprecationNoticeDismissed"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 7f8fb3c223d..d67154b7697 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -146,7 +146,6 @@ export default {
<gl-dropdown
v-gl-tooltip
data-testid="actions-menu"
- data-qa-selector="actions_menu_dropdown"
right
no-caret
toggle-class="gl-px-3!"
@@ -223,7 +222,6 @@ export default {
<gl-dropdown-item
v-if="isMenuItemEnabled.editDashboard"
:href="selectedDashboard ? selectedDashboard.project_blob_path : null"
- data-qa-selector="edit_dashboard_button_enabled"
data-testid="edit-dashboard-item-enabled"
>
{{ $options.i18n.editDashboard }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 90d2498ac19..7bb0d3874d1 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -173,7 +173,6 @@ export default {
<div class="gl-mb-3 gl-mr-3 gl-display-flex gl-sm-display-block">
<dashboards-dropdown
id="monitor-dashboards-dropdown"
- data-qa-selector="dashboards_filter_dropdown"
class="flex-grow-1"
toggle-class="dropdown-menu-toggle"
:default-branch="defaultBranch"
@@ -188,7 +187,6 @@ export default {
id="monitor-environments-dropdown"
ref="monitorEnvironmentsDropdown"
class="flex-grow-1"
- data-qa-selector="environments_dropdown"
data-testid="environments-dropdown"
toggle-class="dropdown-menu-toggle"
menu-class="monitor-environment-dropdown-menu"
@@ -225,7 +223,6 @@ export default {
<date-time-picker
ref="dateTimePicker"
class="flex-grow-1 show-last-dropdown"
- data-qa-selector="range_picker_dropdown"
:value="selectedTimeRange"
:options="$options.timeRanges"
:utc="displayUtc"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 7e7dcef7639..9ad6da35d6b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -292,11 +292,7 @@ export default {
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
<gl-loading-icon size="sm" />
</div>
- <div
- v-if="isContextualMenuShown"
- ref="contextualMenu"
- data-qa-selector="prometheus_graph_widgets"
- >
+ <div v-if="isContextualMenuShown" ref="contextualMenu">
<div data-testid="dropdown-wrapper" class="d-flex align-items-center">
<!--
This component should be replaced with a variant developed
@@ -310,7 +306,6 @@ export default {
:text-sr-only="true"
toggle-class="gl-px-3!"
no-caret
- data-qa-selector="prometheus_widgets_dropdown"
right
:title="__('More actions')"
>
@@ -339,7 +334,6 @@ export default {
ref="copyChartLink"
v-track-event="generateLinkToChartOptions(clipboardText)"
:data-clipboard-text="clipboardText"
- data-qa-selector="generate_chart_link_menu_item"
@click="showToast(clipboardText)"
>
{{ __('Copy link to chart') }}
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
index 8efea2bfc3e..e8a9c24f5c2 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel_builder.vue
@@ -100,7 +100,7 @@ export default {
<gl-form-textarea
id="panel-yml-input"
v-model="yml"
- class="gl-h-200! gl-font-monospace! gl-font-size-monospace!"
+ class="gl-h-200! gl-font-monospace!"
/>
</gl-form-group>
<div class="gl-text-right">
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
index a63008aa382..9ad14b3d52e 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue
@@ -104,13 +104,7 @@ export default {
label-size="sm"
label-for="fileName"
>
- <gl-form-input
- id="fileName"
- ref="fileName"
- v-model="form.fileName"
- data-qa-selector="duplicate_dashboard_filename_field"
- :required="true"
- />
+ <gl-form-input id="fileName" ref="fileName" v-model="form.fileName" :required="true" />
</gl-form-group>
<gl-form-group :label="__('Branch')" label-size="sm" label-for="branch">
<gl-form-radio-group
diff --git a/app/assets/javascripts/monitoring/components/group_empty_state.vue b/app/assets/javascripts/monitoring/components/group_empty_state.vue
index 0365fc66331..a67770b93be 100644
--- a/app/assets/javascripts/monitoring/components/group_empty_state.vue
+++ b/app/assets/javascripts/monitoring/components/group_empty_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlEmptyState, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlEmptyState } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, sprintf } from '~/locale';
import { metricStates } from '../constants';
diff --git a/app/assets/javascripts/monitoring/components/refresh_button.vue b/app/assets/javascripts/monitoring/components/refresh_button.vue
index 544fe10f26e..55c602db33d 100644
--- a/app/assets/javascripts/monitoring/components/refresh_button.vue
+++ b/app/assets/javascripts/monitoring/components/refresh_button.vue
@@ -11,8 +11,6 @@ import Visibility from 'visibilityjs';
import { mapActions } from 'vuex';
import { n__, __, s__ } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-
const makeInterval = (length = 0, unit = 's') => {
const shortLabel = `${length}${unit}`;
switch (unit) {
@@ -58,7 +56,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
data() {
return {
refreshInterval: null,
@@ -66,12 +63,6 @@ export default {
};
},
computed: {
- disableMetricDashboardRefreshRate() {
- // Can refresh rates impact performance?
- // Add "negative" feature flag called `disable_metric_dashboard_refresh_rate`
- // See more at: https://gitlab.com/gitlab-org/gitlab/-/issues/229831
- return this.glFeatures.disableMetricDashboardRefreshRate;
- },
dropdownText() {
return this.refreshInterval?.shortLabel ?? __('Off');
},
@@ -156,12 +147,7 @@ export default {
icon="retry"
@click="refresh"
/>
- <gl-dropdown
- v-if="!disableMetricDashboardRefreshRate"
- v-gl-tooltip
- :title="s__('Metrics|Set refresh rate')"
- :text="dropdownText"
- >
+ <gl-dropdown v-gl-tooltip :title="s__('Metrics|Set refresh rate')" :text="dropdownText">
<gl-dropdown-item
is-check-item
:is-checked="refreshInterval === null"
diff --git a/app/assets/javascripts/monitoring/components/variables_section.vue b/app/assets/javascripts/monitoring/components/variables_section.vue
index 493d37ce263..971f188e9f3 100644
--- a/app/assets/javascripts/monitoring/components/variables_section.vue
+++ b/app/assets/javascripts/monitoring/components/variables_section.vue
@@ -37,11 +37,7 @@ export default {
};
</script>
<template>
- <div
- ref="variablesSection"
- class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section"
- data-qa-selector="variables_content"
- >
+ <div ref="variablesSection" class="d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 variables-section">
<div v-for="variable in variables" :key="variable.name" class="mb-1 pr-2 d-flex d-sm-block">
<component
:is="variableField(variable.type)"
@@ -50,7 +46,6 @@ export default {
:value="variable.value"
:name="variable.name"
:options="variable.options"
- data-qa-selector="variable_item"
@input="refreshDashboard(variable, $event)"
/>
</div>
diff --git a/app/assets/javascripts/monitoring/csv_export.js b/app/assets/javascripts/monitoring/csv_export.js
index eaeed4a54d4..7e15b659767 100644
--- a/app/assets/javascripts/monitoring/csv_export.js
+++ b/app/assets/javascripts/monitoring/csv_export.js
@@ -110,7 +110,7 @@ const csvData = (metricHeaders, metricValues) => {
// "If double-quotes are used to enclose fields, then a double-quote
// appearing inside a field must be escaped by preceding it with
// another double quote."
- // https://tools.ietf.org/html/rfc4180#page-2
+ // https://www.rfc-editor.org/rfc/rfc4180#page-2
const headers = metricHeaders.map((header) => `"${header.replace(/"/g, '""')}"`);
return {
diff --git a/app/assets/javascripts/monitoring/requests/index.js b/app/assets/javascripts/monitoring/requests/index.js
index 26fedb9c81c..8b65eec051f 100644
--- a/app/assets/javascripts/monitoring/requests/index.js
+++ b/app/assets/javascripts/monitoring/requests/index.js
@@ -1,13 +1,16 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, {
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { PROMETHEUS_TIMEOUT } from '../constants';
const cancellableBackOffRequest = (makeRequestCallback) =>
backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
- if (resp.status === statusCodes.NO_CONTENT) {
+ if (resp.status === HTTP_STATUS_NO_CONTENT) {
next();
} else {
stop(resp);
@@ -34,7 +37,7 @@ export const getPrometheusQueryData = (prometheusEndpoint, params, opts) =>
const { response = {} } = error;
if (
response.status === statusCodes.BAD_REQUEST ||
- response.status === statusCodes.UNPROCESSABLE_ENTITY ||
+ response.status === HTTP_STATUS_UNPROCESSABLE_ENTITY ||
response.status === statusCodes.SERVICE_UNAVAILABLE
) {
const { data } = response;
diff --git a/app/assets/javascripts/monitoring/utils.js b/app/assets/javascripts/monitoring/utils.js
index fd8749625da..0d849e1a2d8 100644
--- a/app/assets/javascripts/monitoring/utils.js
+++ b/app/assets/javascripts/monitoring/utils.js
@@ -39,7 +39,6 @@ export const stateAndPropsFromDataset = (dataset = {}) => {
// HTML attributes are always strings, parse other types.
dataProps.hasMetrics = parseBoolean(dataProps.hasMetrics);
dataProps.customMetricsAvailable = parseBoolean(dataProps.customMetricsAvailable);
- dataProps.prometheusAlertsAvailable = parseBoolean(dataProps.prometheusAlertsAvailable);
return {
initState: {
diff --git a/app/assets/javascripts/mr_notes/discussion_counter.js b/app/assets/javascripts/mr_notes/discussion_counter.js
new file mode 100644
index 00000000000..0bb63a7c0f9
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/discussion_counter.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import DiscussionCounter from '~/notes/components/discussion_counter.vue';
+import store from '~/mr_notes/stores';
+
+export function initDiscussionCounter() {
+ const el = document.getElementById('js-vue-discussion-counter');
+
+ if (el) {
+ const { blocksMerge } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'DiscussionCounter',
+ components: {
+ DiscussionCounter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-counter', {
+ props: {
+ blocksMerge: blocksMerge === 'true',
+ },
+ });
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
index c32a1f4c2ac..a202923bd21 100644
--- a/app/assets/javascripts/mr_notes/index.js
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -1,12 +1,8 @@
-import Vue from 'vue';
-import store from '~/mr_notes/stores';
import initCherryPickCommitModal from '~/projects/commit/init_cherry_pick_commit_modal';
import initRevertCommitModal from '~/projects/commit/init_revert_commit_modal';
-import initDiffsApp from '../diffs';
-import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
+import { initMrStateLazyLoad } from '~/mr_notes/init';
import MergeRequest from '../merge_request';
-import DiscussionCounter from '../notes/components/discussion_counter.vue';
-import initNotesApp from './init_notes';
+import { resetServiceWorkersPublicPath } from '../lib/utils/webpack';
export default function initMrNotes() {
resetServiceWorkersPublicPath();
@@ -17,36 +13,10 @@ export default function initMrNotes() {
action: mrShowNode.dataset.mrAction,
});
- initDiffsApp(store);
- initNotesApp();
+ initMrStateLazyLoad();
document.addEventListener('merged:UpdateActions', () => {
initRevertCommitModal('i_code_review_post_merge_submit_revert_modal');
initCherryPickCommitModal('i_code_review_post_merge_submit_cherry_pick_modal');
});
-
- requestIdleCallback(() => {
- const el = document.getElementById('js-vue-discussion-counter');
-
- if (el) {
- const { blocksMerge } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- name: 'DiscussionCounter',
- components: {
- DiscussionCounter,
- },
- store,
- render(createElement) {
- return createElement('discussion-counter', {
- props: {
- blocksMerge: blocksMerge === 'true',
- },
- });
- },
- });
- }
- });
}
diff --git a/app/assets/javascripts/mr_notes/init.js b/app/assets/javascripts/mr_notes/init.js
new file mode 100644
index 00000000000..aab3c41b4cf
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/init.js
@@ -0,0 +1,52 @@
+import { parseBoolean } from '~/lib/utils/common_utils';
+import store from '~/mr_notes/stores';
+import { getLocationHash } from '~/lib/utils/url_utility';
+import eventHub from '~/notes/event_hub';
+import { initReviewBar } from '~/batch_comments';
+import { initDiscussionCounter } from '~/mr_notes/discussion_counter';
+import { initOverviewTabCounter } from '~/mr_notes/init_count';
+
+function setupMrNotesState(notesDataset) {
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
+ noteableData.discussion_locked = parseBoolean(notesDataset.isLocked);
+ const notesData = JSON.parse(notesDataset.notesData);
+ const currentUserData = JSON.parse(notesDataset.currentUserData);
+ const endpoints = { metadata: notesDataset.endpointMetadata };
+
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', noteableData);
+ store.dispatch('setUserData', currentUserData);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ store.dispatch('setEndpoints', endpoints);
+ eventHub.$once('fetchNotesData', () => store.dispatch('fetchNotes'));
+}
+
+export function initMrStateLazyLoad() {
+ store.dispatch('setActiveTab', window.mrTabs.getCurrentAction());
+ window.mrTabs.eventHub.$on('MergeRequestTabChange', (value) =>
+ store.dispatch('setActiveTab', value),
+ );
+
+ const discussionsEl = document.getElementById('js-vue-mr-discussions');
+ const notesDataset = discussionsEl.dataset;
+ let stop = () => {};
+ stop = store.watch(
+ (state) => state.page.activeTab,
+ (activeTab) => {
+ // prevent loading MR state on commits and pipelines pages
+ // this is due to them having a shared controller with the Overview page
+ if (['diffs', 'show'].includes(activeTab)) {
+ setupMrNotesState(notesDataset);
+ requestIdleCallback(() => {
+ initReviewBar();
+ initOverviewTabCounter();
+ initDiscussionCounter();
+ });
+ stop();
+ }
+ },
+ { immediate: true },
+ );
+}
diff --git a/app/assets/javascripts/mr_notes/init_count.js b/app/assets/javascripts/mr_notes/init_count.js
new file mode 100644
index 00000000000..3e924ebd9d5
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/init_count.js
@@ -0,0 +1,13 @@
+import store from '~/mr_notes/stores';
+
+export function initOverviewTabCounter() {
+ const discussionsCount = document.querySelector('.js-discussions-count');
+ store.watch(
+ (state, getters) => getters.discussionTabCounter,
+ (val) => {
+ if (typeof val !== 'undefined') {
+ discussionsCount.textContent = val;
+ }
+ },
+ );
+}
diff --git a/app/assets/javascripts/mr_notes/init_notes.js b/app/assets/javascripts/mr_notes/init_notes.js
index 3a67e7925c3..e10605609b0 100644
--- a/app/assets/javascripts/mr_notes/init_notes.js
+++ b/app/assets/javascripts/mr_notes/init_notes.js
@@ -1,8 +1,8 @@
-import $ from 'jquery';
import Vue from 'vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
import store from '~/mr_notes/stores';
+import notesEventHub from '~/notes/event_hub';
import discussionNavigator from '../notes/components/discussion_navigator.vue';
import NotesApp from '../notes/components/notes_app.vue';
import { getNotesFilterData } from '../notes/utils/get_notes_filter_data';
@@ -36,13 +36,12 @@ export default () => {
endpoints: {
metadata: notesDataset.endpointMetadata,
},
- currentUserData: JSON.parse(notesDataset.currentUserData),
notesData: JSON.parse(notesDataset.notesData),
helpPagePath: notesDataset.helpPagePath,
};
},
computed: {
- ...mapGetters(['discussionTabCounter']),
+ ...mapGetters(['isNotesFetched']),
...mapState({
activeTab: (state) => state.page.activeTab,
}),
@@ -51,15 +50,6 @@ export default () => {
},
},
watch: {
- discussionTabCounter() {
- if (window.gon?.features?.paginatedMrDiscussions) {
- if (this.$store.state.notes.doneFetchingBatchDiscussions) {
- this.updateDiscussionTabCounter();
- }
- } else {
- this.updateDiscussionTabCounter();
- }
- },
isShowTabActive: {
handler(newVal) {
if (newVal) {
@@ -70,25 +60,16 @@ export default () => {
},
},
created() {
- this.setActiveTab(window.mrTabs.getCurrentAction());
this.setEndpoints(this.endpoints);
+ if (!this.isNotesFetched) {
+ notesEventHub.$emit('fetchNotesData');
+ }
+
this.fetchMrMetadata();
},
- mounted() {
- this.notesCountBadge = $('.issuable-details').find('.notes-tab .badge');
- $(document).on('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$on('MergeRequestTabChange', this.setActiveTab);
- },
- beforeDestroy() {
- $(document).off('visibilitychange', this.updateDiscussionTabCounter);
- window.mrTabs.eventHub.$off('MergeRequestTabChange', this.setActiveTab);
- },
methods: {
- ...mapActions(['setActiveTab', 'setEndpoints', 'fetchMrMetadata']),
- updateDiscussionTabCounter() {
- this.notesCountBadge.text(this.discussionTabCounter);
- },
+ ...mapActions(['setEndpoints', 'fetchMrMetadata']),
},
render(createElement) {
// NOTE: Even though `discussionNavigator` is added to the `notes-app`,
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
new file mode 100644
index 00000000000..ef59140115d
--- /dev/null
+++ b/app/assets/javascripts/nav/components/new_nav_toggle.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlBadge, GlToggle } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ badgeLabel: s__('NorthstarNavigation|Alpha'),
+ sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
+ toggleMenuItemLabel: s__('NorthstarNavigation|New navigation'),
+ toggleLabel: s__('NorthstarNavigation|Toggle new navigation'),
+ updateError: s__(
+ 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
+ ),
+ },
+ components: {
+ GlBadge,
+ GlToggle,
+ },
+ props: {
+ enabled: {
+ type: Boolean,
+ required: true,
+ },
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isEnabled: this.enabled,
+ };
+ },
+ methods: {
+ async toggleNav() {
+ try {
+ await axios.put(this.endpoint, { user: { use_new_navigation: !this.enabled } });
+ window.location.reload();
+ } catch (error) {
+ createAlert({
+ message: this.$options.i18n.updateError,
+ error,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <li>
+ <div
+ class="gl-px-4 gl-py-2 gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <b>{{ $options.i18n.sectionTitle }}</b>
+ <gl-badge>{{ $options.i18n.badgeLabel }}</gl-badge>
+ </div>
+
+ <div class="menu-item gl-display-flex! gl-justify-content-space-between gl-align-items-center">
+ {{ $options.i18n.toggleMenuItemLabel }}
+ <gl-toggle
+ v-model="isEnabled"
+ :label="$options.i18n.toggleLabel"
+ label-position="hidden"
+ @change="toggleNav"
+ />
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index ef36e58374c..a7c2e572037 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,15 +1,9 @@
/* eslint-disable func-names, no-return-assign, @gitlab/require-i18n-strings */
-
-import $ from 'jquery';
-import RefSelectDropdown from './ref_select_dropdown';
-
export default class NewBranchForm {
- constructor(form, availableRefs) {
+ constructor(form) {
this.validate = this.validate.bind(this);
this.branchNameError = form.querySelector('.js-branch-name-error');
this.name = form.querySelector('.js-branch-name');
- this.ref = form.querySelector('#ref');
- new RefSelectDropdown($('.js-branch-select'), availableRefs); // eslint-disable-line no-new
this.setupRestrictions();
this.addBinding();
this.init();
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 9aa6abd9d8c..2caa93c3c93 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -1,7 +1,7 @@
<script>
import katex from 'katex';
import { marked } from 'marked';
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { sanitize } from '~/lib/dompurify';
import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue';
diff --git a/app/assets/javascripts/notebook/cells/output/html.vue b/app/assets/javascripts/notebook/cells/output/html.vue
index 5437a607e8a..74a5dd3806d 100644
--- a/app/assets/javascripts/notebook/cells/output/html.vue
+++ b/app/assets/javascripts/notebook/cells/output/html.vue
@@ -1,14 +1,10 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import Prompt from '../prompt.vue';
export default {
components: {
Prompt,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
props: {
count: {
type: Number,
@@ -28,12 +24,6 @@ export default {
return this.index === 0;
},
},
- safeHtmlConfig: {
- ADD_TAGS: ['use'], // to support icon SVGs
- FORBID_TAGS: ['style'],
- FORBID_ATTR: ['style'],
- ALLOW_DATA_ATTR: false,
- },
};
</script>
diff --git a/app/assets/javascripts/notebook/cells/output/latex.vue b/app/assets/javascripts/notebook/cells/output/latex.vue
index d0ed963b55d..55f97fee3dc 100644
--- a/app/assets/javascripts/notebook/cells/output/latex.vue
+++ b/app/assets/javascripts/notebook/cells/output/latex.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import 'mathjax/es5/tex-svg';
import Prompt from '../prompt.vue';
diff --git a/app/assets/javascripts/notebook/cells/output/markdown.vue b/app/assets/javascripts/notebook/cells/output/markdown.vue
index 5da057dee72..ad74e28ac74 100644
--- a/app/assets/javascripts/notebook/cells/output/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/output/markdown.vue
@@ -1,5 +1,4 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Prompt from '../prompt.vue';
import Markdown from '../markdown.vue';
@@ -9,9 +8,6 @@ export default {
Prompt,
Markdown,
},
- directives: {
- SafeHtml,
- },
props: {
count: {
type: Number,
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 0d7ff022f8f..2ccb9a0b514 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -7,7 +7,7 @@ import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { badgeState } from '~/issuable/components/status_box.vue';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
capitalizeFirstCharacter,
convertToCamelCase,
@@ -28,8 +28,6 @@ import CommentTypeDropdown from './comment_type_dropdown.vue';
import DiscussionLockedWidget from './discussion_locked_widget.vue';
import NoteSignedOutWidget from './note_signed_out_widget.vue';
-const { UNPROCESSABLE_ENTITY } = httpStatusCodes;
-
export default {
name: 'CommentForm',
i18n: COMMENT_FORM,
@@ -198,7 +196,7 @@ export default {
'toggleIssueLocalState',
]),
handleSaveError({ data, status }) {
- if (status === UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
+ if (status === HTTP_STATUS_UNPROCESSABLE_ENTITY && data.errors?.commands_only?.length) {
this.errors = data.errors.commands_only;
} else {
this.errors = [this.$options.i18n.GENERIC_UNSUBMITTABLE_NETWORK];
diff --git a/app/assets/javascripts/notes/components/diff_discussion_header.vue b/app/assets/javascripts/notes/components/diff_discussion_header.vue
index cf6474270a2..f949142d90a 100644
--- a/app/assets/javascripts/notes/components/diff_discussion_header.vue
+++ b/app/assets/javascripts/notes/components/diff_discussion_header.vue
@@ -1,7 +1,8 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { truncateSha } from '~/lib/utils/text_utility';
import { s__, __, sprintf } from '~/locale';
import NoteEditedText from './note_edited_text.vue';
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
index 3bdf8349a12..aabdc1c99b6 100644
--- a/app/assets/javascripts/notes/components/diff_with_note.vue
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ImageDiffOverlay from '~/diffs/components/image_diff_overlay.vue';
import { getDiffMode } from '~/diffs/store/utils';
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 930876e90b1..c15c11ed9db 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -19,6 +19,7 @@ export default {
editCommentLabel: __('Edit comment'),
deleteCommentLabel: __('Delete comment'),
moreActionsLabel: __('More actions'),
+ reportAbuse: __('Report abuse to administrator'),
},
name: 'NoteActions',
components: {
@@ -362,7 +363,7 @@ export default {
<!-- eslint-enable @gitlab/vue-no-data-toggle -->
<ul class="dropdown-menu more-actions-dropdown dropdown-open-left">
<gl-dropdown-item v-if="canReportAsAbuse" :href="reportAbusePath">
- {{ __('Report abuse to admin') }}
+ {{ $options.i18n.reportAbuse }}
</gl-dropdown-item>
<gl-dropdown-item
v-if="noteUrl"
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 82c125b79ce..20cf21cd1b6 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -1,12 +1,10 @@
<script>
-import $ from 'jquery';
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
-
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
-import '~/behaviors/markdown/render_gfm';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import autosave from '../mixins/autosave';
import NoteAttachment from './note_attachment.vue';
import NoteAwardsList from './note_awards_list.vue';
@@ -22,7 +20,7 @@ export default {
Suggestions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [autosave],
props: {
@@ -122,7 +120,7 @@ export default {
'removeSuggestionInfoFromBatch',
]),
renderGFM() {
- $(this.$refs['note-body']).renderGFM();
+ renderGFM(this.$refs['note-body']);
},
handleFormUpdate(noteText, parentElement, callback, resolveDiscussion) {
this.$emit('handleFormUpdate', { noteText, parentElement, callback, resolveDiscussion });
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 63c7010983e..36f7d720e48 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,17 +1,10 @@
<script>
-import {
- GlIcon,
- GlBadge,
- GlLoadingIcon,
- GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlBadge, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
components: {
TimeAgoTooltip,
GitlabTeamMemberBadge: () =>
@@ -21,7 +14,6 @@ export default {
GlLoadingIcon,
},
directives: {
- SafeHtml,
GlTooltip: GlTooltipDirective,
},
props: {
diff --git a/app/assets/javascripts/notes/components/note_signed_out_widget.vue b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
index 593933016e1..94636b3e47b 100644
--- a/app/assets/javascripts/notes/components/note_signed_out_widget.vue
+++ b/app/assets/javascripts/notes/components/note_signed_out_widget.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index b668d6ec182..ff801cdccea 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -235,7 +235,7 @@ export default {
this.saveNote(replyData)
.then((res) => {
- if (res.hasFlash !== true) {
+ if (res.hasAlert !== true) {
this.isReplying = false;
clearDraft(this.autosaveKey);
}
@@ -307,7 +307,7 @@ export default {
:draft="draftForDiscussion(discussion.reply_id)"
:line="line"
/>
- <div
+ <li
v-else-if="canShowReplyActions && showReplies"
:class="{ 'is-replying': isReplying }"
class="discussion-reply-holder gl-border-t-0! clearfix"
@@ -334,7 +334,7 @@ export default {
@cancelForm="cancelReplyForm"
/>
<note-signed-out-widget v-if="!isLoggedIn" />
- </div>
+ </li>
</template>
</discussion-notes>
</component>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 8ce0c2f8648..826e7e5a3d0 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -1,16 +1,18 @@
<script>
-import { GlSprintf, GlSafeHtmlDirective as SafeHtml, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlSprintf, GlAvatarLink, GlAvatar } from '@gitlab/ui';
import $ from 'jquery';
import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
import { createAlert } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_GONE } from '~/lib/utils/http_status';
import { ignoreWhilePending } from '~/lib/utils/ignore_while_pending';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { __, s__, sprintf } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
@@ -286,7 +288,7 @@ export default {
this.isEditing = false;
this.isRequesting = false;
this.oldContent = null;
- $(this.$refs.noteBody.$el).renderGFM();
+ renderGFM(this.$refs.noteBody.$el);
this.$refs.noteBody.resetAutoSave();
this.$emit('updateSuccess');
},
@@ -336,7 +338,7 @@ export default {
callback();
})
.catch((response) => {
- if (response.status === httpStatusCodes.GONE) {
+ if (response.status === HTTP_STATUS_GONE) {
this.removeNote(this.note);
this.updateSuccess();
callback();
@@ -515,6 +517,9 @@ export default {
@handleFormUpdate="formUpdateHandler"
@cancelForm="formCancelHandler"
/>
+ <div class="timeline-discussion-body-footer">
+ <slot name="after-note-body"></slot>
+ </div>
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 7bb1a1a1bfe..fcf37217902 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,13 +1,11 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
-import { createAlert } from '~/flash';
-import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import { getLocationHash, doesHashExistInUrl } from '~/lib/utils/url_utility';
+import { getLocationHash } from '~/lib/utils/url_utility';
import PlaceholderNote from '~/vue_shared/components/notes/placeholder_note.vue';
import PlaceholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_note.vue';
@@ -57,11 +55,6 @@ export default {
default: undefined,
required: false,
},
- userData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
shouldShow: {
type: Boolean,
required: false,
@@ -90,16 +83,12 @@ export default {
'commentsDisabled',
'getNoteableData',
'userCanReply',
- 'discussionTabCounter',
'sortDirection',
'timelineEnabled',
]),
sortDirDesc() {
return this.sortDirection === constants.DESC;
},
- discussionTabCounterText() {
- return this.isLoading ? '' : this.discussionTabCounter;
- },
noteableType() {
return this.noteableData.noteableType;
},
@@ -147,11 +136,6 @@ export default {
this.renderSkeleton = !this.shouldShow;
});
},
- discussionTabCounterText(val) {
- if (this.discussionsCount) {
- this.discussionsCount.textContent = val;
- }
- },
isAppReady: {
handler(isReady) {
if (!isReady) return;
@@ -162,20 +146,7 @@ export default {
immediate: true,
},
},
- created() {
- this.discussionsCount = document.querySelector('.js-discussions-count');
-
- this.setNotesData(this.notesData);
- this.setNoteableData(this.noteableData);
- this.setUserData(this.userData);
- this.setTargetNoteHash(getLocationHash());
- eventHub.$once('fetchNotesData', this.fetchNotes);
- },
mounted() {
- if (this.shouldShow) {
- this.fetchNotes();
- }
-
const { parentElement } = this.$el;
if (parentElement && parentElement.classList.contains('js-vue-notes-event')) {
parentElement.addEventListener('toggleAward', (event) => {
@@ -200,23 +171,16 @@ export default {
},
methods: {
...mapActions([
- 'setFetchingState',
- 'setLoadingState',
- 'fetchDiscussions',
- 'poll',
'toggleAward',
- 'setNotesData',
- 'setNoteableData',
- 'setUserData',
'setLastFetchedAt',
'setTargetNoteHash',
'toggleDiscussion',
- 'setNotesFetchedState',
'expandDiscussion',
'startTaskList',
'convertToDiscussion',
'stopPolling',
'setConfidentiality',
+ 'fetchNotes',
]),
discussionIsIndividualNoteAndNotConverted(discussion) {
return discussion.individual_note && !this.convertedDisscussionIds.includes(discussion.id);
@@ -228,37 +192,6 @@ export default {
this.setTargetNoteHash(getLocationHash());
}
},
- fetchNotes() {
- if (this.isFetching) return null;
-
- this.setFetchingState(true);
-
- return this.fetchDiscussions(this.getFetchDiscussionsConfig())
- .then(this.initPolling)
- .then(() => {
- this.setLoadingState(false);
- this.setNotesFetchedState(true);
- eventHub.$emit('fetchedNotesData');
- this.setFetchingState(false);
- })
- .catch(() => {
- this.setLoadingState(false);
- this.setNotesFetchedState(true);
- createAlert({
- message: __('Something went wrong while fetching comments. Please try again.'),
- });
- });
- },
- initPolling() {
- if (this.isPollingInitialized) {
- return;
- }
-
- this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
-
- this.poll();
- this.isPollingInitialized = true;
- },
checkLocationHash() {
const hash = getLocationHash();
const noteId = hash && hash.replace(/^note_/, '');
@@ -278,24 +211,6 @@ export default {
.then(this.$nextTick)
.then(() => eventHub.$emit('startReplying', discussionId));
},
- getFetchDiscussionsConfig() {
- const defaultConfig = { path: this.getNotesDataByProp('discussionsPath') };
-
- const currentFilter =
- this.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
-
- if (
- doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
- currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
- ) {
- return {
- ...defaultConfig,
- filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
- persistFilter: false,
- };
- }
- return defaultConfig;
- },
},
systemNote: constants.SYSTEM_NOTE,
};
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index defcb0533b7..95263e666b2 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
import NotesApp from './components/notes_app.vue';
import { store } from './stores';
import { getNotesFilterData } from './utils/get_notes_filter_data';
@@ -13,6 +14,34 @@ export default () => {
const notesFilterProps = getNotesFilterData(el);
const showTimelineViewToggle = parseBoolean(el.dataset.showTimelineViewToggle);
+ const notesDataset = el.dataset;
+ const parsedUserData = JSON.parse(notesDataset.currentUserData);
+ const noteableData = JSON.parse(notesDataset.noteableData);
+ let currentUserData = {};
+
+ noteableData.noteableType = notesDataset.noteableType;
+ noteableData.targetType = notesDataset.targetType;
+ noteableData.discussion_locked = parseBoolean(noteableData.discussion_locked);
+
+ if (parsedUserData) {
+ currentUserData = {
+ id: parsedUserData.id,
+ name: parsedUserData.name,
+ username: parsedUserData.username,
+ avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
+ path: parsedUserData.path,
+ can_add_timeline_events: parseBoolean(notesDataset.canAddTimelineEvents),
+ };
+ }
+
+ const notesData = JSON.parse(notesDataset.notesData);
+
+ store.dispatch('setNotesData', notesData);
+ store.dispatch('setNoteableData', noteableData);
+ store.dispatch('setUserData', currentUserData);
+ store.dispatch('setTargetNoteHash', getLocationHash());
+ store.dispatch('fetchNotes');
+
// eslint-disable-next-line no-new
new Vue({
el,
@@ -25,30 +54,6 @@ export default () => {
showTimelineViewToggle,
},
data() {
- const notesDataset = el.dataset;
- const parsedUserData = JSON.parse(notesDataset.currentUserData);
- const noteableData = JSON.parse(notesDataset.noteableData);
- let currentUserData = {};
-
- noteableData.noteableType = notesDataset.noteableType;
- noteableData.targetType = notesDataset.targetType;
- if (noteableData.discussion_locked === null) {
- // discussion_locked has never been set for this issuable.
- // set to `false` for safety.
- noteableData.discussion_locked = false;
- }
-
- if (parsedUserData) {
- currentUserData = {
- id: parsedUserData.id,
- name: parsedUserData.name,
- username: parsedUserData.username,
- avatar_url: parsedUserData.avatar_path || parsedUserData.avatar_url,
- path: parsedUserData.path,
- can_add_timeline_events: parseBoolean(notesDataset.canAddTimelineEvents),
- };
- }
-
return {
noteableData,
currentUserData,
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index fcef26d720c..d290a8ccb84 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -2,14 +2,14 @@ import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
import Api from '~/api';
-import createFlash from '~/flash';
+import { createAlert, VARIANT_INFO } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
-import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
-import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/queries/update_issue_lock.mutation.graphql';
+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';
@@ -114,6 +114,39 @@ export const fetchDiscussions = (
});
};
+export const fetchNotes = ({ dispatch, getters }) => {
+ if (getters.isFetching) return null;
+
+ dispatch('setFetchingState', true);
+
+ return dispatch('fetchDiscussions', getters.getFetchDiscussionsConfig)
+ .then(() => dispatch('initPolling'))
+ .then(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ notesEventHub.$emit('fetchedNotesData');
+ dispatch('setFetchingState', false);
+ })
+ .catch(() => {
+ dispatch('setLoadingState', false);
+ dispatch('setNotesFetchedState', true);
+ createAlert({
+ message: __('Something went wrong while fetching comments. Please try again.'),
+ });
+ });
+};
+
+export const initPolling = ({ state, dispatch, getters, commit }) => {
+ if (state.isPollingInitialized) {
+ return;
+ }
+
+ dispatch('setLastFetchedAt', getters.getNotesDataByProp('lastFetchedAt'));
+
+ dispatch('poll');
+ commit(types.SET_IS_POLLING_INITIALIZED, true);
+};
+
export const fetchDiscussionsBatch = ({ commit, dispatch }, { path, config, cursor, perPage }) => {
const params = { ...config?.params, per_page: perPage };
@@ -270,7 +303,7 @@ export const promoteCommentToTimelineEvent = (
errorObj = error;
}
- createFlash({
+ createAlert({
message,
captureError,
error: errorObj,
@@ -465,9 +498,9 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- createFlash({
+ createAlert({
message: message || __('Commands applied'),
- type: 'notice',
+ variant: VARIANT_INFO,
parent: noteData.flashContainer,
});
}
@@ -490,7 +523,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
awardsHandler.scrollToAwards();
})
.catch(() => {
- createFlash({
+ createAlert({
message: __('Something went wrong while adding your award. Please try again.'),
parent: noteData.flashContainer,
});
@@ -529,11 +562,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const errorMsg = sprintf(__('Your comment could not be submitted because %{error}'), {
error: base[0].toLowerCase(),
});
- createFlash({
+ createAlert({
message: errorMsg,
parent: noteData.flashContainer,
});
- return { ...data, hasFlash: true };
+ return { ...data, hasAlert: true };
}
}
@@ -580,7 +613,7 @@ const getFetchDataParams = (state) => {
export const poll = ({ commit, state, getters, dispatch }) => {
const notePollOccurrenceTracking = create();
- let flashContainer;
+ let alert;
notePollOccurrenceTracking.handle(1, () => {
// Since polling halts internally after 1 failure, we manually try one more time
@@ -588,7 +621,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
});
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)
- flashContainer = createFlash({
+ alert = createAlert({
message: __('Something went wrong while fetching latest comments.'),
});
setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
@@ -608,7 +641,7 @@ export const poll = ({ commit, state, getters, dispatch }) => {
if (notePollOccurrenceTracking.count) {
notePollOccurrenceTracking.reset();
}
- flashContainer?.close();
+ alert?.dismiss();
},
errorCallback: () => notePollOccurrenceTracking.occur(),
});
@@ -681,7 +714,7 @@ export const filterDiscussion = ({ commit, dispatch }, { path, filter, persistFi
.catch(() => {
dispatch('setLoadingState', false);
dispatch('setNotesFetchedState', true);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching comments. Please try again.'),
});
});
@@ -726,7 +759,7 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
- createFlash({
+ createAlert({
message: flashMessage,
parent: flashContainer,
});
@@ -762,7 +795,7 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { message, fl
const flashMessage = errorMessage || defaultMessage;
- createFlash({
+ createAlert({
message: flashMessage,
parent: flashContainer,
});
@@ -804,7 +837,7 @@ export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersio
})
.catch((error) => {
dispatch('receiveDescriptionVersionError', error);
- createFlash({
+ createAlert({
message: __('Something went wrong while fetching description changes. Please try again.'),
});
});
@@ -838,7 +871,7 @@ export const softDeleteDescriptionVersion = (
})
.catch((error) => {
dispatch('receiveDeleteDescriptionVersionError', error);
- createFlash({
+ createAlert({
message: __('Something went wrong while deleting description changes. Please try again.'),
});
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 5ad7a811726..f6373f24b74 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -2,6 +2,7 @@ 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 * as constants from '../constants';
import { collapseSystemNotes } from './collapse_utils';
@@ -314,3 +315,22 @@ export const getSuggestionsFilePaths = (state) => () =>
return acc;
}, []);
+
+export const getFetchDiscussionsConfig = (state, getters) => {
+ const defaultConfig = { path: getters.getNotesDataByProp('discussionsPath') };
+
+ const currentFilter =
+ getters.getNotesDataByProp('notesFilter') || constants.DISCUSSION_FILTERS_DEFAULT_VALUE;
+
+ if (
+ doesHashExistInUrl(constants.NOTE_UNDERSCORE) &&
+ currentFilter !== constants.DISCUSSION_FILTERS_DEFAULT_VALUE
+ ) {
+ return {
+ ...defaultConfig,
+ filter: constants.DISCUSSION_FILTERS_DEFAULT_VALUE,
+ persistFilter: false,
+ };
+ }
+ return defaultConfig;
+};
diff --git a/app/assets/javascripts/notes/stores/modules/index.js b/app/assets/javascripts/notes/stores/modules/index.js
index 7ba1f470b05..81c4c42a49a 100644
--- a/app/assets/javascripts/notes/stores/modules/index.js
+++ b/app/assets/javascripts/notes/stores/modules/index.js
@@ -50,6 +50,7 @@ export default () => ({
descriptionVersions: {},
isTimelineEnabled: false,
isFetching: false,
+ isPollingInitialized: false,
},
actions,
getters,
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 42df6bc0980..bc1d5b5bba4 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -27,6 +27,7 @@ export const CLEAR_SUGGESTION_BATCH = 'CLEAR_SUGGESTION_BATCH';
export const CONVERT_TO_DISCUSSION = 'CONVERT_TO_DISCUSSION';
export const REMOVE_CONVERTED_DISCUSSION = 'REMOVE_CONVERTED_DISCUSSION';
export const UPDATE_ASSIGNEES = 'UPDATE_ASSIGNEES';
+export const SET_IS_POLLING_INITIALIZED = 'SET_IS_POLLING_INITIALIZED';
// DISCUSSION
export const COLLAPSE_DISCUSSION = 'COLLAPSE_DISCUSSION';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index 83c15c12eac..5d532b68f1b 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -428,4 +428,7 @@ export default {
[types.SET_PROMOTE_COMMENT_TO_TIMELINE_PROGRESS](state, value) {
state.isPromoteCommentToTimelineEventInProgress = value;
},
+ [types.SET_IS_POLLING_INITIALIZED](state, value) {
+ state.isPollingInitialized = value;
+ },
};
diff --git a/app/assets/javascripts/observability/components/observability_app.vue b/app/assets/javascripts/observability/components/observability_app.vue
index 4f5e27be46f..33d23ea043b 100644
--- a/app/assets/javascripts/observability/components/observability_app.vue
+++ b/app/assets/javascripts/observability/components/observability_app.vue
@@ -1,21 +1,69 @@
<script>
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { setUrlParams } from '~/lib/utils/url_utility';
+
+import { MESSAGE_EVENT_TYPE, OBSERVABILITY_ROUTES, SKELETON_VARIANT } from '../constants';
+import ObservabilitySkeleton from './skeleton/index.vue';
+
export default {
+ components: {
+ ObservabilitySkeleton,
+ },
props: {
observabilityIframeSrc: {
type: String,
required: true,
},
},
+ computed: {
+ iframeSrcWithParams() {
+ return setUrlParams(
+ { theme: darkModeEnabled() ? 'dark' : 'light', username: gon?.current_username },
+ this.observabilityIframeSrc,
+ );
+ },
+ getSkeletonVariant() {
+ switch (this.$route.path) {
+ case OBSERVABILITY_ROUTES.DASHBOARDS:
+ return SKELETON_VARIANT.DASHBOARDS;
+ case OBSERVABILITY_ROUTES.EXPLORE:
+ return SKELETON_VARIANT.EXPLORE;
+ case OBSERVABILITY_ROUTES.MANAGE:
+ return SKELETON_VARIANT.MANAGE;
+ default:
+ return SKELETON_VARIANT.DASHBOARDS;
+ }
+ },
+ },
mounted() {
window.addEventListener('message', this.messageHandler);
},
+ destroyed() {
+ window.removeEventListener('message', this.messageHandler);
+ },
methods: {
messageHandler(e) {
const isExpectedOrigin = e.origin === new URL(this.observabilityIframeSrc)?.origin;
+ if (!isExpectedOrigin) return;
- const isNewObservabilityPath = this.$route?.query?.observability_path !== e.data?.url;
+ const {
+ data: { type, payload },
+ } = e;
+ switch (type) {
+ case MESSAGE_EVENT_TYPE.GOUI_LOADED:
+ this.$refs.iframeSkeleton.handleSkeleton();
+ break;
+ case MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE:
+ this.routeUpdateHandler(payload);
+ break;
+ default:
+ break;
+ }
+ },
+ routeUpdateHandler(payload) {
+ const isNewObservabilityPath = this.$route?.query?.observability_path !== payload?.url;
- const shouldNotHandleMessage = !isExpectedOrigin || !e.data.url || !isNewObservabilityPath;
+ const shouldNotHandleMessage = !payload.url || !isNewObservabilityPath;
if (shouldNotHandleMessage) {
return;
@@ -24,7 +72,7 @@ export default {
// this will update the `observability_path` query param on each route change inside Observability UI
this.$router.replace({
name: this.$route.pathname,
- query: { ...this.$route.query, observability_path: e.data.url },
+ query: { ...this.$route.query, observability_path: payload.url },
});
},
},
@@ -32,11 +80,14 @@ export default {
</script>
<template>
- <iframe
- id="observability-ui-iframe"
- data-testid="observability-ui-iframe"
- frameborder="0"
- height="100%"
- :src="observabilityIframeSrc"
- ></iframe>
+ <observability-skeleton ref="iframeSkeleton" :variant="getSkeletonVariant">
+ <iframe
+ id="observability-ui-iframe"
+ data-testid="observability-ui-iframe"
+ frameborder="0"
+ height="100%"
+ :src="iframeSrcWithParams"
+ sandbox="allow-same-origin allow-forms allow-scripts"
+ ></iframe>
+ </observability-skeleton>
</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/dashboards.vue b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
new file mode 100644
index 00000000000..8b106407953
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/dashboards.vue
@@ -0,0 +1,29 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top left -->
+ <rect y="2" width="10" height="8" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="15" height="8" />
+
+ <!-- Top right -->
+ <rect y="2" x="354" width="10" height="8" />
+ <rect y="2" x="366" width="10" height="8" />
+ <rect y="2" x="378" width="10" height="8" />
+ <rect y="2" x="390" width="10" height="8" />
+
+ <!-- Middle header -->
+ <rect y="15" width="400" height="30" rx="2" ry="2" />
+
+ <!-- Dashboard container -->
+ <rect y="50" width="200" height="100" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/explore.vue b/app/assets/javascripts/observability/components/skeleton/explore.vue
new file mode 100644
index 00000000000..1fcbd4fb1cb
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/explore.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top left -->
+ <circle y="2" cx="6" cy="6" r="4" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="40" height="8" />
+
+ <!-- Top right -->
+
+ <rect y="2" x="263" width="13" height="8" />
+ <rect y="2" x="278" width="8" height="8" />
+ <rect y="2" x="288" width="50" height="8" />
+ <rect y="2" x="340" width="18" height="8" />
+ <rect y="2" x="360" width="30" height="8" />
+
+ <rect y="15" width="400" height="30" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/index.vue b/app/assets/javascripts/observability/components/skeleton/index.vue
new file mode 100644
index 00000000000..1e2671c8166
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/index.vue
@@ -0,0 +1,89 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { SKELETON_VARIANT } from '../../constants';
+import DashboardsSkeleton from './dashboards.vue';
+import ExploreSkeleton from './explore.vue';
+import ManageSkeleton from './manage.vue';
+
+export default {
+ SKELETON_VARIANT,
+ components: {
+ GlSkeletonLoader,
+ DashboardsSkeleton,
+ ExploreSkeleton,
+ ManageSkeleton,
+ },
+ props: {
+ variant: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ loading: null,
+ timerId: null,
+ };
+ },
+ mounted() {
+ this.timerId = setTimeout(() => {
+ /**
+ * If observability UI is not loaded then this.loading would be null
+ * we will show skeleton in that case
+ */
+ if (this.loading !== false) {
+ this.showSkeleton();
+ }
+ }, 500);
+ },
+ methods: {
+ handleSkeleton() {
+ if (this.loading === null) {
+ /**
+ * If observability UI content loads with in 500ms
+ * do not show skeleton.
+ */
+ clearTimeout(this.timerId);
+ return;
+ }
+
+ /**
+ * If observability UI content loads after 500ms
+ * wait for 400ms to hide skeleton.
+ * This is mostly to avoid the flashing effect If content loads imediately after skeleton
+ */
+ setTimeout(this.hideSkeleton, 400);
+ },
+ hideSkeleton() {
+ this.loading = false;
+ },
+ showSkeleton() {
+ this.loading = true;
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch">
+ <div v-show="loading" class="gl-px-5">
+ <dashboards-skeleton v-if="variant === $options.SKELETON_VARIANT.DASHBOARDS" />
+ <explore-skeleton v-else-if="variant === $options.SKELETON_VARIANT.EXPLORE" />
+ <manage-skeleton v-else-if="variant === $options.SKELETON_VARIANT.MANAGE" />
+
+ <gl-skeleton-loader v-else>
+ <rect y="2" width="10" height="8" />
+ <rect y="2" x="15" width="15" height="8" />
+ <rect y="2" x="35" width="15" height="8" />
+ <rect y="15" width="400" height="30" />
+ </gl-skeleton-loader>
+ </div>
+
+ <div
+ v-show="!loading"
+ class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch"
+ >
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/observability/components/skeleton/manage.vue b/app/assets/javascripts/observability/components/skeleton/manage.vue
new file mode 100644
index 00000000000..4b029120328
--- /dev/null
+++ b/app/assets/javascripts/observability/components/skeleton/manage.vue
@@ -0,0 +1,25 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+<template>
+ <gl-skeleton-loader :height="200">
+ <!-- Top header-->
+ <rect y="2" width="400" height="30" />
+
+ <rect y="35" x="65" width="80" height="8" />
+ <rect y="35" x="205" width="30" height="8" />
+ <rect y="35" x="240" width="25" height="8" />
+ <rect y="35" x="270" width="20" height="8" />
+
+ <rect y="55" x="65" width="100" height="8" />
+ <rect y="55" x="225" width="65" height="8" />
+
+ <rect y="65" x="65" width="225" height="200" rx="2" ry="2" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/observability/constants.js b/app/assets/javascripts/observability/constants.js
new file mode 100644
index 00000000000..74dd543e285
--- /dev/null
+++ b/app/assets/javascripts/observability/constants.js
@@ -0,0 +1,16 @@
+export const MESSAGE_EVENT_TYPE = Object.freeze({
+ GOUI_LOADED: 'GOUI_LOADED',
+ GOUI_ROUTE_UPDATE: 'GOUI_ROUTE_UPDATE',
+});
+
+export const OBSERVABILITY_ROUTES = Object.freeze({
+ DASHBOARDS: '/groups/gitlab-org/-/observability/dashboards',
+ EXPLORE: '/groups/gitlab-org/-/observability/explore',
+ MANAGE: '/groups/gitlab-org/-/observability/manage',
+});
+
+export const SKELETON_VARIANT = Object.freeze({
+ DASHBOARDS: 'dashboards',
+ EXPLORE: 'explore',
+ MANAGE: 'manage',
+});
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
index 1b7d5af6134..56d2ff86fb7 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/delete_alert.vue
@@ -1,11 +1,7 @@
<script>
import { GlSprintf, GlAlert, GlLink } from '@gitlab/ui';
-import {
- ALERT_MESSAGES,
- ADMIN_GARBAGE_COLLECTION_TIP,
- ALERT_DANGER_IMPORTING,
-} from '../../constants/index';
+import { ALERT_MESSAGES, ADMIN_GARBAGE_COLLECTION_TIP } from '../../constants/index';
export default {
components: {
@@ -27,7 +23,6 @@ export default {
},
},
garbageCollectionHelpPagePath: { type: String, required: false, default: '' },
- containerRegistryImportingHelpPagePath: { type: String, required: false, default: '' },
isAdmin: {
type: Boolean,
default: false,
@@ -53,11 +48,6 @@ export default {
}
return config;
},
- alertHref() {
- return this.deleteAlertType === ALERT_DANGER_IMPORTING
- ? this.containerRegistryImportingHelpPagePath
- : this.garbageCollectionHelpPagePath;
- },
},
};
</script>
@@ -71,7 +61,7 @@ export default {
>
<gl-sprintf :message="deleteAlertConfig.message">
<template #docLink="{ content }">
- <gl-link :href="alertHref" target="_blank">
+ <gl-link :href="garbageCollectionHelpPagePath" target="_blank">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 597df2b9bc3..c10d8be69a0 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -6,8 +6,8 @@ import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 98c24350f09..7bb69363743 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -93,10 +93,6 @@ export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
);
-export const DETAILS_IMPORTING_ERROR_MESSAGE = s__(
- 'ContainerRegistry|Tags temporarily cannot be marked for deletion. Please try again in a few minutes. %{docLinkStart}More details%{docLinkEnd}.',
-);
-
export const DELETE_IMAGE_CONFIRMATION_TITLE = s__('ContainerRegistry|Delete image repository?');
export const DELETE_IMAGE_CONFIRMATION_TEXT = s__(
'ContainerRegistry|Deleting the image repository will delete all images and tags inside. This action cannot be undone. Please type the following to confirm: %{code}',
@@ -137,7 +133,6 @@ export const ALERT_DANGER_TAG = 'danger_tag';
export const ALERT_SUCCESS_TAGS = 'success_tags';
export const ALERT_DANGER_TAGS = 'danger_tags';
export const ALERT_DANGER_IMAGE = 'danger_image';
-export const ALERT_DANGER_IMPORTING = 'danger_importing';
export const DELETE_SCHEDULED = 'DELETE_SCHEDULED';
export const DELETE_FAILED = 'DELETE_FAILED';
@@ -148,7 +143,6 @@ export const ALERT_MESSAGES = {
[ALERT_SUCCESS_TAGS]: DELETE_TAGS_SUCCESS_MESSAGE,
[ALERT_DANGER_TAGS]: DELETE_TAGS_ERROR_MESSAGE,
[ALERT_DANGER_IMAGE]: DETAILS_DELETE_IMAGE_ERROR_MESSAGE,
- [ALERT_DANGER_IMPORTING]: DETAILS_IMPORTING_ERROR_MESSAGE,
};
export const UNFINISHED_STATUS = 'UNFINISHED';
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index b339c8c8371..83c0d2cdfca 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -20,7 +20,6 @@ import {
ALERT_SUCCESS_TAGS,
ALERT_DANGER_TAGS,
ALERT_DANGER_IMAGE,
- ALERT_DANGER_IMPORTING,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
UNFINISHED_STATUS,
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
@@ -33,8 +32,6 @@ import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container
import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_repository_tags.query.graphql';
import getContainerRepositoriesDetails from '../graphql/queries/get_container_repositories_details.query.graphql';
-const REPOSITORY_IMPORTING_ERROR_MESSAGE = 'repository importing';
-
export default {
name: 'RegistryDetailsPage',
components: {
@@ -157,17 +154,12 @@ export default {
});
if (data?.destroyContainerRepositoryTags?.errors[0]) {
- throw new Error(data.destroyContainerRepositoryTags.errors[0]);
+ throw new Error();
}
this.deleteAlertType =
itemsToBeDeleted.length === 0 ? ALERT_SUCCESS_TAG : ALERT_SUCCESS_TAGS;
} catch (e) {
- if (e.message === REPOSITORY_IMPORTING_ERROR_MESSAGE) {
- this.deleteAlertType = ALERT_DANGER_IMPORTING;
- } else {
- this.deleteAlertType =
- itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
- }
+ this.deleteAlertType = itemsToBeDeleted.length === 0 ? ALERT_DANGER_TAG : ALERT_DANGER_TAGS;
}
this.mutationLoading = false;
@@ -203,7 +195,6 @@ export default {
<delete-alert
v-model="deleteAlertType"
:garbage-collection-help-page-path="config.garbageCollectionHelpPagePath"
- :container-registry-importing-help-page-path="config.containerRegistryImportingHelpPagePath"
:is-admin="config.isAdmin"
class="gl-my-2"
/>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
index 794be8d5195..8a038d7c974 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/list.vue
@@ -11,9 +11,9 @@ import {
import { get } from 'lodash';
import getContainerRepositoriesQuery from 'shared_queries/container_registry/get_container_repositories.query.graphql';
import { createAlert } from '~/flash';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import Tracking from '~/tracking';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import DeleteImage from '../components/delete_image.vue';
import RegistryHeader from '../components/list_page/registry_header.vue';
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
index c6ab746b9f4..bafcd78ad5d 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/pages/details.vue
@@ -8,7 +8,7 @@ import {
TOKEN_TYPE_TAG_NAME,
TAG_LABEL,
} from '~/packages_and_registries/harbor_registry/constants/index';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import TagsLoader from '~/packages_and_registries/shared/components/tags_loader.vue';
@@ -39,7 +39,7 @@ export default {
title: TAG_LABEL,
unique: true,
token: GlFilteredSearchToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
data() {
diff --git a/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
index 13df303cffe..2ae5957343b 100644
--- a/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
+++ b/app/assets/javascripts/packages_and_registries/harbor_registry/utils.js
@@ -3,8 +3,8 @@ import {
SORT_FIELD_MAPPING,
TOKEN_TYPE_TAG_NAME,
} from '~/packages_and_registries/harbor_registry/constants';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { normalizeHeaders, parseIntPagination } from '~/lib/utils/common_utils';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export const extractSortingDetail = (parsedSorting = '') => {
const [orderBy, sortOrder] = parsedSorting.split('_');
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
index 2adf6187c4b..0aeeb2c3d15 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/components/packages_list_app.vue
@@ -4,16 +4,14 @@ import { mapActions, mapState } from 'vuex';
import { createAlert, VARIANT_INFO } from '~/flash';
import { historyReplaceState } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
-import {
- SHOW_DELETE_SUCCESS_ALERT,
- FILTERED_SEARCH_TERM,
-} from '~/packages_and_registries/shared/constants';
+import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages_and_registries/shared/constants';
import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
import InfrastructureTitle from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_title.vue';
import InfrastructureSearch from '~/packages_and_registries/infrastructure_registry/list/components/infrastructure_search.vue';
import PackageList from '~/packages_and_registries/infrastructure_registry/list/components/packages_list.vue';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages_and_registries/infrastructure_registry/list/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export default {
components: {
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
index 37b51797490..7a452abdc26 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/list/stores/actions.js
@@ -2,6 +2,7 @@ import Api from '~/api';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages_and_registries/shared/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import {
FETCH_PACKAGES_LIST_ERROR_MESSAGE,
DELETE_PACKAGE_SUCCESS_MESSAGE,
@@ -31,7 +32,7 @@ export const requestPackagesList = ({ dispatch, state }, params = {}) => {
const type = state.config.forceTerraform
? TERRAFORM_SEARCH_TYPE
: state.filter.find((f) => f.type === 'type');
- const name = state.filter.find((f) => f.type === 'filtered-search-term');
+ const name = state.filter.find((f) => f.type === FILTERED_SEARCH_TERM);
const packageFilters = { package_type: type?.value?.data, package_name: name?.value?.data };
const apiMethod = state.config.isGroupPage ? 'groupPackages' : 'projectPackages';
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 4553dd3421b..7ad1ebac11e 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -54,6 +54,9 @@ export default {
},
},
computed: {
+ containsWebPathLink() {
+ return Boolean(this.packageEntity?._links?.webPath);
+ },
packageType() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
@@ -109,6 +112,7 @@ export default {
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
<router-link
+ v-if="containsWebPathLink"
:class="errorPackageStyle"
class="gl-text-body gl-min-w-0"
data-testid="details-link"
@@ -118,6 +122,7 @@ export default {
>
<gl-truncate :text="packageEntity.name" />
</router-link>
+ <gl-truncate v-else :text="packageEntity.name" />
<package-tags
v-if="showTags"
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 d28847c7900..0cf49b25bf2 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
@@ -1,14 +1,14 @@
<script>
-import { s__ } from '~/locale';
import { sortableFields } from '~/packages_and_registries/package_registry/utils';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
+ 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 {
- FILTERED_SEARCH_TERM,
- FILTERED_SEARCH_TYPE,
-} from '~/packages_and_registries/shared/constants';
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';
@@ -16,12 +16,12 @@ import PackageTypeToken from './tokens/package_type_token.vue';
export default {
tokens: [
{
- type: 'type',
+ type: TOKEN_TYPE_TYPE,
icon: 'package',
- title: s__('PackageRegistry|Type'),
+ title: TOKEN_TITLE_TYPE,
unique: true,
token: PackageTypeToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
],
components: { RegistrySearch, UrlSync, LocalStorageSync },
@@ -51,7 +51,7 @@ export default {
};
return this.filters.reduce((acc, filter) => {
- if (filter.type === FILTERED_SEARCH_TYPE && filter.value?.data) {
+ if (filter.type === TOKEN_TYPE_TYPE && filter.value?.data) {
return {
...acc,
packageType: filter.value.data.toUpperCase(),
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
index b5695a01376..2d405f3e9cc 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/fragments/package_data.fragment.graphql
@@ -29,4 +29,7 @@ fragment PackageData on Package {
fullPath
webUrl
}
+ _links {
+ webPath
+ }
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 51e0ab5aba8..9153906a38c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -62,6 +62,7 @@ query getPackageDetails(
}
}
versions(after: $after, before: $before, first: $first, last: $last) {
+ count
nodes {
id
name
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index c59dcaee411..03352f01aca 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -304,7 +304,7 @@ export default {
deleteFileModalContent: s__(
`PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
),
- otherVersionsTabTitle: __('Other versions'),
+ otherVersionsTabTitle: s__('PackageRegistry|Other versions'),
},
modal: {
packageDeletePrimaryAction: {
@@ -380,7 +380,9 @@ export default {
<gl-tab v-if="showDependencies">
<template #title>
<span>{{ __('Dependencies') }}</span>
- <gl-badge size="sm">{{ packageDependencies.length }}</gl-badge>
+ <gl-badge size="sm" data-testid="dependencies-badge">{{
+ packageDependencies.length
+ }}</gl-badge>
</template>
<template v-if="packageDependencies.length > 0">
@@ -392,7 +394,14 @@ export default {
</p>
</gl-tab>
- <gl-tab :title="$options.i18n.otherVersionsTabTitle" title-item-class="js-versions-tab" lazy>
+ <gl-tab title-item-class="js-versions-tab" lazy>
+ <template #title>
+ <span>{{ $options.i18n.otherVersionsTabTitle }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge" data-testid="other-versions-badge">{{
+ packageEntity.versions.count
+ }}</gl-badge>
+ </template>
+
<package-versions-list
:is-loading="isLoading"
:page-info="versionPageInfo"
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
index 6fb001e5e92..0a94f67ea5e 100644
--- a/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/package_path.vue
@@ -47,7 +47,7 @@ export default {
</script>
<template>
- <div data-qa-selector="package-path" class="gl-display-flex gl-align-items-center">
+ <div data-qa-selector="package_path" class="gl-display-flex gl-align-items-center">
<gl-icon data-testid="base-icon" name="project" class="gl-mx-3 gl-min-w-0" />
<gl-link
diff --git a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
index f3ce967b756..fe6e06ad830 100644
--- a/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
+++ b/app/assets/javascripts/packages_and_registries/shared/constants/package_registry.js
@@ -1,7 +1,5 @@
import { s__ } from '~/locale';
-export const FILTERED_SEARCH_TERM = 'filtered-search-term';
-export const FILTERED_SEARCH_TYPE = 'type';
export const HISTORY_PIPELINES_LIMIT = 5;
export const DELETE_PACKAGE_TRACKING_ACTION = 'delete_package';
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index 7e963cd0b08..76623377d90 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
-import { FILTERED_SEARCH_TERM } from './constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
export const getQueryParams = (query) =>
queryToObject(query, { gatherArrays: true, legacySpacesDecode: true });
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
index b68148e5461..96477b9f476 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_form.vue
@@ -43,7 +43,6 @@ export default {
'settingsPath',
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'emailConfirmationSetting',
'minimumPasswordLength',
'minimumPasswordLengthMin',
@@ -68,7 +67,6 @@ export default {
form: {
signupEnabled: this.signupEnabled,
requireAdminApproval: this.requireAdminApprovalAfterUserSignup,
- sendConfirmationEmail: this.sendUserConfirmationEmail,
emailConfirmationSetting: this.emailConfirmationSetting,
minimumPasswordLength: this.minimumPasswordLength,
minimumPasswordLengthMin: this.minimumPasswordLengthMin,
@@ -204,7 +202,6 @@ export default {
buttonText: s__('ApplicationSettings|Save changes'),
signupEnabledLabel: s__('ApplicationSettings|Sign-up enabled'),
requireAdminApprovalLabel: s__('ApplicationSettings|Require admin approval for new sign-ups'),
- sendConfirmationEmailLabel: s__('ApplicationSettings|Send confirmation email on sign-up'),
emailConfirmationSettingsLabel: s__('ApplicationSettings|Email confirmation settings'),
emailConfirmationSettingsOffLabel: s__('ApplicationSettings|Off'),
emailConfirmationSettingsOffHelpText: s__(
@@ -284,13 +281,6 @@ export default {
data-testid="require-admin-approval-checkbox"
/>
- <signup-checkbox
- v-model="form.sendConfirmationEmail"
- class="gl-mb-5"
- name="application_setting[send_user_confirmation_email]"
- :label="$options.i18n.sendConfirmationEmailLabel"
- />
-
<gl-form-group :label="$options.i18n.emailConfirmationSettingsLabel">
<gl-form-radio-group
v-model="form.emailConfirmationSetting"
diff --git a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
index 0d5c55cb87b..395d8a38bf7 100644
--- a/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
+++ b/app/assets/javascripts/pages/admin/application_settings/signup_restrictions.js
@@ -14,7 +14,6 @@ export default function initSignupRestrictions(elementSelector = '#js-signup-for
booleanAttributes: [
'signupEnabled',
'requireAdminApprovalAfterUserSignup',
- 'sendUserConfirmationEmail',
'domainDenylistEnabled',
'denylistTypeRawSelected',
'emailRestrictionsEnabled',
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
new file mode 100644
index 00000000000..25036984082
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/edit/index.js
@@ -0,0 +1,8 @@
+import initEditBroadcastMessage from '~/admin/broadcast_messages/edit';
+import initBroadcastMessagesForm from '../broadcast_message';
+
+if (gon.features.vueBroadcastMessages) {
+ initEditBroadcastMessage();
+} else {
+ initBroadcastMessagesForm();
+}
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
index ffd976be8c6..1f37df2b340 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index/index.js
@@ -1,6 +1,6 @@
import initBroadcastMessages from '~/admin/broadcast_messages';
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-import initBroadcastMessagesForm from './broadcast_message';
+import initBroadcastMessagesForm from '../broadcast_message';
if (gon.features.vueBroadcastMessages) {
initBroadcastMessages();
diff --git a/app/assets/javascripts/pages/admin/dashboard/index.js b/app/assets/javascripts/pages/admin/dashboard/index.js
deleted file mode 100644
index b63e612be47..00000000000
--- a/app/assets/javascripts/pages/admin/dashboard/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initGitlabVersionCheck from '~/gitlab_version_check';
-
-initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
index b06c804f3ca..48241a213ef 100644
--- a/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
+++ b/app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__, sprintf } from '~/locale';
export default {
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 2a7619da8cc..c5d62ae5daf 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -6,9 +6,7 @@ import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
-import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import UsersSelect from '~/users_select';
@@ -34,10 +32,6 @@ export default class Todos {
document.querySelectorAll('.js-todos-mark-all, .js-todos-undo-all').forEach((el) => {
el.removeEventListener('click', this.updateallStateClickedWrapper);
});
- document.querySelectorAll('.todo').forEach((el) => {
- el.removeEventListener('click', this.goToTodoUrl);
- el.removeEventListener('auxclick', this.goToTodoUrl);
- });
}
bindEvents() {
@@ -50,10 +44,6 @@ export default class Todos {
document.querySelectorAll('.js-todos-mark-all, .js-todos-undo-all').forEach((el) => {
el.addEventListener('click', this.updateAllStateClickedWrapper);
});
- document.querySelectorAll('.todo').forEach((el) => {
- el.addEventListener('click', this.goToTodoUrl);
- el.addEventListener('auxclick', this.goToTodoUrl);
- });
}
initFilters() {
@@ -106,19 +96,22 @@ export default class Todos {
e.stopPropagation();
e.preventDefault();
- const { target } = e;
- target.setAttribute('disabled', true);
- target.classList.add('disabled');
+ let { currentTarget } = e;
+ if (currentTarget.tagName === 'svg' || currentTarget.tagName === 'use') {
+ currentTarget = currentTarget.closest('a');
+ }
+ currentTarget.setAttribute('disabled', true);
+ currentTarget.classList.add('disabled');
- target.querySelector('.gl-spinner-container').classList.add('gl-mr-2');
+ currentTarget.querySelector('.js-todo-button-icon').classList.add('hidden');
- axios[target.dataset.method](target.dataset.href)
+ axios[currentTarget.dataset.method](currentTarget.href)
.then(({ data }) => {
- this.updateRowState(target);
+ this.updateRowState(currentTarget);
this.updateBadges(data);
})
.catch(() => {
- this.updateRowState(target, true);
+ this.updateRowState(currentTarget, true);
return createAlert({
message: __('Error updating status of to-do item.'),
});
@@ -134,7 +127,7 @@ export default class Todos {
target.removeAttribute('disabled');
target.classList.remove('disabled');
- target.querySelector('.gl-spinner-container').classList.remove('gl-mr-2');
+ target.querySelector('.js-todo-button-icon').classList.remove('hidden');
if (isInactive === true) {
restoreBtn.classList.add('hidden');
@@ -209,25 +202,4 @@ export default class Todos {
data.done_count,
);
}
-
- goToTodoUrl(e) {
- const todoLink = this.dataset.url;
-
- if (!todoLink || e.target.closest('a')) {
- return;
- }
-
- e.stopPropagation();
- e.preventDefault();
-
- const isPrimaryClick = e.button === 0;
-
- if (isMetaClick(e)) {
- const windowTarget = '_blank';
-
- window.open(todoLink, windowTarget);
- } else if (isPrimaryClick) {
- visitUrl(todoLink);
- }
- }
}
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 377ba0f13a9..bf0147ca885 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,11 +1,7 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import {
- initBulkUpdateSidebar,
- initStatusDropdown,
- initSubscriptionsDropdown,
-} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { initBulkUpdateSidebar } from '~/issuable';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
@@ -13,8 +9,6 @@ const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
-initStatusDropdown();
-initSubscriptionsDropdown();
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js
index a8e67c57307..da748223440 100644
--- a/app/assets/javascripts/pages/help/index/index.js
+++ b/app/assets/javascripts/pages/help/index/index.js
@@ -1,5 +1,3 @@
import docs from '~/docs/docs_bundle';
-import initGitlabVersionCheck from '~/gitlab_version_check';
docs();
-initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
index 20ce296bbec..912b84dbae6 100644
--- a/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
+++ b/app/assets/javascripts/pages/import/fogbugz/new_user_map/components/user_select.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAvatarLabeled, GlListbox } from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { __ } from '~/locale';
import searchUsersQuery from '~/graphql_shared/queries/users_search_all.query.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -11,7 +11,7 @@ const USERS_PER_PAGE = 20;
export default {
components: {
GlAvatarLabeled,
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
name: {
@@ -70,7 +70,7 @@ export default {
</script>
<template>
<div>
- <gl-listbox
+ <gl-collapsible-listbox
ref="listbox"
v-model="user"
:items="users"
@@ -89,7 +89,7 @@ export default {
:sub-label="item.username"
/>
</template>
- </gl-listbox>
+ </gl-collapsible-listbox>
<input type="hidden" :name="name" :value="userId" />
</div>
</template>
diff --git a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
index 870c14f99ae..d0560af5b3f 100644
--- a/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
+++ b/app/assets/javascripts/pages/import/gitlab_projects/new/index.js
@@ -1,3 +1,5 @@
import initGitLabImportProject from '~/projects/project_import_gitlab_project';
+import { initNewProjectUrlSelect } from '~/projects/new';
+initNewProjectUrlSelect();
initGitLabImportProject();
diff --git a/app/assets/javascripts/pages/import/manifest/new/index.js b/app/assets/javascripts/pages/import/manifest/new/index.js
new file mode 100644
index 00000000000..0bb70a7364e
--- /dev/null
+++ b/app/assets/javascripts/pages/import/manifest/new/index.js
@@ -0,0 +1,3 @@
+import { initNewProjectUrlSelect } from '~/projects/new';
+
+initNewProjectUrlSelect();
diff --git a/app/assets/javascripts/pages/import/phabricator/new/index.js b/app/assets/javascripts/pages/import/phabricator/new/index.js
new file mode 100644
index 00000000000..0bb70a7364e
--- /dev/null
+++ b/app/assets/javascripts/pages/import/phabricator/new/index.js
@@ -0,0 +1,3 @@
+import { initNewProjectUrlSelect } from '~/projects/new';
+
+initNewProjectUrlSelect();
diff --git a/app/assets/javascripts/pages/projects/branches/new/index.js b/app/assets/javascripts/pages/projects/branches/new/index.js
index dbae89b5ade..f2b03468b0b 100644
--- a/app/assets/javascripts/pages/projects/branches/new/index.js
+++ b/app/assets/javascripts/pages/projects/branches/new/index.js
@@ -1,7 +1,6 @@
import NewBranchForm from '~/new_branch_form';
+import initNewBranchRefSelector from '~/branches/init_new_branch_ref_selector';
+initNewBranchRefSelector();
// eslint-disable-next-line no-new
-new NewBranchForm(
- document.querySelector('.js-create-branch-form'),
- JSON.parse(document.getElementById('availableRefs').innerHTML),
-);
+new NewBranchForm(document.querySelector('.js-create-branch-form'));
diff --git a/app/assets/javascripts/pages/projects/ci/lints/show/index.js b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
index 6e1cdf557b5..caac76fc6d7 100644
--- a/app/assets/javascripts/pages/projects/ci/lints/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/lints/show/index.js
@@ -1,3 +1,3 @@
-import initCiLint from '~/ci_lint';
+import initCiLint from '~/ci/ci_lint';
initCiLint();
diff --git a/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js b/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
index 67d32648ce8..7e91f23dd7f 100644
--- a/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
+++ b/app/assets/javascripts/pages/projects/ci/pipeline_editor/show/index.js
@@ -1,3 +1,3 @@
-import { initPipelineEditor } from '~/pipeline_editor';
+import { initPipelineEditor } from '~/ci/pipeline_editor';
initPipelineEditor();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index ee74628a994..f5ecf9be591 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,9 +1,10 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
-import mountCommits from '~/projects/commits';
+import { mountCommits, initCommitsRefSwitcher } from '~/projects/commits';
new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
GpgBadges.fetch();
mountCommits(document.getElementById('js-author-dropdown'));
+initCommitsRefSwitcher();
diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
index bef21ef8fdf..05a1bbc69ed 100644
--- a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
+++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
@@ -1,3 +1,3 @@
-import initCycleAnalytics from '~/cycle_analytics';
+import initCycleAnalytics from '~/analytics/cycle_analytics';
initCycleAnalytics();
diff --git a/app/assets/javascripts/pages/projects/environments/show/index.js b/app/assets/javascripts/pages/projects/environments/show/index.js
index 53e48ad8d86..1ce8899ac63 100644
--- a/app/assets/javascripts/pages/projects/environments/show/index.js
+++ b/app/assets/javascripts/pages/projects/environments/show/index.js
@@ -1,5 +1,6 @@
import initConfirmRollBackModal from '~/environments/init_confirm_rollback_modal';
-import { initHeader } from '~/environments/mount_show';
+import { initHeader, initPage } from '~/environments/mount_show';
initHeader();
+initPage();
initConfirmRollBackModal();
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index 30cefa3d717..91650003d4a 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -23,6 +23,7 @@ import {
VISIBILITY_LEVEL_INTERNAL_STRING,
VISIBILITY_LEVEL_PUBLIC_STRING,
VISIBILITY_LEVELS_STRING_TO_INTEGER,
+ VISIBILITY_LEVELS_INTEGER_TO_STRING,
} from '~/visibility_level/constants';
import ProjectNamespace from './project_namespace.vue';
@@ -105,39 +106,8 @@ export default {
};
},
computed: {
- projectVisibilityLevel() {
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
- },
- namespaceVisibilityLevel() {
- const visibility =
- this.form.fields.namespace.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
- return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
- },
- visibilityLevelCap() {
- return Math.min(this.projectVisibilityLevel, this.namespaceVisibilityLevel);
- },
- restrictedVisibilityLevelsSet() {
- return new Set(this.restrictedVisibilityLevels);
- },
allowedVisibilityLevels() {
- const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
- (levels, [levelName, levelValue]) => {
- if (
- !this.restrictedVisibilityLevelsSet.has(levelValue) &&
- levelValue <= this.visibilityLevelCap
- ) {
- levels.push(levelName);
- }
- return levels;
- },
- [],
- );
-
- if (!allowedLevels.length) {
- return [VISIBILITY_LEVEL_PRIVATE_STRING];
- }
-
- return allowedLevels;
+ return this.getAllowedVisibilityLevels();
},
visibilityLevels() {
return [
@@ -178,13 +148,60 @@ export default {
return !this.allowedVisibilityLevels.includes(visibility);
},
getInitialVisibilityValue() {
- return this.restrictedVisibilityLevels.length !== 0 ? null : this.projectVisibility;
+ return this.getMaximumAllowedVisibilityLevel(this.projectVisibility);
},
setNamespace(namespace) {
- this.form.fields.visibility.value =
- this.restrictedVisibilityLevels.length !== 0 ? null : VISIBILITY_LEVEL_PRIVATE_STRING;
this.form.fields.namespace.value = namespace;
this.form.fields.namespace.state = true;
+ this.form.fields.visibility.value = this.getMaximumAllowedVisibilityLevel(
+ this.form.fields.visibility.value,
+ );
+ },
+ getProjectVisibilityLevel() {
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[this.projectVisibility];
+ },
+ getNamespaceVisibilityLevel() {
+ const visibility =
+ this.form?.fields?.namespace?.value?.visibility || VISIBILITY_LEVEL_PUBLIC_STRING;
+ return VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
+ },
+ getVisibilityLevelCap() {
+ return Math.min(this.getProjectVisibilityLevel(), this.getNamespaceVisibilityLevel());
+ },
+ getRestrictedVisibilityLevelsSet() {
+ return new Set(this.restrictedVisibilityLevels);
+ },
+ getAllowedVisibilityLevels() {
+ const allowedLevels = Object.entries(VISIBILITY_LEVELS_STRING_TO_INTEGER).reduce(
+ (levels, [levelName, levelValue]) => {
+ if (
+ !this.getRestrictedVisibilityLevelsSet().has(levelValue) &&
+ levelValue <= this.getVisibilityLevelCap()
+ ) {
+ levels.push(levelName);
+ }
+ return levels;
+ },
+ [],
+ );
+
+ if (!allowedLevels.length) {
+ return [VISIBILITY_LEVEL_PRIVATE_STRING];
+ }
+
+ return allowedLevels;
+ },
+ getMaximumAllowedVisibilityLevel(visibility) {
+ const allowedVisibilities = this.getAllowedVisibilityLevels().map(
+ (s) => VISIBILITY_LEVELS_STRING_TO_INTEGER[s],
+ );
+ const current = VISIBILITY_LEVELS_STRING_TO_INTEGER[visibility];
+ const lower = allowedVisibilities.filter((l) => l <= current);
+ if (lower.length) {
+ return VISIBILITY_LEVELS_INTEGER_TO_STRING[Math.max(...lower)];
+ }
+ const higher = allowedVisibilities.filter((l) => l >= current);
+ return VISIBILITY_LEVELS_INTEGER_TO_STRING[Math.min(...higher)];
},
async onSubmit() {
this.form.showValidation = true;
diff --git a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
index 08d24344ffc..10bfcdc2294 100644
--- a/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
+++ b/app/assets/javascripts/pages/projects/graphs/components/code_coverage.vue
@@ -1,5 +1,5 @@
<script>
-import { GlAlert, GlButton, GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import { GlAlert, GlButton, GlListbox, GlSprintf } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { get } from 'lodash';
import { formatDate } from '~/lib/utils/datetime_utility';
@@ -12,8 +12,7 @@ export default {
GlAlert,
GlAreaChart,
GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlListbox,
GlSprintf,
},
props: {
@@ -96,6 +95,14 @@ export default {
formattedData() {
return this.sortedData.map((value) => [value.date, value.coverage]);
},
+ mappedCoverages() {
+ return this.dailyCoverageData?.map((item, index) => ({
+ // A numerical index makes an item into a group header, so
+ // convert these to strings to get non-header GlListbox items
+ value: index.toString(),
+ text: item.group_name,
+ }));
+ },
chartData() {
return [
{
@@ -175,18 +182,13 @@ export default {
{{ __('It seems that there is currently no available data for code coverage') }}
</span>
</gl-alert>
- <gl-dropdown v-if="canShowData" :text="selectedDailyCoverageName">
- <gl-dropdown-item
- v-for="({ group_name }, index) in dailyCoverageData"
- :key="index"
- :value="group_name"
- is-check-item
- :is-checked="index === selectedCoverageIndex"
- @click="setSelectedCoverage(index)"
- >
- {{ group_name }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-listbox
+ v-if="canShowData"
+ :items="mappedCoverages"
+ :selected="selectedCoverageIndex.toString()"
+ :toggle-text="selectedDailyCoverageName"
+ @select="setSelectedCoverage"
+ />
</div>
<gl-area-chart
v-if="!isLoading"
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
index 2d26d3922bf..653f903c6d1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js
@@ -26,7 +26,10 @@ const updateCommitList = (url, $emptyState, $loadingIndicator, $commitList, para
export default (mrNewCompareNode) => {
const { sourceBranchUrl, targetBranchUrl } = mrNewCompareNode.dataset;
- initTargetProjectDropdown();
+
+ if (!window.gon?.features?.mrCompareDropdowns) {
+ initTargetProjectDropdown();
+ }
const updateSourceBranchCommitList = () =>
updateCommitList(
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 9aecd154483..b3868653d6a 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
@@ -1,10 +1,37 @@
+import $ from 'jquery';
+import Vue from 'vue';
import initPipelines from '~/commit/pipelines/pipelines_bundle';
import MergeRequest from '~/merge_request';
+import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
import initCompare from './compare';
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
if (mrNewCompareNode) {
initCompare(mrNewCompareNode);
+
+ const el = document.getElementById('js-target-project-dropdown');
+ const { targetProjectsPath, currentProject } = el.dataset;
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ name: 'TargetProjectDropdown',
+ provide: {
+ targetProjectsPath,
+ currentProject: JSON.parse(currentProject),
+ },
+ render(h) {
+ return h(TargetProjectDropdown, {
+ on: {
+ 'project-selected': function projectSelectedFunction(refsUrl) {
+ const $targetBranchDropdown = $('.js-target-branch');
+ $targetBranchDropdown.data('refsUrl', refsUrl);
+ $targetBranchDropdown.data('deprecatedJQueryDropdown').clearMenu();
+ },
+ },
+ });
+ },
+ });
} else {
const mrNewSubmitNode = document.querySelector('.js-merge-request-new-submit');
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js b/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js
new file mode 100644
index 00000000000..77294c0fb9e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/diffs/index.js
@@ -0,0 +1,5 @@
+import initDiffsApp from '~/diffs';
+import { initMrPage } from '../page';
+
+initMrPage();
+initDiffsApp();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index 2399aafc9b5..b3a09cc0be3 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,20 +1,13 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import {
- initBulkUpdateSidebar,
- initStatusDropdown,
- initSubscriptionsDropdown,
-} from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
+import { initBulkUpdateSidebar, initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
-initStatusDropdown();
-initSubscriptionsDropdown();
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 42fa306d226..a4e3ddfc506 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
+import IssuableLabelSelector from '~/issuable/issuable_label_selector';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import GLForm from '~/gl_form';
@@ -14,6 +15,7 @@ export default () => {
new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
+ IssuableLabelSelector();
new LabelsSelect();
new IssuableTemplateSelectors({
warnTemplateOverride: true,
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
new file mode 100644
index 00000000000..a8699b350f8
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -0,0 +1,45 @@
+import Vue from 'vue';
+import StickyHeader from '~/merge_requests/components/sticky_header.vue';
+import { initIssuableHeaderWarnings } from '~/issuable';
+import initMrNotes from '~/mr_notes';
+import store from '~/mr_notes/stores';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import { apolloProvider } from '~/graphql_shared/issuable_client';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import initShow from './init_merge_request_show';
+import getStateQuery from './queries/get_state.query.graphql';
+
+export function initMrPage() {
+ initMrNotes();
+ initShow();
+}
+
+requestIdleCallback(() => {
+ initSidebarBundle(store);
+ initIssuableHeaderWarnings(store);
+
+ const el = document.getElementById('js-merge-sticky-header');
+
+ if (el) {
+ const { data } = el.dataset;
+ const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ store,
+ apolloProvider,
+ provide: {
+ query: getStateQuery,
+ iid,
+ projectPath,
+ title,
+ tabs,
+ isFluidLayout: parseBoolean(isFluidLayout),
+ },
+ render(h) {
+ return h(StickyHeader);
+ },
+ });
+ }
+});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index cc5c393ff8c..568bf19b55e 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,45 +1,5 @@
-import Vue from 'vue';
-import StickyHeader from '~/merge_requests/components/sticky_header.vue';
-import { initReviewBar } from '~/batch_comments';
-import { initIssuableHeaderWarnings } from '~/issuable';
-import initMrNotes from '~/mr_notes';
-import store from '~/mr_notes/stores';
-import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import { apolloProvider } from '~/graphql_shared/issuable_client';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import initShow from '../init_merge_request_show';
-import getStateQuery from '../queries/get_state.query.graphql';
+import initNotesApp from '~/mr_notes/init_notes';
+import { initMrPage } from '../page';
-initMrNotes();
-initShow();
-
-requestIdleCallback(() => {
- initSidebarBundle(store);
- initReviewBar();
- initIssuableHeaderWarnings(store);
-
- const el = document.getElementById('js-merge-sticky-header');
-
- if (el) {
- const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout } = JSON.parse(data);
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- store,
- apolloProvider,
- provide: {
- query: getStateQuery,
- iid,
- projectPath,
- title,
- tabs,
- isFluidLayout: parseBoolean(isFluidLayout),
- },
- render(h) {
- return h(StickyHeader);
- },
- });
- }
-});
+initMrPage();
+initNotesApp();
diff --git a/app/assets/javascripts/pages/projects/ml/candidates/show/index.js b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js
new file mode 100644
index 00000000000..c1acef5ac13
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/candidates/show/index.js
@@ -0,0 +1,27 @@
+import Vue from 'vue';
+import MlCandidate from '~/ml/experiment_tracking/components/ml_candidate.vue';
+
+const initShowCandidate = () => {
+ const element = document.querySelector('#js-show-ml-candidate');
+ if (!element) {
+ return;
+ }
+
+ const container = document.createElement('div');
+ element.appendChild(container);
+
+ const candidate = JSON.parse(element.dataset.candidate);
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: container,
+ provide: {
+ candidate,
+ },
+ render(h) {
+ return h(MlCandidate);
+ },
+ });
+};
+
+initShowCandidate();
diff --git a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
index 0a9d9f4c987..97e436920c7 100644
--- a/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/experiments/show/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import ShowExperiment from '~/ml/experiment_tracking/components/experiment.vue';
+import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
const initShowExperiment = () => {
const element = document.querySelector('#js-show-ml-experiment');
@@ -23,7 +23,7 @@ const initShowExperiment = () => {
paramNames,
},
render(h) {
- return h(ShowExperiment);
+ return h(MlExperiment);
},
});
};
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 50733d8a145..d022428df98 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -4,10 +4,8 @@ import {
initDeploymentTargetSelect,
} from '~/projects/new';
import initProjectVisibilitySelector from '~/projects/project_visibility';
-import initProjectNew from '~/projects/project_new';
initProjectVisibilitySelector();
-initProjectNew.bindEvents();
initNewProjectCreation();
initNewProjectUrlSelect();
initDeploymentTargetSelect();
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 85443843684..fd8b1a6290f 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
@@ -39,6 +39,11 @@ export default {
required: false,
default: '',
},
+ sendNativeErrors: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -114,9 +119,11 @@ export default {
cronInterval() {
// updates field validation state when model changes, as
// glFieldError only updates on input.
- this.$nextTick(() => {
- gl.pipelineScheduleFieldErrors.updateFormValidityState();
- });
+ if (this.sendNativeErrors) {
+ this.$nextTick(() => {
+ gl.pipelineScheduleFieldErrors.updateFormValidityState();
+ });
+ }
},
radioValue: {
immediate: true,
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
deleted file mode 100644
index bc467952551..00000000000
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ /dev/null
@@ -1,82 +0,0 @@
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { formatTimezone } from '~/lib/utils/datetime_utility';
-
-const defaultTimezone = { identifier: 'Etc/UTC', name: 'UTC', offset: 0 };
-const defaults = {
- $inputEl: null,
- $dropdownEl: null,
- onSelectTimezone: null,
- displayFormat: (item) => item.name,
-};
-
-export const formatUtcOffset = (offset) => {
- const parsed = parseInt(offset, 10);
- if (Number.isNaN(parsed) || parsed === 0) {
- return `0`;
- }
- const prefix = offset > 0 ? '+' : '-';
- return `${prefix} ${Math.abs(offset / 3600)}`;
-};
-
-export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
- if (tzList && tzList.length && identifier && identifier.length) {
- return tzList.find((tz) => tz.identifier === identifier) || null;
- }
- return null;
-};
-
-export default class TimezoneDropdown {
- constructor({
- $dropdownEl,
- $inputEl,
- onSelectTimezone,
- displayFormat,
- allowEmpty = false,
- } = defaults) {
- this.$dropdown = $dropdownEl;
- this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
- this.$input = $inputEl;
- this.timezoneData = this.$dropdown.data('data') || [];
-
- this.onSelectTimezone = onSelectTimezone;
- this.displayFormat = displayFormat || defaults.displayFormat;
- this.allowEmpty = allowEmpty;
-
- this.initDropdown();
- }
-
- initDropdown() {
- initDeprecatedJQueryDropdown(this.$dropdown, {
- data: this.timezoneData,
- filterable: true,
- selectable: true,
- toggleLabel: this.displayFormat,
- search: {
- fields: ['name'],
- },
- clicked: (cfg) => this.handleDropdownChange(cfg),
- text: (item) => formatTimezone(item),
- });
-
- const initialTimezone = findTimezoneByIdentifier(this.timezoneData, this.$input.val());
-
- if (initialTimezone !== null) {
- this.setDropdownValue(initialTimezone);
- } else if (!this.allowEmpty) {
- this.setDropdownValue(defaultTimezone);
- }
- }
-
- setDropdownValue(timezone) {
- this.$dropdownToggle.text(this.displayFormat(timezone));
- this.$input.val(timezone.identifier);
- }
-
- handleDropdownChange({ selectedObj, e }) {
- e.preventDefault();
- this.$input.val(selectedObj.identifier);
- if (this.onSelectTimezone) {
- this.onSelectTimezone({ selectedObj, e });
- }
- }
-}
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index d177c67f133..4c9eb830ff6 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -11,10 +11,14 @@ import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import projectSelect from '~/project_select';
+const BRANCH_REF_TYPE = 'heads';
+const TAG_REF_TYPE = 'tags';
+const BRANCH_GROUP_NAME = __('Branches');
+const TAG_GROUP_NAME = __('Tags');
+
export default class Project {
constructor() {
initClonePanel();
-
// Ref switcher
if (document.querySelector('.js-project-refs-dropdown')) {
Project.initRefSwitcher();
@@ -62,6 +66,7 @@ export default class Project {
return $('.js-project-refs-dropdown').each(function () {
const $dropdown = $(this);
const selected = $dropdown.data('selected');
+ const refType = $dropdown.data('refType');
const fieldName = $dropdown.data('fieldName');
const shouldVisit = Boolean($dropdown.data('visit'));
const $form = $dropdown.closest('form');
@@ -91,18 +96,32 @@ export default class Project {
filterByText: true,
inputFieldName: $dropdown.data('inputFieldName'),
fieldName,
- renderRow(ref) {
+ renderRow(ref, _, params) {
const li = refListItem.cloneNode(false);
const link = refLink.cloneNode(false);
if (ref === selected) {
- link.className = 'is-active';
+ // Check group and current ref type to avoid adding a class when tags and branches share the same name
+ if (
+ (refType === BRANCH_REF_TYPE && params.group === BRANCH_GROUP_NAME) ||
+ (refType === TAG_REF_TYPE && params.group === TAG_GROUP_NAME) ||
+ !refType
+ ) {
+ link.className = 'is-active';
+ }
}
+
link.textContent = ref;
link.dataset.ref = ref;
if (ref.length > 0 && shouldVisit) {
- link.href = mergeUrlParams({ [fieldName]: ref }, linkTarget);
+ const urlParams = { [fieldName]: ref };
+ if (params.group === BRANCH_GROUP_NAME) {
+ urlParams.ref_type = BRANCH_REF_TYPE;
+ } else {
+ urlParams.ref_type = TAG_REF_TYPE;
+ }
+ link.href = mergeUrlParams(urlParams, linkTarget);
}
li.appendChild(link);
diff --git a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
index 739e666644c..0f7ede8ed42 100644
--- a/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
+++ b/app/assets/javascripts/pages/projects/settings/merge_requests/index.js
@@ -1,9 +1,6 @@
import groupsSelect from '~/groups_select';
import UserCallout from '~/user_callout';
-import UsersSelect from '~/users_select';
-// eslint-disable-next-line no-new
-new UsersSelect();
groupsSelect();
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
index c37b4cc643a..5fa3288bbef 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue
@@ -23,6 +23,12 @@ import ProjectSettingRow from './project_setting_row.vue';
const FEATURE_ACCESS_LEVEL_ANONYMOUS = [30, s__('ProjectSettings|Everyone')];
+const PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY = {
+ [VISIBILITY_LEVEL_PRIVATE_INTEGER]: featureAccessLevel.PROJECT_MEMBERS,
+ [VISIBILITY_LEVEL_INTERNAL_INTEGER]: featureAccessLevel.EVERYONE,
+ [VISIBILITY_LEVEL_PUBLIC_INTEGER]: FEATURE_ACCESS_LEVEL_ANONYMOUS[0],
+};
+
export default {
i18n: {
...CVE_ID_REQUEST_BUTTON_I18N,
@@ -32,7 +38,6 @@ export default {
issuesLabel: s__('ProjectSettings|Issues'),
lfsLabel: s__('ProjectSettings|Git Large File Storage (LFS)'),
mergeRequestsLabel: s__('ProjectSettings|Merge requests'),
- operationsLabel: s__('ProjectSettings|Operations'),
environmentsLabel: s__('ProjectSettings|Environments'),
environmentsHelpText: s__(
'ProjectSettings|Every project can make deployments to environments either via CI/CD or API calls. Non-project members have read-only access.',
@@ -47,11 +52,15 @@ export default {
packagesHelpText: s__(
'ProjectSettings|Every project can have its own space to store its packages. Note: The Package Registry is always visible when a project is public.',
),
- packageRegistryHelpText: s__(
- 'ProjectSettings|Every project can have its own space to store its packages.',
+ packageRegistryHelpText: s__('ProjectSettings|Publish, store, and view packages in a project.'),
+ packageRegistryForEveryoneHelpText: s__(
+ 'ProjectSettings|Anyone can pull packages with a package manager API.',
),
packagesLabel: s__('ProjectSettings|Packages'),
packageRegistryLabel: s__('ProjectSettings|Package registry'),
+ packageRegistryForEveryoneLabel: s__(
+ 'ProjectSettings|Allow anyone to pull from Package Registry',
+ ),
pagesLabel: s__('ProjectSettings|Pages'),
ciCdLabel: __('CI/CD'),
repositoryLabel: s__('ProjectSettings|Repository'),
@@ -249,7 +258,6 @@ export default {
analyticsAccessLevel: featureAccessLevel.EVERYONE,
requirementsAccessLevel: featureAccessLevel.EVERYONE,
securityAndComplianceAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
- operationsAccessLevel: featureAccessLevel.EVERYONE,
environmentsAccessLevel: featureAccessLevel.EVERYONE,
featureFlagsAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
infrastructureAccessLevel: featureAccessLevel.PROJECT_MEMBERS,
@@ -287,18 +295,6 @@ export default {
);
},
- packageRegistryFeatureAccessLevelOptions() {
- const options = [FEATURE_ACCESS_LEVEL_ANONYMOUS];
-
- if (this.visibilityLevel === VISIBILITY_LEVEL_PRIVATE_INTEGER) {
- options.unshift(featureAccessLevelMembers);
- } else if (this.visibilityLevel === VISIBILITY_LEVEL_INTERNAL_INTEGER) {
- options.unshift(featureAccessLevelEveryone);
- }
-
- return options;
- },
-
pagesFeatureAccessLevelOptions() {
const options = [featureAccessLevelMembers];
@@ -318,10 +314,6 @@ export default {
return options;
},
- operationsEnabled() {
- return this.operationsAccessLevel > featureAccessLevel.NOT_ENABLED;
- },
-
environmentsEnabled() {
return this.environmentsAccessLevel > featureAccessLevel.NOT_ENABLED;
},
@@ -351,7 +343,7 @@ export default {
}
return s__(
- 'ProjectSettings|View and edit files in this project. Non-project members have only read access.',
+ 'ProjectSettings|View and edit files in this project. When set to **Everyone With Access** non-project members have only read access.',
);
},
cveIdRequestIsDisabled() {
@@ -366,16 +358,17 @@ export default {
packageRegistryAccessLevelEnabled() {
return this.glFeatures.packageRegistryAccessLevel;
},
- splitOperationsEnabled() {
- return this.glFeatures.splitOperationsVisibilityPermissions;
+ packageRegistryEnabled() {
+ return this.packageRegistryAccessLevel > featureAccessLevel.NOT_ENABLED;
+ },
+ packageRegistryApiForEveryoneEnabled() {
+ return this.packageRegistryAccessLevel === FEATURE_ACCESS_LEVEL_ANONYMOUS[0];
+ },
+ packageRegistryApiForEveryoneEnabledShown() {
+ return this.visibilityLevel !== VISIBILITY_LEVEL_PUBLIC_INTEGER;
},
monitorOperationsFeatureAccessLevelOptions() {
- if (this.splitOperationsEnabled) {
- return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
- }
- return this.featureAccessLevelOptions.filter(
- ([value]) => value <= this.operationsAccessLevel,
- );
+ return this.featureAccessLevelOptions.filter(([value]) => value <= this.monitorAccessLevel);
},
},
@@ -429,10 +422,6 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.securityAndComplianceAccessLevel,
);
- this.operationsAccessLevel = Math.min(
- featureAccessLevel.PROJECT_MEMBERS,
- this.operationsAccessLevel,
- );
this.environmentsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
this.environmentsAccessLevel,
@@ -474,9 +463,8 @@ export default {
this.packageRegistryAccessLevelEnabled &&
this.packageRegistryAccessLevel === featureAccessLevel.PROJECT_MEMBERS
) {
- this.packageRegistryAccessLevel = Math.min(
- ...this.packageRegistryFeatureAccessLevelOptions.map((option) => option[0]),
- );
+ this.packageRegistryAccessLevel =
+ PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY[value];
}
if (this.buildsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.buildsAccessLevel = featureAccessLevel.EVERYONE;
@@ -492,8 +480,6 @@ export default {
this.metricsDashboardAccessLevel = featureAccessLevel.EVERYONE;
if (this.requirementsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.requirementsAccessLevel = featureAccessLevel.EVERYONE;
- if (this.operationsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
- this.operationsAccessLevel = featureAccessLevel.EVERYONE;
if (this.environmentsAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
this.environmentsAccessLevel = featureAccessLevel.EVERYONE;
if (this.monitorAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
@@ -532,10 +518,6 @@ export default {
toggleHiddenClassBySelector('.merge-requests-feature', false);
},
- operationsAccessLevel(value, oldValue) {
- this.updateSubFeatureAccessLevel(value, oldValue);
- },
-
monitorAccessLevel(value, oldValue) {
this.updateSubFeatureAccessLevel(value, oldValue);
},
@@ -561,6 +543,22 @@ export default {
visibilityAllowed(option) {
return this.allowedVisibilityOptions.includes(option);
},
+ onPackageRegistryEnabledToggle(value) {
+ this.packageRegistryAccessLevel = value
+ ? this.packageRegistryAccessLevelDefault()
+ : featureAccessLevel.NOT_ENABLED;
+ },
+ onPackageRegistryApiForEveryoneEnabledToggle(value) {
+ this.packageRegistryAccessLevel = value
+ ? FEATURE_ACCESS_LEVEL_ANONYMOUS[0]
+ : this.packageRegistryAccessLevelDefault();
+ },
+ packageRegistryAccessLevelDefault() {
+ return (
+ PACKAGE_REGISTRY_ACCESS_LEVEL_DEFAULT_BY_PROJECT_VISIBILITY[this.visibilityLevel] ??
+ featureAccessLevel.NOT_ENABLED
+ );
+ },
},
};
</script>
@@ -897,10 +895,36 @@ export default {
:help-text="$options.i18n.packageRegistryHelpText"
data-testid="package-registry-access-level"
>
- <project-feature-setting
- v-model="packageRegistryAccessLevel"
+ <gl-toggle
+ class="gl-my-2"
+ :value="packageRegistryEnabled"
:label="$options.i18n.packageRegistryLabel"
- :options="packageRegistryFeatureAccessLevelOptions"
+ label-position="hidden"
+ name="package_registry_enabled"
+ @change="onPackageRegistryEnabledToggle"
+ />
+ <div
+ v-if="packageRegistryApiForEveryoneEnabledShown"
+ class="project-feature-setting-group gl-pl-7 gl-sm-pl-5 gl-my-3"
+ >
+ <project-setting-row
+ :label="$options.i18n.packageRegistryForEveryoneLabel"
+ :help-text="$options.i18n.packageRegistryForEveryoneHelpText"
+ >
+ <gl-toggle
+ class="gl-my-2"
+ :value="packageRegistryApiForEveryoneEnabled"
+ :disabled="!packageRegistryEnabled"
+ :label="$options.i18n.packageRegistryForEveryoneLabel"
+ label-position="hidden"
+ name="package_registry_api_for_everyone_enabled"
+ @change="onPackageRegistryApiForEveryoneEnabledToggle"
+ />
+ </project-setting-row>
+ </div>
+ <input
+ :value="packageRegistryAccessLevel"
+ type="hidden"
name="project[project_feature_attributes][package_registry_access_level]"
/>
</project-setting-row>
@@ -923,11 +947,10 @@ export default {
/>
</project-setting-row>
<project-setting-row
- v-if="splitOperationsEnabled"
ref="monitor-settings"
:label="$options.i18n.monitorLabel"
:help-text="
- s__('ProjectSettings|Configure your project resources and monitor their health.')
+ s__('ProjectSettings|Monitor the health of your project and respond to incidents.')
"
>
<project-feature-setting
@@ -937,21 +960,6 @@ export default {
name="project[project_feature_attributes][monitor_access_level]"
/>
</project-setting-row>
- <project-setting-row
- v-else
- ref="operations-settings"
- :label="$options.i18n.operationsLabel"
- :help-text="
- s__('ProjectSettings|Configure your project resources and monitor their health.')
- "
- >
- <project-feature-setting
- v-model="operationsAccessLevel"
- :label="$options.i18n.operationsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][operations_access_level]"
- />
- </project-setting-row>
<div class="project-feature-setting-group gl-pl-7 gl-sm-pl-5">
<project-setting-row
ref="metrics-visibility-settings"
@@ -966,47 +974,45 @@ export default {
/>
</project-setting-row>
</div>
- <template v-if="splitOperationsEnabled">
- <project-setting-row
- ref="environments-settings"
+ <project-setting-row
+ ref="environments-settings"
+ :label="$options.i18n.environmentsLabel"
+ :help-text="$options.i18n.environmentsHelpText"
+ :help-path="environmentsHelpPath"
+ >
+ <project-feature-setting
+ v-model="environmentsAccessLevel"
:label="$options.i18n.environmentsLabel"
- :help-text="$options.i18n.environmentsHelpText"
- :help-path="environmentsHelpPath"
- >
- <project-feature-setting
- v-model="environmentsAccessLevel"
- :label="$options.i18n.environmentsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][environments_access_level]"
- />
- </project-setting-row>
- <project-setting-row
- ref="feature-flags-settings"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][environments_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
+ ref="feature-flags-settings"
+ :label="$options.i18n.featureFlagsLabel"
+ :help-text="$options.i18n.featureFlagsHelpText"
+ :help-path="featureFlagsHelpPath"
+ >
+ <project-feature-setting
+ v-model="featureFlagsAccessLevel"
:label="$options.i18n.featureFlagsLabel"
- :help-text="$options.i18n.featureFlagsHelpText"
- :help-path="featureFlagsHelpPath"
- >
- <project-feature-setting
- v-model="featureFlagsAccessLevel"
- :label="$options.i18n.featureFlagsLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][feature_flags_access_level]"
- />
- </project-setting-row>
- <project-setting-row
- ref="infrastructure-settings"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][feature_flags_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
+ ref="infrastructure-settings"
+ :label="$options.i18n.infrastructureLabel"
+ :help-text="$options.i18n.infrastructureHelpText"
+ :help-path="infrastructureHelpPath"
+ >
+ <project-feature-setting
+ v-model="infrastructureAccessLevel"
:label="$options.i18n.infrastructureLabel"
- :help-text="$options.i18n.infrastructureHelpText"
- :help-path="infrastructureHelpPath"
- >
- <project-feature-setting
- v-model="infrastructureAccessLevel"
- :label="$options.i18n.infrastructureLabel"
- :options="featureAccessLevelOptions"
- name="project[project_feature_attributes][infrastructure_access_level]"
- />
- </project-setting-row>
- </template>
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][infrastructure_access_level]"
+ />
+ </project-setting-row>
<project-setting-row
ref="releases-settings"
:label="$options.i18n.releasesLabel"
diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
index 5f08943d211..84ff802c268 100644
--- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
+++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
@@ -1,7 +1,15 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { joinPaths, webIDEUrl } from '~/lib/utils/url_utility';
import WebIdeButton from '~/vue_shared/components/web_ide_link.vue';
+import createDefaultClient from '~/lib/graphql';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
export default ({ el, router }) => {
if (!el) return;
@@ -9,15 +17,18 @@ export default ({ el, router }) => {
const { projectPath, ref, isBlob, webIdeUrl, ...options } = convertObjectPropsToCamelCase(
JSON.parse(el.dataset.options),
);
+ const { webIdePromoPopoverImg } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
el,
router,
+ apolloProvider,
render(h) {
return h(WebIdeButton, {
props: {
isBlob,
+ webIdePromoPopoverImg,
webIdeUrl: isBlob
? webIdeUrl
: webIDEUrl(
diff --git a/app/assets/javascripts/pages/projects/tags/new/index.js b/app/assets/javascripts/pages/projects/tags/new/index.js
index 9ef1017f9f2..eb1f705eab9 100644
--- a/app/assets/javascripts/pages/projects/tags/new/index.js
+++ b/app/assets/javascripts/pages/projects/tags/new/index.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import GLForm from '~/gl_form';
-import RefSelectDropdown from '~/ref_select_dropdown';
import ZenMode from '~/zen_mode';
+import initNewTagRefSelector from '~/tags/init_new_tag_ref_selector';
+initNewTagRefSelector();
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.tag-form')); // eslint-disable-line no-new
-new RefSelectDropdown($('.js-branch-select')); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 897acf9b02c..eaafc0235a8 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -4,6 +4,7 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
import EmailFormatValidator from '~/pages/sessions/new/email_format_validator';
+import { initLanguageSwitcher } from '~/language_switcher';
import Tracking from '~/tracking';
new UsernameValidator(); // eslint-disable-line no-new
@@ -19,3 +20,5 @@ trackNewRegistrations();
Tracking.enableFormTracking({
forms: { allow: ['new_user'] },
});
+
+initLanguageSwitcher();
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index b62417cf595..a84ed5f01ad 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import initVueAlerts from '~/vue_alerts';
import NoEmojiValidator from '~/emoji/no_emoji_validator';
+import { initLanguageSwitcher } from '~/language_switcher';
import LengthValidator from './length_validator';
import OAuthRememberMe from './oauth_remember_me';
import preserveUrlFragment from './preserve_url_fragment';
@@ -20,3 +21,4 @@ new OAuthRememberMe({
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
initVueAlerts();
+initLanguageSwitcher();
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index b72579276e8..b19809aff53 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -1,10 +1,11 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective, GlAlert } from '@gitlab/ui';
+import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { handleLocationHash } from '~/lib/utils/common_utils';
-import { renderGFM } from '../render_gfm_facade';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
components: {
@@ -12,7 +13,7 @@ export default {
GlAlert,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
getWikiContentUrl: {
@@ -86,9 +87,9 @@ export default {
<div
v-else-if="!loadingContentFailed && !isLoadingContent"
ref="content"
+ v-safe-html="content"
data-qa-selector="wiki_page_content"
data-testid="wiki-page-content"
class="js-wiki-page-content md"
- v-html="content /* eslint-disable-line vue/no-v-html */"
></div>
</template>
diff --git a/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js b/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js
deleted file mode 100644
index 90cc2983153..00000000000
--- a/app/assets/javascripts/pages/shared/wikis/render_gfm_facade.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import $ from 'jquery';
-
-export const renderGFM = (el) => {
- return $(el).renderGFM();
-};
diff --git a/app/assets/javascripts/pages/web_ide/remote_ide/index.js b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
new file mode 100644
index 00000000000..463798e85b9
--- /dev/null
+++ b/app/assets/javascripts/pages/web_ide/remote_ide/index.js
@@ -0,0 +1,3 @@
+import { mountRemoteIDE } from '~/ide/remote';
+
+mountRemoteIDE(document.getElementById('ide'));
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 0640faae8b7..ea8005e8dfb 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlDropdown, GlDropdownItem, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective, GlCollapsibleListbox } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { sortOrders, sortOrderOptions } from '../constants';
@@ -9,9 +9,8 @@ export default {
components: {
RequestWarning,
GlButton,
- GlDropdown,
- GlDropdownItem,
GlModal,
+ GlCollapsibleListbox,
},
directives: {
'gl-modal': GlModalDirective,
@@ -119,9 +118,6 @@ export default {
itemHasOpenedBacktrace(toggledIndex) {
return this.openedBacktraces.find((openedIndex) => openedIndex === toggledIndex) >= 0;
},
- changeSortOrder(order) {
- this.sortOrder = order;
- },
sortDetailByDuration(a, b) {
return a.duration < b.duration ? 1 : -1;
},
@@ -157,19 +153,14 @@ export default {
</div>
</div>
</div>
- <gl-dropdown
+ <gl-collapsible-listbox
v-if="displaySortOrder"
- :text="$options.sortOrderOptions[sortOrder]"
+ v-model="sortOrder"
+ :toggle-text="$options.sortOrderOptions[sortOrder].text"
+ :items="Object.values($options.sortOrderOptions)"
right
data-testid="performance-bar-sort-order"
- >
- <gl-dropdown-item
- v-for="option in Object.keys($options.sortOrderOptions)"
- :key="option"
- @click="changeSortOrder(option)"
- >{{ $options.sortOrderOptions[option] }}</gl-dropdown-item
- >
- </gl-dropdown>
+ />
</div>
<hr />
<table class="table gl-table">
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index a5fa85f1ed5..dbca8bc9be7 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -15,7 +15,7 @@ export default {
RequestSelector,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
store: {
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 3ebd222029b..91e905d62e6 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
export default {
@@ -7,7 +8,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
htmlId: {
diff --git a/app/assets/javascripts/performance_bar/constants.js b/app/assets/javascripts/performance_bar/constants.js
index 09745797424..6f4ddd5c242 100644
--- a/app/assets/javascripts/performance_bar/constants.js
+++ b/app/assets/javascripts/performance_bar/constants.js
@@ -6,6 +6,12 @@ export const sortOrders = {
};
export const sortOrderOptions = {
- [sortOrders.DURATION]: s__('PerformanceBar|Sort by duration'),
- [sortOrders.CHRONOLOGICAL]: s__('PerformanceBar|Sort chronologically'),
+ [sortOrders.DURATION]: {
+ value: sortOrders.DURATION,
+ text: s__('PerformanceBar|Sort by duration'),
+ },
+ [sortOrders.CHRONOLOGICAL]: {
+ value: sortOrders.CHRONOLOGICAL,
+ text: s__('PerformanceBar|Sort chronologically'),
+ },
};
diff --git a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
deleted file mode 100644
index cd7cb7f8393..00000000000
--- a/app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue
+++ /dev/null
@@ -1,490 +0,0 @@
-<script>
-import {
- GlAlert,
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- GlLink,
- GlSprintf,
- GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import * as Sentry from '@sentry/browser';
-import { uniqueId } from 'lodash';
-import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
-import { backOff } from '~/lib/utils/common_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-import { redirectTo } from '~/lib/utils/url_utility';
-import { s__, __, n__ } from '~/locale';
-import {
- VARIABLE_TYPE,
- FILE_TYPE,
- CONFIG_VARIABLES_TIMEOUT,
- CC_VALIDATION_REQUIRED_ERROR,
-} from '../constants';
-import filterVariables from '../utils/filter_variables';
-import RefsDropdown from './refs_dropdown.vue';
-
-const i18n = {
- variablesDescription: s__(
- 'Pipeline|Specify variable values to be used in this run. The values specified in %{linkStart}CI/CD settings%{linkEnd} will be used by default.',
- ),
- defaultError: __('Something went wrong on our end. Please try again.'),
- refsLoadingErrorTitle: s__('Pipeline|Branches or tags could not be loaded.'),
- submitErrorTitle: s__('Pipeline|Pipeline cannot be run.'),
- warningTitle: __('The form contains the following warning:'),
- maxWarningsSummary: __('%{total} warnings found: showing first %{warningsDisplayed}'),
- removeVariableLabel: s__('CiVariables|Remove variable'),
-};
-
-export default {
- typeOptions: {
- [VARIABLE_TYPE]: __('Variable'),
- [FILE_TYPE]: __('File'),
- },
- i18n,
- formElementClasses: 'gl-mr-3 gl-mb-3 gl-flex-basis-quarter gl-flex-shrink-0 gl-flex-grow-0',
- // this height value is used inline on the textarea to match the input field height
- // it's used to prevent the overwrite if 'gl-h-7' or 'gl-h-7!' were used
- textAreaStyle: { height: '32px' },
- components: {
- GlAlert,
- GlIcon,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlForm,
- GlFormGroup,
- GlFormInput,
- GlFormTextarea,
- GlLink,
- GlSprintf,
- GlLoadingIcon,
- RefsDropdown,
- CcValidationRequiredAlert: () =>
- import('ee_component/billings/components/cc_validation_required_alert.vue'),
- },
- directives: { SafeHtml },
- props: {
- pipelinesPath: {
- type: String,
- required: true,
- },
- configVariablesPath: {
- type: String,
- required: true,
- },
- defaultBranch: {
- type: String,
- required: true,
- },
- projectId: {
- type: String,
- required: true,
- },
- settingsLink: {
- type: String,
- required: true,
- },
- fileParams: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- refParam: {
- type: String,
- required: false,
- default: '',
- },
- variableParams: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- maxWarnings: {
- type: Number,
- required: true,
- },
- },
- data() {
- return {
- refValue: {
- shortName: this.refParam,
- },
- form: {},
- errorTitle: null,
- error: null,
- warnings: [],
- totalWarnings: 0,
- isWarningDismissed: false,
- isLoading: false,
- submitted: false,
- ccAlertDismissed: false,
- };
- },
- computed: {
- overMaxWarningsLimit() {
- return this.totalWarnings > this.maxWarnings;
- },
- warningsSummary() {
- return n__('%d warning found:', '%d warnings found:', this.warnings.length);
- },
- summaryMessage() {
- return this.overMaxWarningsLimit ? i18n.maxWarningsSummary : this.warningsSummary;
- },
- shouldShowWarning() {
- return this.warnings.length > 0 && !this.isWarningDismissed;
- },
- refShortName() {
- return this.refValue.shortName;
- },
- refFullName() {
- return this.refValue.fullName;
- },
- variables() {
- return this.form[this.refFullName]?.variables ?? [];
- },
- descriptions() {
- return this.form[this.refFullName]?.descriptions ?? {};
- },
- ccRequiredError() {
- return this.error === CC_VALIDATION_REQUIRED_ERROR && !this.ccAlertDismissed;
- },
- },
- watch: {
- refValue() {
- this.loadConfigVariablesForm();
- },
- },
- created() {
- // this is needed until we add support for ref type in url query strings
- // ensure default branch is called with full ref on load
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- if (this.refValue.shortName === this.defaultBranch) {
- this.refValue.fullName = `refs/heads/${this.refValue.shortName}`;
- }
-
- this.loadConfigVariablesForm();
- },
- methods: {
- addEmptyVariable(refValue) {
- const { variables } = this.form[refValue];
-
- const lastVar = variables[variables.length - 1];
- if (lastVar?.key === '' && lastVar?.value === '') {
- return;
- }
-
- variables.push({
- uniqueId: uniqueId(`var-${refValue}`),
- variable_type: VARIABLE_TYPE,
- key: '',
- value: '',
- });
- },
- setVariable(refValue, type, key, value) {
- const { variables } = this.form[refValue];
-
- const variable = variables.find((v) => v.key === key);
- if (variable) {
- variable.type = type;
- variable.value = value;
- } else {
- variables.push({
- uniqueId: uniqueId(`var-${refValue}`),
- key,
- value,
- variable_type: type,
- });
- }
- },
- setVariableType(key, type) {
- const { variables } = this.form[this.refFullName];
- const variable = variables.find((v) => v.key === key);
- variable.variable_type = type;
- },
- setVariableParams(refValue, type, paramsObj) {
- Object.entries(paramsObj).forEach(([key, value]) => {
- this.setVariable(refValue, type, key, value);
- });
- },
- removeVariable(index) {
- this.variables.splice(index, 1);
- },
- canRemove(index) {
- return index < this.variables.length - 1;
- },
- loadConfigVariablesForm() {
- // Skip when variables already cached in `form`
- if (this.form[this.refFullName]) {
- return;
- }
-
- this.fetchConfigVariables(this.refFullName || this.refShortName)
- .then(({ descriptions, params }) => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions,
- });
-
- // Add default variables from yml
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, params);
- })
- .catch(() => {
- Vue.set(this.form, this.refFullName, {
- variables: [],
- descriptions: {},
- });
- })
- .finally(() => {
- // Add/update variables, e.g. from query string
- if (this.variableParams) {
- this.setVariableParams(this.refFullName, VARIABLE_TYPE, this.variableParams);
- }
- if (this.fileParams) {
- this.setVariableParams(this.refFullName, FILE_TYPE, this.fileParams);
- }
-
- // Adds empty var at the end of the form
- this.addEmptyVariable(this.refFullName);
- });
- },
- fetchConfigVariables(refValue) {
- this.isLoading = true;
-
- return backOff((next, stop) => {
- axios
- .get(this.configVariablesPath, {
- params: {
- sha: refValue,
- },
- })
- .then(({ data, status }) => {
- if (status === httpStatusCodes.NO_CONTENT) {
- next();
- } else {
- this.isLoading = false;
- stop(data);
- }
- })
- .catch((error) => {
- stop(error);
- });
- }, CONFIG_VARIABLES_TIMEOUT)
- .then((data) => {
- const params = {};
- const descriptions = {};
-
- Object.entries(data).forEach(([key, { value, description }]) => {
- if (description) {
- params[key] = value;
- descriptions[key] = description;
- }
- });
-
- return { params, descriptions };
- })
- .catch((error) => {
- this.isLoading = false;
-
- Sentry.captureException(error);
-
- return { params: {}, descriptions: {} };
- });
- },
- createPipeline() {
- this.submitted = true;
- this.ccAlertDismissed = false;
-
- return axios
- .post(this.pipelinesPath, {
- // send shortName as fall back for query params
- // https://gitlab.com/gitlab-org/gitlab/-/issues/287815
- ref: this.refValue.fullName || this.refShortName,
- variables_attributes: filterVariables(this.variables),
- })
- .then(({ data }) => {
- redirectTo(`${this.pipelinesPath}/${data.id}`);
- })
- .catch((err) => {
- // always re-enable submit button
- this.submitted = false;
-
- const {
- errors = [],
- warnings = [],
- total_warnings: totalWarnings = 0,
- } = err.response.data;
- const [error] = errors;
-
- this.reportError({
- title: i18n.submitErrorTitle,
- error,
- warnings,
- totalWarnings,
- });
- });
- },
- onRefsLoadingError(error) {
- this.reportError({ title: i18n.refsLoadingErrorTitle });
-
- Sentry.captureException(error);
- },
- reportError({ title = null, error = i18n.defaultError, warnings = [], totalWarnings = 0 }) {
- this.errorTitle = title;
- this.error = error;
- this.warnings = warnings;
- this.totalWarnings = totalWarnings;
- },
- dismissError() {
- this.ccAlertDismissed = true;
- this.error = null;
- },
- },
-};
-</script>
-
-<template>
- <gl-form @submit.prevent="createPipeline">
- <cc-validation-required-alert v-if="ccRequiredError" class="gl-pb-5" @dismiss="dismissError" />
- <gl-alert
- v-else-if="error"
- :title="errorTitle"
- :dismissible="false"
- variant="danger"
- class="gl-mb-4"
- data-testid="run-pipeline-error-alert"
- >
- <span v-safe-html="error"></span>
- </gl-alert>
- <gl-alert
- v-if="shouldShowWarning"
- :title="$options.i18n.warningTitle"
- variant="warning"
- class="gl-mb-4"
- data-testid="run-pipeline-warning-alert"
- @dismiss="isWarningDismissed = true"
- >
- <details>
- <summary>
- <gl-sprintf :message="summaryMessage">
- <template #total>
- {{ totalWarnings }}
- </template>
- <template #warningsDisplayed>
- {{ maxWarnings }}
- </template>
- </gl-sprintf>
- </summary>
- <p
- v-for="(warning, index) in warnings"
- :key="`warning-${index}`"
- data-testid="run-pipeline-warning"
- >
- {{ warning }}
- </p>
- </details>
- </gl-alert>
- <gl-form-group :label="s__('Pipeline|Run for branch name or tag')">
- <refs-dropdown v-model="refValue" @loadingError="onRefsLoadingError" />
- </gl-form-group>
-
- <gl-loading-icon v-if="isLoading" class="gl-mb-5" size="lg" />
-
- <gl-form-group v-else :label="s__('Pipeline|Variables')">
- <div
- v-for="(variable, index) in variables"
- :key="variable.uniqueId"
- class="gl-mb-3 gl-pb-2"
- data-testid="ci-variable-row"
- data-qa-selector="ci_variable_row_container"
- >
- <div
- class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-md-flex-direction-row"
- >
- <gl-dropdown
- :text="$options.typeOptions[variable.variable_type]"
- :class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-type"
- >
- <gl-dropdown-item
- v-for="type in Object.keys($options.typeOptions)"
- :key="type"
- @click="setVariableType(variable.key, type)"
- >
- {{ $options.typeOptions[type] }}
- </gl-dropdown-item>
- </gl-dropdown>
- <gl-form-input
- v-model="variable.key"
- :placeholder="s__('CiVariables|Input variable key')"
- :class="$options.formElementClasses"
- data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
- @change="addEmptyVariable(refFullName)"
- />
- <gl-form-textarea
- v-model="variable.value"
- :placeholder="s__('CiVariables|Input variable value')"
- class="gl-mb-3"
- :style="$options.textAreaStyle"
- :no-resize="false"
- data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
- />
-
- <template v-if="variables.length > 1">
- <gl-button
- v-if="canRemove(index)"
- class="gl-md-ml-3 gl-mb-3"
- data-testid="remove-ci-variable-row"
- variant="danger"
- category="secondary"
- :aria-label="$options.i18n.removeVariableLabel"
- @click="removeVariable(index)"
- >
- <gl-icon class="gl-mr-0! gl-display-none gl-md-display-block" name="clear" />
- <span class="gl-md-display-none">{{ $options.i18n.removeVariableLabel }}</span>
- </gl-button>
- <gl-button
- v-else
- class="gl-md-ml-3 gl-mb-3 gl-display-none gl-md-display-block gl-visibility-hidden"
- icon="clear"
- :aria-label="$options.i18n.removeVariableLabel"
- />
- </template>
- </div>
- <div v-if="descriptions[variable.key]" class="gl-text-gray-500 gl-mb-3">
- {{ descriptions[variable.key] }}
- </div>
- </div>
-
- <template #description
- ><gl-sprintf :message="$options.i18n.variablesDescription">
- <template #link="{ content }">
- <gl-link :href="settingsLink">{{ content }}</gl-link>
- </template>
- </gl-sprintf></template
- >
- </gl-form-group>
- <div class="gl-pt-5 gl-display-flex">
- <gl-button
- type="submit"
- category="primary"
- variant="confirm"
- class="js-no-auto-disable gl-mr-3"
- data-qa-selector="run_pipeline_button"
- data-testid="run_pipeline_button"
- :disabled="submitted"
- >{{ s__('Pipeline|Run pipeline') }}</gl-button
- >
- <gl-button :href="pipelinesPath">{{ __('Cancel') }}</gl-button>
- </div>
- </gl-form>
-</template>
diff --git a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
index a9af1181027..5692627abef 100644
--- a/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
+++ b/app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue
@@ -12,11 +12,11 @@ import {
GlLink,
GlSprintf,
GlLoadingIcon,
- GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { uniqueId } from 'lodash';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { redirectTo } from '~/lib/utils/url_utility';
import { s__, __, n__ } from '~/locale';
import { VARIABLE_TYPE, FILE_TYPE, CC_VALIDATION_REQUIRED_ERROR } from '../constants';
@@ -400,11 +400,13 @@ export default {
:class="$options.formElementClasses"
class="gl-flex-grow-1 gl-mr-0!"
data-testid="pipeline-form-ci-variable-value-dropdown"
+ data-qa-selector="ci_variable_value_dropdown"
>
<gl-dropdown-item
v-for="value in predefinedValueOptions[variable.key]"
:key="value"
data-testid="pipeline-form-ci-variable-value-dropdown-items"
+ data-qa-selector="ci_variable_value_dropdown_item"
@click="setVariableAttribute(variable.key, 'value', value)"
>
{{ value }}
diff --git a/app/assets/javascripts/pipeline_new/index.js b/app/assets/javascripts/pipeline_new/index.js
index 60b4c93d1d5..71c76aeab36 100644
--- a/app/assets/javascripts/pipeline_new/index.js
+++ b/app/assets/javascripts/pipeline_new/index.js
@@ -1,53 +1,9 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import LegacyPipelineNewForm from './components/legacy_pipeline_new_form.vue';
import PipelineNewForm from './components/pipeline_new_form.vue';
import { resolvers } from './graphql/resolvers';
-const mountLegacyPipelineNewForm = (el) => {
- const {
- // provide/inject
- projectRefsEndpoint,
-
- // props
- configVariablesPath,
- defaultBranch,
- fileParam,
- maxWarnings,
- pipelinesPath,
- projectId,
- refParam,
- settingsLink,
- varParam,
- } = el.dataset;
-
- const variableParams = JSON.parse(varParam);
- const fileParams = JSON.parse(fileParam);
-
- return new Vue({
- el,
- provide: {
- projectRefsEndpoint,
- },
- render(createElement) {
- return createElement(LegacyPipelineNewForm, {
- props: {
- configVariablesPath,
- defaultBranch,
- fileParams,
- maxWarnings: Number(maxWarnings),
- pipelinesPath,
- projectId,
- refParam,
- settingsLink,
- variableParams,
- },
- });
- },
- });
-};
-
const mountPipelineNewForm = (el) => {
const {
// provide/inject
@@ -101,9 +57,5 @@ const mountPipelineNewForm = (el) => {
export default () => {
const el = document.getElementById('js-new-pipeline');
- if (gon.features?.runPipelineGraphql) {
- mountPipelineNewForm(el);
- } else {
- mountLegacyPipelineNewForm(el);
- }
+ mountPipelineNewForm(el);
};
diff --git a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
index 8f9198855c6..e3d825bbcc7 100644
--- a/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
+++ b/app/assets/javascripts/pipeline_wizard/components/step_nav.vue
@@ -27,12 +27,13 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex">
<slot name="before"></slot>
<gl-button
v-if="showBackButton"
category="secondary"
data-testid="back-button"
+ class="gl-mr-3"
@click="$emit('back')"
>
{{ __('Back') }}
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
index f822e2c0874..4d7596e6e16 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -148,12 +148,13 @@ export default {
reportMessageToSentry(
this.$options.name,
- `| type: ${LOAD_FAILURE} , info: ${serializeLoadErrors(err)}`,
+ `| type: ${LOAD_FAILURE} , info: ${JSON.stringify(err)}`,
{
+ graphViewType: this.graphViewType,
+ graphqlResourceEtag: this.graphqlResourceEtag,
+ metricsPath: this.metricsPath,
projectPath: this.pipelineProjectPath,
pipelineIid: this.pipelineIid,
- pipelineStages: this.pipeline?.stages?.length || 0,
- nbOfDownstreams: this.pipeline?.downstream?.length || 0,
},
);
},
diff --git a/app/assets/javascripts/pipelines/components/graph/job_item.vue b/app/assets/javascripts/pipelines/components/graph/job_item.vue
index 377f21b299f..4f2be27486c 100644
--- a/app/assets/javascripts/pipelines/components/graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/graph/job_item.vue
@@ -252,7 +252,7 @@ export default {
@click="jobItemClick"
@mouseout="hideTooltips"
>
- <div class="ci-job-name-component gl-display-flex gl-align-items-center">
+ <div class="gl-display-flex gl-align-items-center gl-flex-grow-1">
<ci-icon :size="24" :status="job.status" class="gl-line-height-0" />
<div class="gl-pl-3 gl-pr-3 gl-display-flex gl-flex-direction-column gl-pipeline-job-width">
<div class="gl-text-truncate gl-pr-9 gl-line-height-normal">{{ job.name }}</div>
diff --git a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
index 18607bfae1c..c56537f4039 100644
--- a/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/failed_jobs_table.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlLink, GlSafeHtmlDirective, GlTableLite } from '@gitlab/ui';
+import { GlButton, GlLink, GlTableLite } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __, s__ } from '~/locale';
import { createAlert } from '~/flash';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -17,7 +18,7 @@ export default {
GlTableLite,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
failedJobs: {
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
index 7ee5ec48f44..387b01aee7e 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -70,7 +70,6 @@ export default {
axios
.post(`${this.link}.json`)
.then(() => {
- this.isDisabled = false;
this.isLoading = false;
this.$emit('pipelineActionRequestComplete');
diff --git a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
index f4fc6893520..1c7f5a7476d 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/job_name_component.vue
@@ -29,7 +29,7 @@ export default {
};
</script>
<template>
- <span class="ci-job-name-component mw-100 gl-display-flex gl-align-items-center">
+ <span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
<ci-icon :size="iconSize" :status="status" class="gl-line-height-0" />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
index 211c5f117c7..51b46f25048 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/job_item.vue
@@ -137,9 +137,6 @@ export default {
hideTooltips() {
this.$root.$emit(BV_HIDE_TOOLTIP);
},
- pipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
},
};
</script>
@@ -163,7 +160,7 @@ export default {
@click.stop="hideTooltips"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
+ <job-name-component :name="job.name" :status="job.status" />
</gl-link>
<div
@@ -175,7 +172,7 @@ export default {
data-testid="job-without-link"
@mouseout="hideTooltips"
>
- <job-name-component :name="job.name" :status="job.status" :icon-size="24" />
+ <job-name-component :name="job.name" :status="job.status" />
</div>
<action-component
@@ -184,7 +181,6 @@ export default {
:link="status.action.path"
:action-icon="status.action.icon"
data-qa-selector="action_button"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
index 993fa121d89..827adf9f7f7 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue
@@ -35,11 +35,6 @@ export default {
required: true,
default: () => [],
},
- stagesClass: {
- type: [Array, Object, String],
- required: false,
- default: '',
- },
updateDropdown: {
type: Boolean,
required: false,
@@ -56,15 +51,10 @@ export default {
return Boolean(this.downstreamPipelines.length);
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
- <div class="stage-cell" data-testid="pipeline-mini-graph">
+ <div data-testid="pipeline-mini-graph">
<linked-pipelines-mini-list
v-if="upstreamPipeline"
:triggered-by="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ [
@@ -82,9 +72,7 @@ export default {
:is-merge-train="isMergeTrain"
:stages="stages"
:update-dropdown="updateDropdown"
- :stages-class="stagesClass"
data-testid="pipeline-stages"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
<gl-icon
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
index ba150919e58..ec42b738e03 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stage.vue
@@ -100,13 +100,6 @@ export default {
});
});
},
- pipelineActionRequestComplete() {
- // close the dropdown in MR widget
- this.$refs.dropdown.hide();
-
- // warn the pipelines table to update
- this.$emit('pipelineActionRequestComplete');
- },
stageAriaLabel(title) {
return sprintf(__('View Stage: %{title}'), { title });
},
@@ -149,7 +142,7 @@ export default {
class="js-builds-dropdown-list scrollable-menu"
data-testid="mini-pipeline-graph-dropdown-menu-list"
>
- <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-pb-3">
+ <div class="gl--flex-center gl-border-b gl-font-weight-bold gl-mb-3 gl-pb-3">
<span class="gl-mr-1">{{ $options.i18n.stage }}</span>
<span data-testid="pipeline-stage-dropdown-menu-title">{{ stageName }}</span>
</div>
@@ -158,11 +151,10 @@ export default {
:dropdown-length="dropdownContent.length"
:job="job"
css-class-job-name="mini-pipeline-graph-dropdown-item"
- @pipelineActionRequestComplete="pipelineActionRequestComplete"
/>
</li>
<template v-if="isMergeTrain">
- <li class="gl-new-dropdown-divider" role="presentation">
+ <li class="gl-dropdown-divider" role="presentation">
<hr role="separator" aria-orientation="horizontal" class="dropdown-divider" />
</li>
<li>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
index e965dc5e6b0..ba549d9b423 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_mini_graph/pipeline_stages.vue
@@ -17,22 +17,12 @@ export default {
required: false,
default: false,
},
- stagesClass: {
- type: [Array, Object, String],
- required: false,
- default: '',
- },
isMergeTrain: {
type: Boolean,
required: false,
default: false,
},
},
- methods: {
- onPipelineActionRequestComplete() {
- this.$emit('pipelineActionRequestComplete');
- },
- },
};
</script>
<template>
@@ -40,14 +30,12 @@ export default {
<div
v-for="stage in stages"
:key="stage.name"
- :class="stagesClass"
- class="dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle stage-container"
+ class="pipeline-mini-graph-stage-container dropdown gl-display-inline-block gl-mr-2 gl-my-2 gl-vertical-align-middle"
>
<pipeline-stage
:stage="stage"
:update-dropdown="updateDropdown"
:is-merge-train="isMergeTrain"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="$emit('miniGraphStageClick')"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
index 3eafb36bd1d..03a2eac89e4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue
@@ -8,7 +8,7 @@ import {
RUNNERS_DOCUMENTATION_LINK_CLICKED_EVENT,
RUNNERS_SETTINGS_BUTTON_CLICKED_EVENT,
I18N,
-} from '~/pipeline_editor/constants';
+} from '~/ci/pipeline_editor/constants';
import Tracking from '~/tracking';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isExperimentVariant } from '~/experimentation/utils';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
index af089aebbbe..7dc1e60610e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_filtered_search.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearch } from '@gitlab/ui';
import { map } from 'lodash';
import { s__ } from '~/locale';
import Tracking from '~/tracking';
-import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
+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';
@@ -54,7 +54,7 @@ export default {
title: s__('Pipeline|Trigger author'),
unique: true,
token: PipelineTriggerAuthorToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
},
{
@@ -63,7 +63,7 @@ export default {
title: s__('Pipeline|Branch name'),
unique: true,
token: PipelineBranchNameToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
defaultBranchName: this.defaultBranchName,
disabled: this.selectedTypes.includes(this.$options.tagType),
@@ -74,7 +74,7 @@ export default {
title: s__('Pipeline|Tag name'),
unique: true,
token: PipelineTagNameToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
projectId: this.projectId,
disabled: this.selectedTypes.includes(this.$options.branchType),
},
@@ -84,7 +84,7 @@ export default {
title: s__('Pipeline|Status'),
unique: true,
token: PipelineStatusToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
{
type: this.$options.sourceType,
@@ -92,7 +92,7 @@ export default {
title: s__('Pipeline|Source'),
unique: true,
token: PipelineSourceToken,
- operators: OPERATOR_IS_ONLY,
+ operators: OPERATORS_IS,
},
];
},
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
index f6e46c090d3..346f5735576 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_table.vue
@@ -124,9 +124,6 @@ export default {
eventHub.$emit('postAction', this.endpoint);
this.cancelingPipeline = this.pipelineId;
},
- onPipelineActionRequestComplete() {
- eventHub.$emit('refreshPipelinesTable');
- },
trackPipelineMiniGraph() {
this.track('click_minigraph', { label: TRACKING_CATEGORIES.table });
},
@@ -179,7 +176,6 @@ export default {
:stages="item.details.stages"
:update-dropdown="updateGraphDropdown"
:upstream-pipeline="item.triggered_by"
- @pipelineActionRequestComplete="onPipelineActionRequestComplete"
@miniGraphStageClick="trackPipelineMiniGraph"
/>
</template>
diff --git a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
index e5666f7a658..3f2c013d44a 100644
--- a/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
+++ b/app/assets/javascripts/pipelines/components/test_reports/test_reports.vue
@@ -1,8 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import createTestReportsStore from '../../stores/test_reports';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
@@ -17,7 +15,6 @@ export default {
TestSummary,
TestSummaryTable,
},
- mixins: [glFeatureFlagMixin()],
inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'],
computed: {
...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']),
@@ -31,17 +28,6 @@ export default {
},
},
created() {
- if (!this.glFeatures.pipelineTabsVue) {
- this.$store.registerModule(
- 'testReports',
- createTestReportsStore({
- blobPath: this.blobPath,
- summaryEndpoint: this.summaryEndpoint,
- suiteEndpoint: this.suiteEndpoint,
- }),
- );
- }
-
this.fetchSummary();
},
methods: {
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
index 9602ca1ba88..07551c2342f 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines_mixin.js
@@ -55,7 +55,6 @@ export default {
eventHub.$on('retryPipeline', this.postAction);
eventHub.$on('clickedDropdown', this.updateTable);
eventHub.$on('updateTable', this.updateTable);
- eventHub.$on('refreshPipelinesTable', this.fetchPipelines);
eventHub.$on('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
beforeDestroy() {
@@ -63,7 +62,6 @@ export default {
eventHub.$off('retryPipeline', this.postAction);
eventHub.$off('clickedDropdown', this.updateTable);
eventHub.$off('updateTable', this.updateTable);
- eventHub.$off('refreshPipelinesTable', this.fetchPipelines);
eventHub.$off('runMergeRequestPipeline', this.runMergeRequestPipeline);
},
destroyed() {
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 1bbdd3625be..f00378733fc 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,36 +1,33 @@
import VueRouter from 'vue-router';
import { createAlert } from '~/flash';
-import { __, s__ } from '~/locale';
-import createDagApp from './pipeline_details_dag';
-import { createPipelinesDetailApp } from './pipeline_details_graph';
+import { __ } from '~/locale';
import { createPipelineHeaderApp } from './pipeline_details_header';
-import { createPipelineJobsApp } from './pipeline_details_jobs';
-import { createPipelineFailedJobsApp } from './pipeline_details_failed_jobs';
import { apolloProvider } from './pipeline_shared_client';
-import { createTestDetails } from './pipeline_test_details';
const SELECTORS = {
- PIPELINE_DETAILS: '.js-pipeline-details-vue',
- PIPELINE_GRAPH: '#js-pipeline-graph-vue',
PIPELINE_HEADER: '#js-pipeline-header-vue',
PIPELINE_TABS: '#js-pipeline-tabs',
- PIPELINE_TESTS: '#js-pipeline-tests-detail',
- PIPELINE_JOBS: '#js-pipeline-jobs-vue',
- PIPELINE_FAILED_JOBS: '#js-pipeline-failed-jobs-vue',
};
export default async function initPipelineDetailsBundle() {
- const { dataset } = document.querySelector(SELECTORS.PIPELINE_DETAILS);
+ const { dataset: headerDataset } = document.querySelector(SELECTORS.PIPELINE_HEADER);
try {
- createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
+ createPipelineHeaderApp(
+ SELECTORS.PIPELINE_HEADER,
+ apolloProvider,
+ headerDataset.graphqlResourceEtag,
+ );
} catch {
createAlert({
message: __('An error occurred while loading a section of this page.'),
});
}
- if (gon.features?.pipelineTabsVue) {
+ const tabsEl = document.querySelector(SELECTORS.PIPELINE_TABS);
+
+ if (tabsEl) {
+ const { dataset } = tabsEl;
const { createAppOptions } = await import('ee_else_ce/pipelines/pipeline_tabs');
const { createPipelineTabs } = await import('./pipeline_tabs');
const { routes } = await import('ee_else_ce/pipelines/routes');
@@ -49,45 +46,5 @@ export default async function initPipelineDetailsBundle() {
message: __('An error occurred while loading a section of this page.'),
});
}
- } else {
- try {
- createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
- } catch {
- createAlert({
- message: __('An error occurred while loading the pipeline.'),
- });
- }
-
- try {
- createDagApp(apolloProvider);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Needs tab.'),
- });
- }
-
- try {
- createTestDetails(SELECTORS.PIPELINE_TESTS);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Test Reports tab.'),
- });
- }
-
- try {
- createPipelineJobsApp(SELECTORS.PIPELINE_JOBS);
- } catch {
- createAlert({
- message: __('An error occurred while loading the Jobs tab.'),
- });
- }
-
- try {
- createPipelineFailedJobsApp(SELECTORS.PIPELINE_FAILED_JOBS);
- } catch {
- createAlert({
- message: s__('Jobs|An error occurred while loading the Failed Jobs tab.'),
- });
- }
}
}
diff --git a/app/assets/javascripts/pipelines/pipeline_details_dag.js b/app/assets/javascripts/pipelines/pipeline_details_dag.js
deleted file mode 100644
index b2cb0457c4d..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_dag.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import Dag from './components/dag/dag.vue';
-
-Vue.use(VueApollo);
-
-const createDagApp = (apolloProvider) => {
- const el = document.querySelector('#js-pipeline-dag-vue');
-
- if (!el) {
- return;
- }
-
- const {
- aboutDagDocPath,
- dagDocPath,
- emptySvgPath,
- pipelineProjectPath,
- pipelineIid,
- } = el.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- Dag,
- },
- apolloProvider,
- provide: {
- aboutDagDocPath,
- dagDocPath,
- emptySvgPath,
- pipelineProjectPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement('dag', {});
- },
- });
-};
-
-export default createDagApp;
diff --git a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
deleted file mode 100644
index 7bf3b64bf47..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_failed_jobs.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import FailedJobsApp from './components/jobs/failed_jobs_app.vue';
-
-Vue.use(VueApollo);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
-export const createPipelineFailedJobsApp = (selector) => {
- const containerEl = document.querySelector(selector);
-
- if (!containerEl) {
- return false;
- }
-
- const { fullPath, pipelineIid, failedJobsSummaryData } = containerEl.dataset;
-
- return new Vue({
- el: containerEl,
- apolloProvider,
- provide: {
- fullPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement(FailedJobsApp, {
- props: {
- failedJobsSummary: JSON.parse(failedJobsSummaryData),
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_graph.js b/app/assets/javascripts/pipelines/pipeline_details_graph.js
deleted file mode 100644
index 9dd5cd7b281..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_graph.js
+++ /dev/null
@@ -1,35 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import PipelineGraphWrapper from './components/graph/graph_component_wrapper.vue';
-import { reportToSentry } from './utils';
-
-Vue.use(VueApollo);
-
-const createPipelinesDetailApp = (
- selector,
- apolloProvider,
- { pipelineProjectPath, pipelineIid, metricsPath, graphqlResourceEtag } = {},
-) => {
- // eslint-disable-next-line no-new
- new Vue({
- el: selector,
- components: {
- PipelineGraphWrapper,
- },
- apolloProvider,
- provide: {
- metricsPath,
- pipelineProjectPath,
- pipelineIid,
- graphqlResourceEtag,
- },
- errorCaptured(err, _vm, info) {
- reportToSentry('pipeline_details_graph', `error: ${err}, info: ${info}`);
- },
- render(createElement) {
- return createElement(PipelineGraphWrapper);
- },
- });
-};
-
-export { createPipelinesDetailApp };
diff --git a/app/assets/javascripts/pipelines/pipeline_details_jobs.js b/app/assets/javascripts/pipelines/pipeline_details_jobs.js
deleted file mode 100644
index a1294a484f0..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_details_jobs.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { GlToast } from '@gitlab/ui';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import JobsApp from './components/jobs/jobs_app.vue';
-
-Vue.use(VueApollo);
-Vue.use(GlToast);
-
-const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(),
-});
-
-export const createPipelineJobsApp = (selector) => {
- const containerEl = document.querySelector(selector);
-
- if (!containerEl) {
- return false;
- }
-
- const { fullPath, pipelineIid } = containerEl.dataset;
-
- return new Vue({
- el: containerEl,
- apolloProvider,
- provide: {
- fullPath,
- pipelineIid,
- },
- render(createElement) {
- return createElement(JobsApp);
- },
- });
-};
diff --git a/app/assets/javascripts/pipelines/pipeline_test_details.js b/app/assets/javascripts/pipelines/pipeline_test_details.js
deleted file mode 100644
index fe4ca8e9529..00000000000
--- a/app/assets/javascripts/pipelines/pipeline_test_details.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import Translate from '~/vue_shared/translate';
-import TestReports from './components/test_reports/test_reports.vue';
-
-Vue.use(Vuex);
-Vue.use(Translate);
-
-export const createTestDetails = (selector) => {
- const el = document.querySelector(selector);
- const {
- blobPath,
- emptyStateImagePath,
- hasTestReport,
- summaryEndpoint,
- suiteEndpoint,
- artifactsExpiredImagePath,
- } = el?.dataset || {};
-
- // eslint-disable-next-line no-new
- new Vue({
- el,
- components: {
- TestReports,
- },
- provide: {
- emptyStateImagePath,
- artifactsExpiredImagePath,
- hasTestReport: parseBoolean(hasTestReport),
- blobPath,
- summaryEndpoint,
- suiteEndpoint,
- },
- store: new Vuex.Store(),
- render(createElement) {
- return createElement('test-reports');
- },
- });
-};
diff --git a/app/assets/javascripts/popovers/components/popovers.vue b/app/assets/javascripts/popovers/components/popovers.vue
index a758503b56b..7ec54231e65 100644
--- a/app/assets/javascripts/popovers/components/popovers.vue
+++ b/app/assets/javascripts/popovers/components/popovers.vue
@@ -1,5 +1,6 @@
<script>
-import { GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
const newPopover = (element) => {
const { content, html, placement, title, triggers = 'focus' } = element.dataset;
@@ -19,7 +20,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
data() {
return {
diff --git a/app/assets/javascripts/profile/account/components/update_username.vue b/app/assets/javascripts/profile/account/components/update_username.vue
index b038b78088f..51e62984715 100644
--- a/app/assets/javascripts/profile/account/components/update_username.vue
+++ b/app/assets/javascripts/profile/account/components/update_username.vue
@@ -1,6 +1,7 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import { escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert, VARIANT_INFO } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __, s__, sprintf } from '~/locale';
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index 52da8aaba4d..a037e721677 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -28,6 +28,11 @@ export default {
required: false,
default: '',
},
+ blanked: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
i18n: {
noResultsMessage: I18N_NO_RESULTS_MESSAGE,
@@ -36,7 +41,7 @@ export default {
},
data() {
return {
- searchTerm: this.value,
+ searchTerm: this.blanked ? '' : this.value,
};
},
computed: {
diff --git a/app/assets/javascripts/projects/commit/components/form_modal.vue b/app/assets/javascripts/projects/commit/components/form_modal.vue
index d9aaa574fec..1febe8ceaab 100644
--- a/app/assets/javascripts/projects/commit/components/form_modal.vue
+++ b/app/assets/javascripts/projects/commit/components/form_modal.vue
@@ -41,6 +41,11 @@ export default {
required: false,
default: false,
},
+ isRevert: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
primaryActionEventName: {
type: String,
required: false,
@@ -150,7 +155,12 @@ export default {
>
<input id="start_branch" type="hidden" name="start_branch" :value="branch" />
- <branches-dropdown class="gl-w-half" :value="branch" @selectBranch="setBranch" />
+ <branches-dropdown
+ class="gl-w-half"
+ :value="branch"
+ :blanked="isRevert"
+ @selectBranch="setBranch"
+ />
</gl-form-group>
<gl-form-checkbox
diff --git a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
index 849b2f4858c..41be71932e5 100644
--- a/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
+++ b/app/assets/javascripts/projects/commit/init_revert_commit_modal.js
@@ -49,6 +49,7 @@ export default function initInviteMembersModal(primaryActionEventName) {
i18n: { ...I18N_REVERT_MODAL, ...I18N_MODAL },
openModal: OPEN_REVERT_MODAL,
modalId: REVERT_MODAL_ID,
+ isRevert: true,
primaryActionEventName,
},
}),
diff --git a/app/assets/javascripts/projects/commits/index.js b/app/assets/javascripts/projects/commits/index.js
index 03b94fde0f3..53169f689c9 100644
--- a/app/assets/javascripts/projects/commits/index.js
+++ b/app/assets/javascripts/projects/commits/index.js
@@ -1,11 +1,13 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import { visitUrl } from '~/lib/utils/url_utility';
+import RefSelector from '~/ref/components/ref_selector.vue';
import AuthorSelectApp from './components/author_select.vue';
import store from './store';
Vue.use(Vuex);
-export default (el) => {
+export const mountCommits = (el) => {
if (!el) {
return null;
}
@@ -24,3 +26,30 @@ export default (el) => {
},
});
};
+
+export const initCommitsRefSwitcher = () => {
+ const el = document.getElementById('js-project-commits-ref-switcher');
+ const COMMITS_PATH_REGEX = /^(.*?)\/-\/commits/g;
+
+ if (!el) return false;
+
+ const { projectId, ref, commitsPath } = el.dataset;
+ const commitsPathPrefix = commitsPath.match(COMMITS_PATH_REGEX)?.[0];
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(RefSelector, {
+ props: {
+ projectId,
+ value: ref,
+ },
+ on: {
+ input(selected) {
+ visitUrl(`${commitsPathPrefix}/${selected}`);
+ },
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
index ba1e00a2b36..c00e75db722 100644
--- a/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
+++ b/app/assets/javascripts/projects/compare/components/repo_dropdown.vue
@@ -57,7 +57,7 @@ export default {
<gl-dropdown
:text="selectedProject.name"
:header-text="s__(`CompareRevisions|Select target project`)"
- class="gl-w-full gl-font-monospace gl-sm-pr-3"
+ class="gl-w-full gl-font-monospace"
toggle-class="gl-min-w-0"
:disabled="disableRepoDropdown"
>
diff --git a/app/assets/javascripts/projects/compare/components/revision_card.vue b/app/assets/javascripts/projects/compare/components/revision_card.vue
index d6ada24604d..162aca44f9d 100644
--- a/app/assets/javascripts/projects/compare/components/revision_card.vue
+++ b/app/assets/javascripts/projects/compare/components/revision_card.vue
@@ -43,7 +43,7 @@ export default {
<h2 class="gl-font-size-h2">
{{ s__(`CompareRevisions|${revisionText}`) }}
</h2>
- <div class="gl-sm-display-flex gl-align-items-center">
+ <div class="gl-sm-display-flex gl-align-items-center gl-gap-3">
<repo-dropdown
class="gl-sm-w-half"
:params-name="paramsName"
diff --git a/app/assets/javascripts/projects/default_project_templates.js b/app/assets/javascripts/projects/default_project_templates.js
index 3671b24b502..a44855c14d5 100644
--- a/app/assets/javascripts/projects/default_project_templates.js
+++ b/app/assets/javascripts/projects/default_project_templates.js
@@ -113,4 +113,12 @@ export default {
text: s__('ProjectTemplates|Jsonnet for Dynamic Child Pipelines'),
icon: '.template-option .icon-gitlab_logo',
},
+ bridgetown: {
+ text: s__('ProjectTemplates|Pages/Bridgetown'),
+ icon: '.template-option .icon-gitlab_logo',
+ },
+ typo3_distribution: {
+ text: s__('ProjectTemplates|TYPO3 Distribution'),
+ icon: '.template-option .icon-typo3',
+ },
};
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index 59ca393fe92..3100029eb31 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -3,7 +3,7 @@ import createFromTemplateIllustration from '@gitlab/svgs/dist/illustrations/proj
import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg';
import importProjectIllustration from '@gitlab/svgs/dist/illustrations/project-import-sm.svg';
import ciCdProjectIllustration from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg';
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index eccfb3d844c..d6d88b5b297 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -46,7 +46,15 @@ export default {
debounce: DEBOUNCE_DELAY,
},
},
- inject: ['namespaceFullPath', 'namespaceId', 'rootUrl', 'trackLabel', 'userNamespaceId'],
+ inject: [
+ 'namespaceFullPath',
+ 'namespaceId',
+ 'rootUrl',
+ 'trackLabel',
+ 'userNamespaceId',
+ 'inputName',
+ 'inputId',
+ ],
data() {
return {
currentUser: {},
@@ -124,6 +132,11 @@ export default {
}
: this.$options.emptyNameSpace;
},
+ trackDropdownShow() {
+ if (this.trackLabel) {
+ this.track('activate_form_input', { label: this.trackLabel, property: 'project_path' });
+ }
+ },
},
emptyNameSpace: {
id: undefined,
@@ -145,7 +158,7 @@ export default {
class="js-group-namespace-dropdown gl-flex-grow-1"
:toggle-class="`gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20 ${dropdownPlaceholderClass}`"
data-qa-selector="select_namespace_dropdown"
- @show="track('activate_form_input', { label: trackLabel, property: 'project_path' })"
+ @show="trackDropdownShow"
@shown="handleDropdownShown"
>
<template #button-text>
@@ -173,7 +186,7 @@ export default {
{{ group.fullPath }}
</gl-dropdown-item>
</template>
- <template v-if="hasNamespaceMatches">
+ <template v-if="hasNamespaceMatches && userNamespaceId">
<gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
<gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
{{ userNamespace.fullPath }}
@@ -186,9 +199,9 @@ export default {
<input type="hidden" name="project[selected_namespace_id]" :value="selectedNamespace.id" />
<input
- id="project_namespace_id"
+ :id="inputId"
type="hidden"
- name="project[namespace_id]"
+ :name="inputName"
:value="selectedNamespace.id || userNamespaceId"
/>
</gl-button-group>
diff --git a/app/assets/javascripts/projects/new/constants.js b/app/assets/javascripts/projects/new/constants.js
index e52a84dc07e..7b6b2cfc7ca 100644
--- a/app/assets/javascripts/projects/new/constants.js
+++ b/app/assets/javascripts/projects/new/constants.js
@@ -12,6 +12,8 @@ export const DEPLOYMENT_TARGET_SELECTIONS = [
s__('DeploymentTarget|Registry (package or container)'),
s__('DeploymentTarget|Infrastructure provider (Terraform, Cloudformation, and so on)'),
s__('DeploymentTarget|Serverless backend (Lambda, Cloud functions)'),
+ s__('DeploymentTarget|Edge Computing (e.g. Cloudflare Workers)'),
+ s__('DeploymentTarget|Web Deployment Platform (Netlify, Vercel, Gatsby)'),
s__('DeploymentTarget|GitLab Pages'),
s__('DeploymentTarget|Other hosting service'),
s__('DeploymentTarget|No deployment planned'),
diff --git a/app/assets/javascripts/projects/new/index.js b/app/assets/javascripts/projects/new/index.js
index a72172a4f5e..910244c657b 100644
--- a/app/assets/javascripts/projects/new/index.js
+++ b/app/assets/javascripts/projects/new/index.js
@@ -59,6 +59,8 @@ export function initNewProjectUrlSelect() {
rootUrl: el.dataset.rootUrl,
trackLabel: el.dataset.trackLabel,
userNamespaceId: el.dataset.userNamespaceId,
+ inputId: el.dataset.inputId,
+ inputName: el.dataset.inputName,
},
render: (createElement) => createElement(NewProjectUrlSelect),
}),
diff --git a/app/assets/javascripts/projects/project_name_rules.js b/app/assets/javascripts/projects/project_name_rules.js
new file mode 100644
index 00000000000..eeef1fb5afc
--- /dev/null
+++ b/app/assets/javascripts/projects/project_name_rules.js
@@ -0,0 +1,28 @@
+import { __ } from '~/locale';
+
+const rulesReg = [
+ {
+ reg: /^[a-zA-Z0-9\u{00A9}-\u{1f9ff}_]/u,
+ msg: __("Name must start with a letter, digit, emoji, or '_'"),
+ },
+ {
+ reg: /^[a-zA-Z0-9\p{Pd}\u{002B}\u{00A9}-\u{1f9ff}_. ]+$/u,
+ msg: __("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces"),
+ },
+];
+
+/**
+ *
+ * @param {string} text
+ * @returns {string} msg
+ */
+function checkRules(text) {
+ for (const item of rulesReg) {
+ if (!item.reg.test(text)) {
+ return item.msg;
+ }
+ }
+ return '';
+}
+
+export { checkRules };
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 424ea3b61c5..d71e80dffcf 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -12,6 +12,7 @@ import {
slugify,
convertUnicodeToAscii,
} from '../lib/utils/text_utility';
+import { checkRules } from './project_name_rules';
let hasUserDefinedProjectPath = false;
let hasUserDefinedProjectName = false;
@@ -87,10 +88,23 @@ const validateGroupNamespaceDropdown = (e) => {
}
};
+const checkProjectName = (projectNameInput) => {
+ const msg = checkRules(projectNameInput.value);
+ const projectNameError = document.querySelector('#project_name_error');
+ if (!projectNameError) return;
+ if (msg) {
+ projectNameError.innerText = msg;
+ projectNameError.classList.remove('hidden');
+ } else {
+ projectNameError.classList.add('hidden');
+ }
+};
+
const setProjectNamePathHandlers = ($projectNameInput, $projectPathInput) => {
const specialRepo = document.querySelector('.js-user-readme-repo');
const projectNameInputListener = () => {
onProjectNameChange($projectNameInput, $projectPathInput);
+ checkProjectName($projectNameInput);
hasUserDefinedProjectName = $projectNameInput.value.trim().length > 0;
hasUserDefinedProjectPath = $projectPathInput.value.trim().length > 0;
};
diff --git a/app/assets/javascripts/projects/settings/access_dropdown.js b/app/assets/javascripts/projects/settings/access_dropdown.js
index 335545c802a..dcf7415a444 100644
--- a/app/assets/javascripts/projects/settings/access_dropdown.js
+++ b/app/assets/javascripts/projects/settings/access_dropdown.js
@@ -580,7 +580,7 @@ export default class AccessDropdown {
return `
<li>
<a href="#" class="${isActiveClass} item-${role.type}" data-role-id="${role.id}">
- ${role.text}
+ ${escape(role.text)}
</a>
</li>
`;
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
index 6da058ebc9c..61c37a2348a 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/constants.js
@@ -6,6 +6,7 @@ export const I18N = {
branchNameOrPattern: s__('BranchRules|Branch name or pattern'),
branch: s__('BranchRules|Target Branch'),
allBranches: s__('BranchRules|All branches'),
+ matchingBranchesLinkTitle: s__('BranchRules|%{total} matching %{subject}'),
protectBranchTitle: s__('BranchRules|Protect branch'),
protectBranchDescription: s__(
'BranchRules|Keep stable branches secure and force developers to use merge requests. %{linkStart}What are protected branches?%{linkEnd}',
diff --git a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
index eb11e17dd1b..626ed67c466 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
+++ b/app/assets/javascripts/projects/settings/branch_rules/components/view/index.vue
@@ -1,9 +1,10 @@
<script>
import { GlSprintf, GlLink, GlLoadingIcon } from '@gitlab/ui';
-import { sprintf } from '~/locale';
-import { getParameterByName } from '~/lib/utils/url_utility';
+import { sprintf, n__ } from '~/locale';
+import { getParameterByName, mergeUrlParams } from '~/lib/utils/url_utility';
import { helpPagePath } from '~/helpers/help_page_helper';
import branchRulesQuery from '../../queries/branch_rules_details.query.graphql';
+import { getAccessLevels } from '../../../utils';
import Protection from './protection.vue';
import {
I18N,
@@ -41,6 +42,9 @@ export default {
statusChecksPath: {
default: '',
},
+ branchesPath: {
+ default: '',
+ },
},
apollo: {
project: {
@@ -55,6 +59,7 @@ export default {
this.branchProtection = branchRule?.branchProtection;
this.approvalRules = branchRule?.approvalRules;
this.statusChecks = branchRule?.externalStatusChecks?.nodes || [];
+ this.matchingBranchesCount = branchRule?.matchingBranchesCount;
},
},
},
@@ -64,6 +69,7 @@ export default {
branchProtection: {},
approvalRules: {},
statusChecks: [],
+ matchingBranchesCount: null,
};
},
computed: {
@@ -115,28 +121,20 @@ export default {
? this.$options.i18n.targetBranch
: this.$options.i18n.branchNameOrPattern;
},
+ matchingBranchesLinkHref() {
+ return mergeUrlParams({ state: 'all', search: this.branch }, this.branchesPath);
+ },
+ matchingBranchesLinkTitle() {
+ const total = this.matchingBranchesCount;
+ const subject = n__('branch', 'branches', total);
+ return sprintf(this.$options.i18n.matchingBranchesLinkTitle, { total, subject });
+ },
approvals() {
return this.approvalRules?.nodes || [];
},
},
methods: {
- getAccessLevels(accessLevels = {}) {
- const total = accessLevels.edges?.length;
- const accessLevelTypes = { total, users: [], groups: [], roles: [] };
-
- accessLevels.edges?.forEach(({ node }) => {
- if (node.user) {
- const src = node.user.avatarUrl;
- accessLevelTypes.users.push({ src, ...node.user });
- } else if (node.group) {
- accessLevelTypes.groups.push(node);
- } else {
- accessLevelTypes.roles.push(node);
- }
- });
-
- return accessLevelTypes;
- },
+ getAccessLevels,
},
};
</script>
@@ -161,6 +159,10 @@ export default {
</div>
<code v-else class="gl-mt-2" data-testid="branch">{{ branch }}</code>
+ <p v-if="matchingBranchesCount" class="gl-mt-3">
+ <gl-link :href="matchingBranchesLinkHref">{{ matchingBranchesLinkTitle }}</gl-link>
+ </p>
+
<h4 class="gl-mb-1 gl-mt-5">{{ $options.i18n.protectBranchTitle }}</h4>
<gl-sprintf :message="$options.i18n.protectBranchDescription">
<template #link="{ content }">
diff --git a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
index 89cfb1e1c8e..7639acc1181 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
+++ b/app/assets/javascripts/projects/settings/branch_rules/mount_branch_rules.js
@@ -14,7 +14,13 @@ export default function mountBranchRules(el) {
defaultClient: createDefaultClient(),
});
- const { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath } = el.dataset;
+ const {
+ projectPath,
+ protectedBranchesPath,
+ approvalRulesPath,
+ statusChecksPath,
+ branchesPath,
+ } = el.dataset;
return new Vue({
el,
@@ -24,6 +30,7 @@ export default function mountBranchRules(el) {
protectedBranchesPath,
approvalRulesPath,
statusChecksPath,
+ branchesPath,
},
render(h) {
return h(View);
diff --git a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
index aa1e4923aa8..a832e59aa67 100644
--- a/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
+++ b/app/assets/javascripts/projects/settings/branch_rules/queries/branch_rules_details.query.graphql
@@ -68,6 +68,7 @@ query getBranchRulesDetails($projectPath: ID!) {
externalUrl
}
}
+ matchingBranchesCount
}
}
}
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index a9eb2a53fbf..9b669024a8b 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,7 +1,7 @@
<script>
import { s__ } from '~/locale';
import { createAlert } from '~/flash';
-import branchRulesQuery from './graphql/queries/branch_rules.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import BranchRule from './components/branch_rule.vue';
export const i18n = {
@@ -51,13 +51,14 @@ export default {
<template>
<div class="settings-content">
<branch-rule
- v-for="rule in branchRules"
- :key="rule.name"
+ v-for="(rule, index) in branchRules"
+ :key="`${rule.name}-${index}`"
:name="rule.name"
:is-default="rule.isDefault"
:branch-protection="rule.branchProtection"
- :status-checks-total="rule.externalStatusChecks.nodes.length"
- :approval-rules-total="rule.approvalRules.nodes.length"
+ :status-checks-total="rule.externalStatusChecks ? rule.externalStatusChecks.nodes.length : 0"
+ :approval-rules-total="rule.approvalRules ? rule.approvalRules.nodes.length : 0"
+ :matching-branches-count="rule.matchingBranchesCount"
/>
<span v-if="!branchRules.length" data-testid="empty">{{ $options.i18n.emptyState }}</span>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 78c824c66d1..41947834bdb 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -1,6 +1,7 @@
<script>
import { GlBadge, GlButton } from '@gitlab/ui';
import { s__, sprintf, n__ } from '~/locale';
+import { getAccessLevels } from '../../../utils';
export const i18n = {
defaultLabel: s__('BranchRules|default'),
@@ -9,6 +10,9 @@ export const i18n = {
codeOwnerApprovalRequired: s__('BranchRules|Requires CODEOWNERS approval'),
statusChecks: s__('BranchRules|%{total} status %{subject}'),
approvalRules: s__('BranchRules|%{total} approval %{subject}'),
+ matchingBranches: s__('BranchRules|%{total} matching %{subject}'),
+ pushAccessLevels: s__('BranchRules|Allowed to merge'),
+ mergeAccessLevels: s__('BranchRules|Allowed to push'),
};
export default {
@@ -48,8 +52,16 @@ export default {
required: false,
default: 0,
},
+ matchingBranchesCount: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
},
computed: {
+ isWildcard() {
+ return this.name.includes('*');
+ },
hasApprovalDetails() {
return this.approvalDetails.length;
},
@@ -68,8 +80,31 @@ export default {
subject: n__('rule', 'rules', this.approvalRulesTotal),
});
},
+ matchingBranchesText() {
+ return sprintf(this.$options.i18n.matchingBranches, {
+ total: this.matchingBranchesCount,
+ subject: n__('branch', 'branches', this.matchingBranchesCount),
+ });
+ },
+ mergeAccessLevels() {
+ const { mergeAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(mergeAccessLevels);
+ },
+ pushAccessLevels() {
+ const { pushAccessLevels } = this.branchProtection || {};
+ return this.getAccessLevels(pushAccessLevels);
+ },
+ pushAccessLevelsText() {
+ return this.getAccessLevelsText(this.$options.i18n.pushAccessLevels, this.pushAccessLevels);
+ },
+ mergeAccessLevelsText() {
+ return this.getAccessLevelsText(this.$options.i18n.mergeAccessLevels, this.mergeAccessLevels);
+ },
approvalDetails() {
const approvalDetails = [];
+ if (this.isWildcard) {
+ approvalDetails.push(this.matchingBranchesText);
+ }
if (this.branchProtection.allowForcePush) {
approvalDetails.push(this.$options.i18n.allowForcePush);
}
@@ -82,9 +117,31 @@ export default {
if (this.approvalRulesTotal) {
approvalDetails.push(this.approvalRulesText);
}
+ if (this.mergeAccessLevels.total > 0) {
+ approvalDetails.push(this.mergeAccessLevelsText);
+ }
+ if (this.pushAccessLevels.total > 0) {
+ approvalDetails.push(this.pushAccessLevelsText);
+ }
return approvalDetails;
},
},
+ methods: {
+ getAccessLevels,
+ getAccessLevelsText(beginString = '', accessLevels) {
+ const textParts = [];
+ if (accessLevels.roles.length) {
+ textParts.push(n__('1 role', '%d roles', accessLevels.roles.length));
+ }
+ if (accessLevels.groups.length) {
+ textParts.push(n__('1 group', '%d groups', accessLevels.groups.length));
+ }
+ if (accessLevels.users.length) {
+ textParts.push(n__('1 user', '%d users', accessLevels.users.length));
+ }
+ return `${beginString}: ${textParts.join(', ')}`;
+ },
+ },
};
</script>
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
index 49e089e7805..a8cdda5505f 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql
@@ -5,18 +5,24 @@ query getBranchRules($projectPath: ID!) {
nodes {
name
isDefault
+ matchingBranchesCount
branchProtection {
allowForcePush
- codeOwnerApprovalRequired
- }
- externalStatusChecks {
- nodes {
- id
+ mergeAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ }
+ }
}
- }
- approvalRules {
- nodes {
- id
+ pushAccessLevels {
+ edges {
+ node {
+ accessLevel
+ accessLevelDescription
+ }
+ }
}
}
}
diff --git a/app/assets/javascripts/projects/settings/utils.js b/app/assets/javascripts/projects/settings/utils.js
new file mode 100644
index 00000000000..7bcfde39178
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/utils.js
@@ -0,0 +1,17 @@
+export const getAccessLevels = (accessLevels = {}) => {
+ const total = accessLevels.edges?.length;
+ const accessLevelTypes = { total, users: [], groups: [], roles: [] };
+
+ accessLevels.edges?.forEach(({ node }) => {
+ if (node.user) {
+ const src = node.user.avatarUrl;
+ accessLevelTypes.users.push({ src, ...node.user });
+ } else if (node.group) {
+ accessLevelTypes.groups.push(node);
+ } else {
+ accessLevelTypes.roles.push(node);
+ }
+ });
+
+ return accessLevelTypes;
+};
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 71ff3e892b1..b79b3fa4573 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
@@ -16,7 +17,7 @@ export default {
ServiceDeskSetting,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
inject: {
initialIsEnabled: {
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index e3f427b8408..75fd11cd074 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -42,7 +42,7 @@ export default class ProtectedTagCreate {
const $allowedToCreateInput = this.$form.find('#create_access_levels_attributes');
this.$form
- .find('input[type="submit"]')
+ .find('button[type="submit"]')
.prop('disabled', !($tagInput.val() && $allowedToCreateInput.length));
}
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 6dc8240e680..1b360b79b0c 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -263,6 +263,7 @@ export default {
v-for="(release, index) in releases"
:key="getReleaseKey(release, index)"
:release="release"
+ :sort="sort"
:class="{ 'linked-card': releases.length > 1 && index !== releases.length - 1 }"
/>
diff --git a/app/assets/javascripts/releases/components/release_block.vue b/app/assets/javascripts/releases/components/release_block.vue
index b2bd405574f..49c349e7a7b 100644
--- a/app/assets/javascripts/releases/components/release_block.vue
+++ b/app/assets/javascripts/releases/components/release_block.vue
@@ -1,12 +1,12 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
import { isEmpty } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { scrollToElement } from '~/lib/utils/common_utils';
import { slugify } from '~/lib/utils/text_utility';
import { getLocationHash } from '~/lib/utils/url_utility';
+import { CREATED_ASC } from '~/releases/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import '~/behaviors/markdown/render_gfm';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import EvidenceBlock from './evidence_block.vue';
import ReleaseBlockAssets from './release_block_assets.vue';
import ReleaseBlockFooter from './release_block_footer.vue';
@@ -32,6 +32,11 @@ export default {
required: true,
default: () => ({}),
},
+ sort: {
+ type: String,
+ required: false,
+ default: CREATED_ASC,
+ },
},
data() {
return {
@@ -80,7 +85,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
},
},
safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
@@ -119,6 +124,8 @@ export default {
:tag-path="release.tagPath"
:author="release.author"
:released-at="release.releasedAt"
+ :created-at="release.createdAt"
+ :sort="sort"
/>
</div>
</template>
diff --git a/app/assets/javascripts/releases/components/release_block_footer.vue b/app/assets/javascripts/releases/components/release_block_footer.vue
index 3881c83b5c2..85fb7d02a37 100644
--- a/app/assets/javascripts/releases/components/release_block_footer.vue
+++ b/app/assets/javascripts/releases/components/release_block_footer.vue
@@ -3,6 +3,7 @@ import { GlTooltipDirective, GlLink, GlIcon } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import { RELEASED_AT_ASC, RELEASED_AT_DESC } from '~/releases/constants';
export default {
name: 'ReleaseBlockFooter',
@@ -46,10 +47,26 @@ export default {
required: false,
default: null,
},
+ createdAt: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ sort: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
- releasedAtTimeAgo() {
- return this.timeFormatted(this.releasedAt);
+ isSortedByReleaseDate() {
+ return this.sort === RELEASED_AT_ASC || this.sort === RELEASED_AT_DESC;
+ },
+ timeAt() {
+ return this.isSortedByReleaseDate ? this.releasedAt : this.createdAt;
+ },
+ atTimeAgo() {
+ return this.timeFormatted(this.timeAt);
},
userImageAltDescription() {
return this.author && this.author.username
@@ -58,7 +75,10 @@ export default {
},
createdTime() {
const now = new Date();
- const isFuture = now < new Date(this.releasedAt);
+ const isFuture = now < new Date(this.timeAt);
+ if (this.isSortedByReleaseDate) {
+ return isFuture ? __('Will be released') : __('Released');
+ }
return isFuture ? __('Will be created') : __('Created');
},
},
@@ -93,17 +113,17 @@ export default {
</div>
<div
- v-if="releasedAt || author"
+ v-if="timeAt || author"
class="gl-float-left gl-display-flex gl-align-items-center js-author-date-info"
>
<span class="gl-text-secondary">{{ createdTime }}&nbsp;</span>
- <template v-if="releasedAt">
+ <template v-if="timeAt">
<span
v-gl-tooltip.bottom
- :title="tooltipTitle(releasedAt)"
+ :title="tooltipTitle(timeAt)"
class="gl-text-secondary gl-flex-shrink-0"
>
- {{ releasedAtTimeAgo }}&nbsp;
+ {{ atTimeAgo }}&nbsp;
</span>
</template>
diff --git a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
index 3ad66afa259..177dff1823e 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql
@@ -4,6 +4,7 @@ fragment ReleaseForEditing on Release {
tagName
description
releasedAt
+ createdAt
tagPath
assets {
links {
diff --git a/app/assets/javascripts/releases/util.js b/app/assets/javascripts/releases/util.js
index a1027ef08d7..10d7887c0b1 100644
--- a/app/assets/javascripts/releases/util.js
+++ b/app/assets/javascripts/releases/util.js
@@ -15,7 +15,8 @@ const convertScalarProperties = (graphQLRelease) =>
'historicalRelease',
]);
-const convertDateProperties = ({ releasedAt }) => ({
+const convertDateProperties = ({ createdAt, releasedAt }) => ({
+ createdAt: new Date(createdAt),
releasedAt: new Date(releasedAt),
});
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 05d64077866..4d3c1521559 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlTooltipDirective,
- GlLink,
- GlButton,
- GlButtonGroup,
- GlLoadingIcon,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlLink, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import defaultAvatarUrl from 'images/no_avatar.png';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
@@ -32,7 +26,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [getRefMixin],
apollo: {
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index 4935b8029f9..8feac6b8e35 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -1,8 +1,8 @@
<script>
-import { GlIcon, GlLink, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlIcon, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleLocationHash } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import readmeQuery from '../../queries/readme.query.graphql';
export default {
@@ -42,7 +42,7 @@ export default {
if (newVal) {
this.$nextTick(() => {
handleLocationHash();
- $(this.$refs.readme).renderGFM();
+ renderGFM(this.$refs.readme);
});
}
},
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 99eb167172b..46d546c2ee4 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -1,6 +1,5 @@
<script>
import { GlSkeletonLoader, GlButton } from '@gitlab/ui';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { sprintf, __ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import getRefMixin from '../../mixins/get_ref';
@@ -17,7 +16,7 @@ export default {
ParentRow,
GlButton,
},
- mixins: [getRefMixin, glFeatureFlagMixin()],
+ mixins: [getRefMixin],
apollo: {
projectPath: {
query: projectPathQuery,
@@ -93,9 +92,6 @@ export default {
},
generateRowNumber(path, id, index) {
const key = `${path}-${id}-${index}`;
- if (!this.glFeatures.lazyLoadCommits) {
- return 0;
- }
if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
this.$options.totalRowsLoaded += 1;
@@ -105,10 +101,6 @@ export default {
return this.rowNumbers[key];
},
getCommit(fileName) {
- if (!this.glFeatures.lazyLoadCommits) {
- return {};
- }
-
return this.commits.find(
(commitEntry) => commitEntry.filePath === joinPaths(this.path, fileName),
);
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index f3c5ace75fc..27ac11f3c58 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -7,10 +7,10 @@ import {
GlLoadingIcon,
GlIcon,
GlHoverLoadDirective,
- GlSafeHtmlDirective,
GlIntersectionObserver,
} from '@gitlab/ui';
import { escapeRegExp } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import paginatedTreeQuery from 'shared_queries/repository/paginated_tree.query.graphql';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import { TREE_PAGE_SIZE, ROW_APPEAR_DELAY } from '~/repository/constants';
@@ -19,7 +19,6 @@ import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import getRefMixin from '../../mixins/get_ref';
-import commitQuery from '../../queries/commit.query.graphql';
export default {
components: {
@@ -35,23 +34,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
GlHoverLoad: GlHoverLoadDirective,
- SafeHtml: GlSafeHtmlDirective,
- },
- apollo: {
- commit: {
- query: commitQuery,
- variables() {
- return {
- fileName: this.name,
- path: this.currentPath,
- projectPath: this.projectPath,
- maxOffset: this.totalEntries,
- };
- },
- skip() {
- return this.glFeatures.lazyLoadCommits;
- },
- },
+ SafeHtml,
},
mixins: [getRefMixin, glFeatureFlagMixin()],
props: {
@@ -125,14 +108,13 @@ export default {
},
data() {
return {
- commit: null,
hasRowAppeared: false,
delayedRowAppear: null,
};
},
computed: {
commitData() {
- return this.glFeatures.lazyLoadCommits ? this.commitInfo : this.commit;
+ return this.commitInfo;
},
routerLinkTo() {
const blobRouteConfig = { path: `/-/blob/${this.escapedRef}/${escapeFileUrl(this.path)}` };
@@ -200,12 +182,10 @@ export default {
return;
}
- if (this.glFeatures.lazyLoadCommits) {
- this.delayedRowAppear = setTimeout(
- () => this.$emit('row-appear', this.rowNumber),
- ROW_APPEAR_DELAY,
- );
- }
+ this.delayedRowAppear = setTimeout(
+ () => this.$emit('row-appear', this.rowNumber),
+ ROW_APPEAR_DELAY,
+ );
},
rowDisappeared() {
clearTimeout(this.delayedRowAppear);
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 8a45a351c35..4a8f83458f4 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -157,7 +157,7 @@ export default {
.find(({ hasNextPage }) => hasNextPage);
},
handleRowAppear(rowNumber) {
- if (!this.glFeatures.lazyLoadCommits || isRequested(rowNumber)) {
+ if (isRequested(rowNumber)) {
return;
}
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 3a6d7d2f779..e194bddcc56 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -99,5 +99,4 @@ export const LEGACY_FILE_TYPES = [
'requirements_txt',
'cargo_toml',
'go_mod',
- 'go_sum',
];
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 1d295e18332..e9214e3acff 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -2,11 +2,12 @@ import { GlButton } from '@gitlab/ui';
import Vue from 'vue';
import Vuex from 'vuex';
import { parseBoolean } from '~/lib/utils/common_utils';
-import { escapeFileUrl } from '~/lib/utils/url_utility';
+import { escapeFileUrl, visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initWebIdeLink from '~/pages/projects/shared/web_ide_link';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import createStore from '~/code_navigation/store';
+import RefSelector from '~/ref/components/ref_selector.vue';
import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
@@ -20,6 +21,7 @@ import refsQuery from './queries/ref.query.graphql';
import createRouter from './router';
import { updateFormAction } from './utils/dom';
import { setTitle } from './utils/title';
+import { generateRefDestinationPath } from './utils/ref_switcher_utils';
Vue.use(Vuex);
Vue.use(PerformancePlugin, {
@@ -89,9 +91,34 @@ export default function setupVueRepositoryList() {
},
});
- initLastCommitApp();
+ const initRefSwitcher = () => {
+ const refSwitcherEl = document.getElementById('js-tree-ref-switcher');
+
+ if (!refSwitcherEl) return false;
+
+ const { projectId, projectRootPath } = refSwitcherEl.dataset;
+
+ return new Vue({
+ el: refSwitcherEl,
+ render(createElement) {
+ return createElement(RefSelector, {
+ props: {
+ projectId,
+ value: ref,
+ },
+ on: {
+ input(selectedRef) {
+ visitUrl(generateRefDestinationPath(projectRootPath, selectedRef));
+ },
+ },
+ });
+ },
+ });
+ };
+ initLastCommitApp();
initBlobControlsApp();
+ initRefSwitcher();
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
diff --git a/app/assets/javascripts/repository/queries/commit.query.graphql b/app/assets/javascripts/repository/queries/commit.query.graphql
deleted file mode 100644
index 1a01462bd19..00000000000
--- a/app/assets/javascripts/repository/queries/commit.query.graphql
+++ /dev/null
@@ -1,7 +0,0 @@
-#import "ee_else_ce/repository/queries/commit.fragment.graphql"
-
-query getCommit($fileName: String!, $path: String!, $maxOffset: Number!) {
- commit(path: $path, fileName: $fileName, maxOffset: $maxOffset) @client {
- ...TreeEntryCommit
- }
-}
diff --git a/app/assets/javascripts/repository/utils/ref_switcher_utils.js b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
new file mode 100644
index 00000000000..8ff52104c93
--- /dev/null
+++ b/app/assets/javascripts/repository/utils/ref_switcher_utils.js
@@ -0,0 +1,30 @@
+import { joinPaths } from '~/lib/utils/url_utility';
+
+/**
+ * Matches the namespace and target directory/blob in a path
+ * Example: /root/Flight/-/blob/fix/main/test/spec/utils_spec.js
+ * Group 1: /-/blob
+ * Group 2: blob
+ * Group 3: main/test/spec/utils_spec.js
+ */
+const NAMESPACE_TARGET_REGEX = /(\/-\/(blob|tree))\/.*?\/(.*)/;
+
+/**
+ * Generates a ref destination path based on the selected ref and current path.
+ * A user could either be in the project root, a directory on the blob view.
+ * @param {string} projectRootPath - The root path for a project.
+ * @param {string} selectedRef - The selected ref from the ref dropdown.
+ */
+export function generateRefDestinationPath(projectRootPath, selectedRef) {
+ const currentPath = window.location.pathname;
+ let namespace = '/-/tree';
+ let target;
+ const match = NAMESPACE_TARGET_REGEX.exec(currentPath);
+ if (match) {
+ [, namespace, , target] = match;
+ }
+
+ const destinationPath = joinPaths(projectRootPath, namespace, selectedRef, target);
+
+ return `${destinationPath}${window.location.hash}`;
+}
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
index 38dccb9675d..4ddf695f61a 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { confidentialFilterData } from '../constants/confidential_filter_data';
import RadioFilter from './radio_filter.vue';
@@ -8,10 +8,10 @@ export default {
components: {
RadioFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['query']),
- showDropdown() {
- return Object.values(confidentialFilterData.scopes).includes(this.query.scope);
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
},
},
confidentialFilterData,
@@ -19,8 +19,8 @@ export default {
</script>
<template>
- <div v-if="showDropdown">
- <radio-filter :filter-data="$options.confidentialFilterData" />
+ <div>
+ <radio-filter :class="ffBasedXPadding" :filter-data="$options.confidentialFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/results_filters.vue b/app/assets/javascripts/search/sidebar/components/results_filters.vue
index 5b53f94bb53..9b993ab9a86 100644
--- a/app/assets/javascripts/search/sidebar/components/results_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/results_filters.vue
@@ -2,6 +2,8 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { confidentialFilterData } from '../constants/confidential_filter_data';
+import { stateFilterData } from '../constants/state_filter_data';
import ConfidentialityFilter from './confidentiality_filter.vue';
import StatusFilter from './status_filter.vue';
@@ -22,6 +24,15 @@ export default {
searchPageVerticalNavFeatureFlag() {
return this.glFeatures.searchPageVerticalNav;
},
+ showConfidentialityFilter() {
+ return Object.values(confidentialFilterData.scopes).includes(this.urlQuery.scope);
+ },
+ showStatusFilter() {
+ return Object.values(stateFilterData.scopes).includes(this.urlQuery.scope);
+ },
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
+ },
},
methods: {
...mapActions(['applyQuery', 'resetQuery']),
@@ -30,14 +41,14 @@ export default {
</script>
<template>
- <form
- :class="searchPageVerticalNavFeatureFlag ? 'gl-px-5' : 'gl-px-0'"
- @submit.prevent="applyQuery"
- >
- <hr v-if="searchPageVerticalNavFeatureFlag" class="gl-my-5 gl-border-gray-100" />
- <status-filter />
- <confidentiality-filter />
- <div class="gl-display-flex gl-align-items-center gl-mt-4">
+ <form class="gl-pt-5 gl-md-pt-0" @submit.prevent="applyQuery">
+ <hr
+ v-if="searchPageVerticalNavFeatureFlag"
+ class="gl-my-5 gl-border-gray-100 gl-display-none gl-md-display-block"
+ />
+ <status-filter v-if="showStatusFilter" />
+ <confidentiality-filter v-if="showConfidentialityFilter" />
+ <div class="gl-display-flex gl-align-items-center gl-mt-4" :class="ffBasedXPadding">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
</gl-button>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
index f5e1525090e..7a03306e2f9 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_navigation.vue
@@ -1,15 +1,23 @@
<script>
-import { GlNav, GlNavItem } from '@gitlab/ui';
+import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { formatNumber } from '~/locale';
+import { formatNumber, s__ } from '~/locale';
import Tracking from '~/tracking';
-import { NAV_LINK_DEFAULT_CLASSES, NUMBER_FORMATING_OPTIONS } from '../constants';
+import {
+ NAV_LINK_DEFAULT_CLASSES,
+ NUMBER_FORMATING_OPTIONS,
+ NAV_LINK_COUNT_DEFAULT_CLASSES,
+} from '../constants';
export default {
name: 'ScopeNavigation',
+ i18n: {
+ countOverLimitLabel: s__('GlobalSearch|Result count is over limit.'),
+ },
components: {
GlNav,
GlNavItem,
+ GlIcon,
},
mixins: [Tracking.mixin()],
computed: {
@@ -20,9 +28,6 @@ export default {
},
methods: {
...mapActions(['fetchSidebarCount']),
- activeClasses(currentScope) {
- return currentScope === this.urlQuery.scope ? 'gl-font-weight-bold' : '';
- },
showFormatedCount(count) {
if (!count) {
return '0';
@@ -30,17 +35,27 @@ export default {
const countNumber = parseInt(count.replace(/,/g, ''), 10);
return formatNumber(countNumber, NUMBER_FORMATING_OPTIONS);
},
+ isCountOverLimit(count) {
+ return count.includes('+');
+ },
handleClick(scope) {
this.track('click_menu_item', { label: `vertical_navigation_${scope}` });
},
- linkClasses(scope) {
+ linkClasses(isHighlighted) {
+ return [...this.$options.NAV_LINK_DEFAULT_CLASSES, { 'gl-font-weight-bold': isHighlighted }];
+ },
+ countClasses(isHighlighted) {
return [
- { 'gl-font-weight-bold': scope === this.urlQuery.scope },
- ...this.$options.NAV_LINK_DEFAULT_CLASSES,
+ ...this.$options.NAV_LINK_COUNT_DEFAULT_CLASSES,
+ isHighlighted ? 'gl-text-gray-900' : 'gl-text-gray-500',
];
},
+ isActive(scope, index) {
+ return this.urlQuery.scope ? this.urlQuery.scope === scope : index === 0;
+ },
},
NAV_LINK_DEFAULT_CLASSES,
+ NAV_LINK_COUNT_DEFAULT_CLASSES,
};
</script>
@@ -50,14 +65,20 @@ export default {
<gl-nav-item
v-for="(item, scope, index) in navigation"
:key="scope"
- :link-classes="linkClasses(scope)"
+ :link-classes="linkClasses(isActive(scope, index))"
class="gl-mb-1"
:href="item.link"
- :active="urlQuery.scope ? urlQuery.scope === scope : index === 0"
+ :active="isActive(scope, index)"
@click="handleClick(scope)"
><span>{{ item.label }}</span
- ><span v-if="item.count" class="gl-font-sm gl-font-weight-normal">
- {{ showFormatedCount(item.count) }}
+ ><span v-if="item.count" :class="countClasses(isActive(scope, index))">
+ {{ showFormatedCount(item.count)
+ }}<gl-icon
+ v-if="isCountOverLimit(item.count)"
+ name="plus"
+ :aria-label="$options.i18n.countOverLimitLabel"
+ :size="8"
+ />
</span>
</gl-nav-item>
</gl-nav>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter.vue b/app/assets/javascripts/search/sidebar/components/status_filter.vue
index 5cec2090906..eaf7d95822a 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState } from 'vuex';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { stateFilterData } from '../constants/state_filter_data';
import RadioFilter from './radio_filter.vue';
@@ -8,10 +8,10 @@ export default {
components: {
RadioFilter,
},
+ mixins: [glFeatureFlagsMixin()],
computed: {
- ...mapState(['query']),
- showDropdown() {
- return Object.values(stateFilterData.scopes).includes(this.query.scope);
+ ffBasedXPadding() {
+ return this.glFeatures.searchPageVerticalNav ? 'gl-px-5' : 'gl-px-0';
},
},
stateFilterData,
@@ -19,8 +19,8 @@ export default {
</script>
<template>
- <div v-if="showDropdown">
- <radio-filter :filter-data="$options.stateFilterData" />
+ <div>
+ <radio-filter :class="ffBasedXPadding" :filter-data="$options.stateFilterData" />
<hr class="gl-my-5 gl-border-gray-100" />
</div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 3621138afe4..a9c031f91a4 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -9,3 +9,5 @@ export const NAV_LINK_DEFAULT_CLASSES = [
'gl-justify-content-space-between',
'gl-text-gray-900',
];
+
+export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal'];
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index d0fcbb0d83b..0629bea3239 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -1,9 +1,11 @@
<script>
-import { GlSearchBoxByClick } from '@gitlab/ui';
+import { GlSearchBoxByClick, GlButton } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
+import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
+import { SYNTAX_OPTIONS_DOCUMENT } from '../constants';
import GroupFilter from './group_filter.vue';
import ProjectFilter from './project_filter.vue';
@@ -12,24 +14,45 @@ export default {
i18n: {
searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`),
searchLabel: s__(`GlobalSearch|What are you searching for?`),
+ documentFetchErrorMessage: s__(
+ 'GlobalSearch|There was an error fetching the "Syntax Options" document.',
+ ),
+ searchFieldLabel: s__('GlobalSearch|What are you searching for?'),
+ syntaxOptionsLabel: s__('GlobalSearch|Syntax options'),
+ groupFieldLabel: s__('GlobalSearch|Group'),
+ projectFieldLabel: s__('GlobalSearch|Project'),
+ searchButtonLabel: s__('GlobalSearch|Search'),
+ closeButtonLabel: s__('GlobalSearch|Close'),
},
components: {
+ GlButton,
GlSearchBoxByClick,
GroupFilter,
ProjectFilter,
+ MarkdownDrawer,
},
mixins: [glFeatureFlagsMixin()],
props: {
- groupInitialData: {
+ groupInitialJson: {
type: Object,
required: false,
default: () => ({}),
},
- projectInitialData: {
+ projectInitialJson: {
type: Object,
required: false,
default: () => ({}),
},
+ elasticsearchEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ defaultBranchName: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState(['query']),
@@ -44,16 +67,26 @@ export default {
showFilters() {
return !parseBoolean(this.query.snippets);
},
+ showSyntaxOptions() {
+ return this.elasticsearchEnabled && this.isDefaultBranch;
+ },
hasVerticalNav() {
return this.glFeatures.searchPageVerticalNav;
},
+ isDefaultBranch() {
+ return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
+ },
},
created() {
this.preloadStoredFrequentItems();
},
methods: {
...mapActions(['applyQuery', 'setQuery', 'preloadStoredFrequentItems']),
+ onToggleDrawer() {
+ this.$refs.markdownDrawer.toggleDrawer();
+ },
},
+ SYNTAX_OPTIONS_DOCUMENT,
};
</script>
@@ -61,7 +94,25 @@ export default {
<section class="search-page-form gl-lg-display-flex gl-flex-direction-column">
<div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end">
<div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
- <label>{{ $options.i18n.searchLabel }}</label>
+ <div
+ class="gl-sm-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mb-4 gl-md-mb-0"
+ >
+ <label>{{ $options.i18n.searchLabel }}</label>
+ <template v-if="showSyntaxOptions">
+ <gl-button
+ category="tertiary"
+ variant="link"
+ size="small"
+ button-text-classes="gl-font-sm!"
+ @click="onToggleDrawer"
+ >{{ $options.i18n.syntaxOptionsLabel }}
+ </gl-button>
+ <markdown-drawer
+ ref="markdownDrawer"
+ :document-path="$options.SYNTAX_OPTIONS_DOCUMENT"
+ />
+ </template>
+ </div>
<gl-search-box-by-click
id="dashboard_search"
v-model="search"
@@ -70,13 +121,13 @@ export default {
@submit="applyQuery"
/>
</div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Group') }}</label>
- <group-filter :initial-data="groupInitialData" />
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3">
+ <label class="gl-display-block">{{ $options.i18n.groupFieldLabel }}</label>
+ <group-filter :initial-data="groupInitialJson" />
</div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-2">
- <label class="gl-display-block">{{ __('Project') }}</label>
- <project-filter :initial-data="projectInitialData" />
+ <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3">
+ <label class="gl-display-block">{{ $options.i18n.projectFieldLabel }}</label>
+ <project-filter :initial-data="projectInitialJson" />
</div>
</div>
<hr v-if="hasVerticalNav" class="gl-mt-5 gl-mb-0 gl-border-gray-100" />
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
index 70156142365..c1e33df3c42 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
@@ -1,5 +1,6 @@
<script>
-import { GlDropdownItem, GlAvatar, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index dc040fdef34..121c15199dd 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -19,3 +19,5 @@ export const PROJECT_DATA = {
name: 'name',
fullName: 'name_with_namespace',
};
+
+export const SYNTAX_OPTIONS_DOCUMENT = 'drawers/user/search/advanced_search.md';
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index 87316e10e8d..d6e16085c28 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -11,10 +11,18 @@ export const initTopbar = (store) => {
return false;
}
- let { groupInitialData, projectInitialData } = el.dataset;
+ const {
+ groupInitialJson,
+ projectInitialJson,
+ elasticsearchEnabled,
+ defaultBranchName,
+ } = el.dataset;
- groupInitialData = JSON.parse(groupInitialData);
- projectInitialData = JSON.parse(projectInitialData);
+ const groupInitialJsonParsed = JSON.parse(groupInitialJson);
+ const projectInitialJsonParsed = JSON.parse(projectInitialJson);
+ const elasticsearchEnabledParsed = elasticsearchEnabled
+ ? JSON.parse(elasticsearchEnabled)
+ : false;
return new Vue({
el,
@@ -22,8 +30,10 @@ export const initTopbar = (store) => {
render(createElement) {
return createElement(GlobalSearchTopbar, {
props: {
- groupInitialData,
- projectInitialData,
+ groupInitialJson: groupInitialJsonParsed,
+ projectInitialJson: projectInitialJsonParsed,
+ elasticsearchEnabled: elasticsearchEnabledParsed,
+ defaultBranchName,
},
});
},
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 0bcb2bb6720..6dae8e50908 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -8,9 +8,9 @@ import {
GlLink,
GlSkeletonLoader,
GlIcon,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import { __, s__ } from '~/locale';
import {
@@ -54,7 +54,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [Tracking.mixin()],
inject: ['projectFullPath'],
diff --git a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
index ffba3aac681..d9e969e2278 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -1,15 +1,8 @@
<script>
-import {
- GlFormGroup,
- GlButton,
- GlModal,
- GlToast,
- GlToggle,
- GlLink,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlFormGroup, GlButton, GlModal, GlToast, GlToggle, GlLink } from '@gitlab/ui';
import Vue from 'vue';
import { mapState, mapActions } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { visitUrl, getBaseURL } from '~/lib/utils/url_utility';
@@ -26,7 +19,7 @@ export default {
GlLink,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
formLabels: {
createProject: __('Self-monitoring'),
diff --git a/app/assets/javascripts/self_monitor/store/actions.js b/app/assets/javascripts/self_monitor/store/actions.js
index 5b9e994290c..7198dbe8b04 100644
--- a/app/assets/javascripts/self_monitor/store/actions.js
+++ b/app/assets/javascripts/self_monitor/store/actions.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import { __, s__ } from '~/locale';
import * as types from './mutation_types';
@@ -10,7 +10,7 @@ function backOffRequest(makeRequestCallback) {
return backOff((next, stop) => {
makeRequestCallback()
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
next();
} else {
stop(resp);
@@ -31,7 +31,7 @@ export const requestCreateProject = ({ dispatch, state, commit }) => {
axios
.post(state.createProjectEndpoint)
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestCreateProjectStatus', resp.data.job_id);
}
})
@@ -83,7 +83,7 @@ export const requestDeleteProject = ({ dispatch, state, commit }) => {
axios
.delete(state.deleteProjectEndpoint)
.then((resp) => {
- if (resp.status === statusCodes.ACCEPTED) {
+ if (resp.status === HTTP_STATUS_ACCEPTED) {
dispatch('requestDeleteProjectStatus', resp.data.job_id);
}
})
diff --git a/app/assets/javascripts/sentry/constants.js b/app/assets/javascripts/sentry/constants.js
index fd96da5faf6..5531c4f56db 100644
--- a/app/assets/javascripts/sentry/constants.js
+++ b/app/assets/javascripts/sentry/constants.js
@@ -1,5 +1,6 @@
import { __ } from '~/locale';
+// TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
export const IGNORE_ERRORS = [
// Random plugins/extensions
'top.GLOBALS',
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index 176745b4177..5539a061726 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -1,26 +1,34 @@
import '../webpack';
+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,
- whitelistUrls:
+ allowUrls:
process.env.NODE_ENV === 'production'
? [gon.gitlab_url]
: [gon.gitlab_url, 'webpack-internal://'],
- environment: gon.sentry_environment,
release: gon.revision,
tags: {
revision: gon.revision,
feature_category: gon.feature_category,
},
});
-
- return SentryConfig;
};
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;
diff --git a/app/assets/javascripts/sentry/legacy_index.js b/app/assets/javascripts/sentry/legacy_index.js
new file mode 100644
index 00000000000..604b982e128
--- /dev/null
+++ b/app/assets/javascripts/sentry/legacy_index.js
@@ -0,0 +1,34 @@
+import '../webpack';
+
+import * as Sentry5 from 'sentrybrowser5';
+import LegacySentryConfig from './legacy_sentry_config';
+
+const index = function index() {
+ // Configuration for legacy versions of Sentry SDK (v5)
+ LegacySentryConfig.init({
+ dsn: gon.sentry_dsn,
+ currentUserId: gon.current_user_id,
+ whitelistUrls:
+ process.env.NODE_ENV === 'production'
+ ? [gon.gitlab_url]
+ : [gon.gitlab_url, 'webpack-internal://'],
+ environment: gon.sentry_environment,
+ release: gon.revision,
+ tags: {
+ revision: gon.revision,
+ feature_category: gon.feature_category,
+ },
+ });
+};
+
+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 = Sentry5;
+
+export default index;
diff --git a/app/assets/javascripts/sentry/legacy_sentry_config.js b/app/assets/javascripts/sentry/legacy_sentry_config.js
new file mode 100644
index 00000000000..50a943886db
--- /dev/null
+++ b/app/assets/javascripts/sentry/legacy_sentry_config.js
@@ -0,0 +1,64 @@
+import * as Sentry5 from 'sentrybrowser5';
+import $ from 'jquery';
+import { __ } from '~/locale';
+import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
+
+const SentryConfig = {
+ IGNORE_ERRORS,
+ BLACKLIST_URLS: DENY_URLS,
+ SAMPLE_RATE,
+ init(options = {}) {
+ this.options = options;
+
+ this.configure();
+ this.bindSentryErrors();
+ if (this.options.currentUserId) this.setUser();
+ },
+
+ configure() {
+ const { dsn, release, tags, whitelistUrls, environment } = this.options;
+
+ Sentry5.init({
+ dsn,
+ release,
+ whitelistUrls,
+ environment,
+ ignoreErrors: this.IGNORE_ERRORS, // TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
+ blacklistUrls: this.BLACKLIST_URLS,
+ sampleRate: SAMPLE_RATE,
+ });
+
+ Sentry5.setTags(tags);
+ },
+
+ setUser() {
+ Sentry5.setUser({
+ id: this.options.currentUserId,
+ });
+ },
+
+ bindSentryErrors() {
+ $(document).on('ajaxError.sentry', this.handleSentryErrors);
+ },
+
+ handleSentryErrors(event, req, config, err) {
+ const error = err || req.statusText;
+ const { responseText = __('Unknown response text') } = req;
+ const { type, url, data } = config;
+ const { status } = req;
+
+ Sentry5.captureMessage(error, {
+ extra: {
+ type,
+ url,
+ data,
+ status,
+ response: responseText,
+ error,
+ event,
+ },
+ });
+ },
+};
+
+export default SentryConfig;
diff --git a/app/assets/javascripts/sentry/sentry_browser_wrapper.js b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
new file mode 100644
index 00000000000..0382827f82c
--- /dev/null
+++ b/app/assets/javascripts/sentry/sentry_browser_wrapper.js
@@ -0,0 +1,27 @@
+// The _Sentry object is globally exported so it can be used here
+// This hack allows us to load a single version of `@sentry/browser`
+// in the browser (or none). See app/views/layouts/_head.html.haml
+// to find how it is imported.
+
+// This module wraps methods used by our production code.
+// Each export is names as we cannot export the entire namespace from *.
+export const captureException = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.captureException(...args);
+};
+
+export const captureMessage = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.captureMessage(...args);
+};
+
+export const withScope = (...args) => {
+ // eslint-disable-next-line no-underscore-dangle
+ const Sentry = window._Sentry;
+
+ Sentry?.withScope(...args);
+};
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index 4c5b8dbad5a..ed8a55b7d44 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -1,30 +1,24 @@
-import * as Sentry from '@sentry/browser';
-import $ from 'jquery';
-import { __ } from '~/locale';
+import * as Sentry from 'sentrybrowser7';
import { IGNORE_ERRORS, DENY_URLS, SAMPLE_RATE } from './constants';
const SentryConfig = {
- IGNORE_ERRORS,
- BLACKLIST_URLS: DENY_URLS,
- SAMPLE_RATE,
init(options = {}) {
this.options = options;
this.configure();
- this.bindSentryErrors();
if (this.options.currentUserId) this.setUser();
},
configure() {
- const { dsn, release, tags, whitelistUrls, environment } = this.options;
+ const { dsn, release, tags, allowUrls, environment } = this.options;
Sentry.init({
dsn,
release,
- whitelistUrls,
+ allowUrls,
environment,
- ignoreErrors: this.IGNORE_ERRORS, // TODO: Remove in favor of https://gitlab.com/gitlab-org/gitlab/issues/35144
- blacklistUrls: this.BLACKLIST_URLS,
+ ignoreErrors: IGNORE_ERRORS,
+ denyUrls: DENY_URLS,
sampleRate: SAMPLE_RATE,
});
@@ -36,29 +30,6 @@ const SentryConfig = {
id: this.options.currentUserId,
});
},
-
- bindSentryErrors() {
- $(document).on('ajaxError.sentry', this.handleSentryErrors);
- },
-
- handleSentryErrors(event, req, config, err) {
- const error = err || req.statusText;
- const { responseText = __('Unknown response text') } = req;
- const { type, url, data } = config;
- const { status } = req;
-
- Sentry.captureMessage(error, {
- extra: {
- type,
- url,
- data,
- status,
- response: responseText,
- error,
- event,
- },
- });
- },
};
export default SentryConfig;
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 86049a2b781..dd27a12cbee 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -10,9 +10,9 @@ import {
GlDropdownItem,
GlSprintf,
GlFormGroup,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import $ from 'jquery';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { s__ } from '~/locale';
@@ -33,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
defaultEmoji: {
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index 80158c55dbc..5becc03646e 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -1,5 +1,5 @@
<script>
-import { GlToast, GlTooltipDirective, GlSafeHtmlDirective, GlModal } from '@gitlab/ui';
+import { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
@@ -19,7 +19,6 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -110,7 +109,6 @@ export default {
this.availability = value;
},
},
- safeHtmlConfig: { ADD_TAGS: ['gl-emoji'] },
actionPrimary: { text: s__('SetStatusModal|Set status') },
actionSecondary: { text: s__('SetStatusModal|Remove status') },
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
index 78d12ac113b..93fcf2cf1c9 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees_realtime.vue
@@ -1,7 +1,7 @@
<script>
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
-import { assigneesQueries } from '~/sidebar/constants';
+import { assigneesQueries } from '../../constants';
export default {
subscription: null,
diff --git a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
index 4408ebb881b..fd51cd5bb16 100644
--- a/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/issuable_assignees.vue
@@ -1,7 +1,7 @@
<script>
import { GlButton } from '@gitlab/ui';
import { n__ } from '~/locale';
-import UncollapsedAssigneeList from '~/sidebar/components/assignees/uncollapsed_assignee_list.vue';
+import UncollapsedAssigneeList from './uncollapsed_assignee_list.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 15fd365b4da..7979f450fdd 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -2,9 +2,9 @@
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import eventHub from '~/sidebar/event_hub';
-import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import eventHub from '../../event_hub';
+import Store from '../../stores/sidebar_store';
import AssigneeTitle from './assignee_title.vue';
import Assignees from './assignees.vue';
import AssigneesRealtime from './assignees_realtime.vue';
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 395dcf73693..d6c679f2f07 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -4,12 +4,12 @@ import Vue from 'vue';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, n__ } from '~/locale';
-import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
-import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { assigneesQueries } from '~/sidebar/constants';
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 SidebarEditableItem from '../sidebar_editable_item.vue';
+import SidebarAssigneesRealtime from './assignees_realtime.vue';
+import IssuableAssignees from './issuable_assignees.vue';
import SidebarInviteMembers from './sidebar_invite_members.vue';
export const assigneesWidget = Vue.observable({
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 3532b75b6e7..dbedfe57325 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_form.vue
@@ -3,7 +3,7 @@ import { GlSprintf, GlButton } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { __, sprintf } from '~/locale';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries } from '../../constants';
export default {
i18n: {
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 f3bd58c11d4..c2f239b56c7 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -3,8 +3,8 @@ import produce from 'immer';
import Vue from 'vue';
import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { confidentialityQueries, Tracking } from '~/sidebar/constants';
+import { confidentialityQueries, Tracking } from '../../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_email_to_clipboard.vue b/app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue
index fd652583f76..96ecdc84ef5 100644
--- a/app/assets/javascripts/sidebar/components/copy_email_to_clipboard.vue
+++ b/app/assets/javascripts/sidebar/components/copy/copy_email_to_clipboard.vue
@@ -1,5 +1,5 @@
<script>
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import CopyableField from './copyable_field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/sidebar/components/copy/copyable_field.vue
index 6538de085b0..6538de085b0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
+++ b/app/assets/javascripts/sidebar/components/copy/copyable_field.vue
diff --git a/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
index d07c6e0cbd2..3287539e502 100644
--- a/app/assets/javascripts/sidebar/components/reference/sidebar_reference_widget.vue
+++ b/app/assets/javascripts/sidebar/components/copy/sidebar_reference_widget.vue
@@ -1,7 +1,7 @@
<script>
import { __ } from '~/locale';
-import { referenceQueries } from '~/sidebar/constants';
-import CopyableField from '~/vue_shared/components/sidebar/copyable_field.vue';
+import { referenceQueries } from '../../constants';
+import CopyableField from './copyable_field.vue';
export default {
components: {
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 81090bfa062..0660e4f58e4 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -4,8 +4,8 @@ import { __, n__, sprintf } from '~/locale';
import { createAlert } from '~/flash';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE } from '~/graphql_shared/constants';
-import getIssueCrmContactsQuery from './queries/get_issue_crm_contacts.query.graphql';
-import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscription.graphql';
+import getIssueCrmContactsQuery from '../../queries/get_issue_crm_contacts.query.graphql';
+import issueCrmContactsSubscription from '../../queries/issue_crm_contacts.subscription.graphql';
export default {
components: {
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 c262d65f6ce..eb48732f558 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -4,14 +4,8 @@ import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { dateInWords, formatDate, parsePikadayDate } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import {
- dateFields,
- dateTypes,
- dueDateQueries,
- startDateQueries,
- Tracking,
-} from '~/sidebar/constants';
+import { dateFields, dateTypes, dueDateQueries, startDateQueries, Tracking } from '../../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/constants.js b/app/assets/javascripts/sidebar/components/incidents/constants.js
deleted file mode 100644
index cd05a6099fd..00000000000
--- a/app/assets/javascripts/sidebar/components/incidents/constants.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { s__ } from '~/locale';
-
-export const STATUS_TRIGGERED = 'TRIGGERED';
-export const STATUS_ACKNOWLEDGED = 'ACKNOWLEDGED';
-export const STATUS_RESOLVED = 'RESOLVED';
-
-export const STATUS_TRIGGERED_LABEL = s__('IncidentManagement|Triggered');
-export const STATUS_ACKNOWLEDGED_LABEL = s__('IncidentManagement|Acknowledged');
-export const STATUS_RESOLVED_LABEL = s__('IncidentManagement|Resolved');
-
-export const STATUS_LABELS = {
- [STATUS_TRIGGERED]: STATUS_TRIGGERED_LABEL,
- [STATUS_ACKNOWLEDGED]: STATUS_ACKNOWLEDGED_LABEL,
- [STATUS_RESOLVED]: STATUS_RESOLVED_LABEL,
-};
-
-export const i18n = {
- fetchError: s__(
- 'IncidentManagement|An error occurred while fetching the incident status. Please reload the page.',
- ),
- title: s__('IncidentManagement|Status'),
- updateError: s__(
- 'IncidentManagement|An error occurred while updating the incident status. Please reload the page and try again.',
- ),
-};
diff --git a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
index 9c41db98c63..72a572087c7 100644
--- a/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/escalation_status.vue
@@ -1,7 +1,12 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { i18n, STATUS_ACKNOWLEDGED, STATUS_TRIGGERED, STATUS_RESOLVED } from './constants';
-import { getStatusLabel } from './utils';
+import {
+ INCIDENTS_I18N as i18n,
+ STATUS_ACKNOWLEDGED,
+ STATUS_TRIGGERED,
+ STATUS_RESOLVED,
+} from '../../constants';
+import { getStatusLabel } from '../../utils';
const STATUS_LIST = [STATUS_TRIGGERED, STATUS_ACKNOWLEDGED, STATUS_RESOLVED];
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 67ae1e6fcab..f7daad63f45 100644
--- a/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
+++ b/app/assets/javascripts/sidebar/components/incidents/sidebar_escalation_status.vue
@@ -1,12 +1,15 @@
<script>
import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { escalationStatusQuery, escalationStatusMutation } from '~/sidebar/constants';
import { createAlert } from '~/flash';
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 { getStatusLabel } from '../../utils';
import SidebarEditableItem from '../sidebar_editable_item.vue';
-import { i18n } from './constants';
-import { getStatusLabel } from './utils';
export default {
i18n,
diff --git a/app/assets/javascripts/sidebar/components/incidents/utils.js b/app/assets/javascripts/sidebar/components/incidents/utils.js
deleted file mode 100644
index 59bf1ea466c..00000000000
--- a/app/assets/javascripts/sidebar/components/incidents/utils.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import { s__ } from '~/locale';
-
-import { STATUS_LABELS } from './constants';
-
-export const getStatusLabel = (status) => STATUS_LABELS[status] ?? s__('IncidentManagement|None');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js
index 00c54313292..00c54313292 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/constants.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/constants.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
index 9388ef4ba45..864d9b308e7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_button.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_button.vue
@@ -4,7 +4,7 @@ import { mapActions, mapGetters } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
index 1064cbc26e3..89a976d45fa 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents.vue
@@ -6,7 +6,7 @@ import DropdownContentsLabelsView from './dropdown_contents_labels_view.vue';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue` instead.
export default {
components: {
DropdownContentsLabelsView,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
index 3ff3755de46..b8afa67a947 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_create_view.vue
@@ -4,7 +4,7 @@ import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
index e235bfde394..ee6b531c1ca 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_contents_labels_view.vue
@@ -15,7 +15,7 @@ import LabelItem from './label_item.vue';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue` instead.
export default {
components: {
GlIntersectionObserver,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
index e4325492334..1e9edd222c5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
@@ -4,7 +4,7 @@ import { mapState, mapActions } from 'vuex';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue` instead.
export default {
components: {
GlButton,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
index e59d150dd43..583f060be8a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value.vue
@@ -7,7 +7,7 @@ import { isScopedLabel } from '~/lib/utils/common_utils';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue` instead.
export default {
components: {
GlLabel,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue
index 5966c78aa51..e84da6ee12b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value_collapsed.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_value_collapsed.vue
@@ -4,7 +4,7 @@ import { s__, sprintf } from '~/locale';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget` instead.
export default {
directives: {
GlTooltip: GlTooltipDirective,
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
index 154e3013acd..135fa9f6228 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/label_item.vue
@@ -4,7 +4,7 @@ import { __ } from '~/locale';
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue` instead.
export default {
functional: true,
props: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
index e6c29e24f0c..2a78db352d7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/labels_select_root.vue
@@ -17,7 +17,7 @@ Vue.use(Vuex);
// @deprecated This component should only be used when there is no GraphQL API.
// In most cases you should use
-// `app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue` instead.
+// `app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue` instead.
export default {
store: new Vuex.Store(labelsSelectModule()),
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js
index 2dab97826b9..2dab97826b9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/actions.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
index ef3eedd9bb2..ef3eedd9bb2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/getters.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/getters.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js
index 5f61cb732c8..5f61cb732c8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/index.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/index.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js
index f26e36031f4..f26e36031f4 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutation_types.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutation_types.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js
index c85d9befcbb..c85d9befcbb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/mutations.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js
index 0185d5f88e1..0185d5f88e1 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/state.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/store/state.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js
index cd671b4d8f5..cd671b4d8f5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/constants.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/constants.js
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
index 27186281c42..83df9056af2 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents.vue
@@ -110,6 +110,9 @@ export default {
isStandalone() {
return isDropdownVariantStandalone(this.variant);
},
+ isSidebar() {
+ return isDropdownVariantSidebar(this.variant);
+ },
},
watch: {
localSelectedLabels: {
@@ -129,7 +132,7 @@ export default {
}
},
selectedLabels(newVal) {
- if (!this.isDirty) {
+ if (!this.isDirty || !this.isSidebar) {
this.localSelectedLabels = newVal;
}
},
@@ -159,7 +162,7 @@ export default {
},
handleDropdownHide() {
this.$emit('closeDropdown');
- if (!isDropdownVariantSidebar(this.variant)) {
+ if (!this.isSidebar) {
this.setLabels();
}
},
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index ce93ad216ec..aa1184ed314 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -10,7 +10,7 @@ import {
import produce from 'immer';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '../../../constants';
import createLabelMutation from './graphql/create_label.mutation.graphql';
import { LabelType } from './constants';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_labels_view.vue
index 1d854505d11..c1939dc7785 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/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 '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
-import { workspaceLabelsQueries } from '~/sidebar/constants';
+import { workspaceLabelsQueries } from '../../../constants';
import LabelItem from './label_item.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
index e67e704ffb8..e67e704ffb8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_footer.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_footer.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
index 154a8e866d0..154a8e866d0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_header.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
index 57e3ee4aaa5..57e3ee4aaa5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
new file mode 100644
index 00000000000..3a93fc7f3b2
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
@@ -0,0 +1,73 @@
+<script>
+import { GlLabel } from '@gitlab/ui';
+import { sortBy } from 'lodash';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+export default {
+ components: {
+ GlLabel,
+ },
+ inject: ['allowScopedLabels'],
+ props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowLabelRemove: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ labelsFilterParam: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ sortedSelectedLabels() {
+ return sortBy(this.selectedLabels, (label) => isScopedLabel(label));
+ },
+ },
+ methods: {
+ buildFilterUrl({ title }) {
+ const { labelsFilterBasePath: basePath, labelsFilterParam: filterParam } = this;
+
+ return `${basePath}?${filterParam}[]=${encodeURIComponent(title)}`;
+ },
+ showScopedLabel(label) {
+ return this.allowScopedLabels && isScopedLabel(label);
+ },
+ removeLabel(labelId) {
+ this.$emit('onLabelRemove', labelId);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-label
+ v-for="label in sortedSelectedLabels"
+ :key="label.id"
+ class="gl-mr-2 gl-mb-2"
+ :data-qa-label-name="label.title"
+ :title="label.title"
+ :description="label.description"
+ :background-color="label.color"
+ :target="buildFilterUrl(label)"
+ :scoped="showScopedLabel(label)"
+ :show-close-button="allowLabelRemove"
+ :disabled="disabled"
+ tooltip-placement="top"
+ @close="removeLabel(label.id)"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql
index a9c791091fc..a9c791091fc 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/create_label.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/create_label.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql
index c442c17eb88..c442c17eb88 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql
index cb054e2968f..cb054e2968f 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql
index ce1a69f84c0..ce1a69f84c0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/group_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql
index 2904857270e..2904857270e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/issue_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
index e0cdfd91658..e0cdfd91658 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql
index a7c24620aad..a7c24620aad 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
index 314ffbaf84c..314ffbaf84c 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/label_item.vue
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index 2c27a69d587..b7b4bbac661 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -7,11 +7,12 @@ import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { IssuableType } from '~/issues/constants';
import { __ } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { issuableLabelsQueries } from '~/sidebar/constants';
+import { issuableLabelsQueries } from '../../../constants';
+import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, DropdownVariant } from './constants';
import DropdownContents from './dropdown_contents.vue';
import DropdownValue from './dropdown_value.vue';
+import EmbeddedLabelsList from './embedded_labels_list.vue';
import {
isDropdownVariantSidebar,
isDropdownVariantStandalone,
@@ -22,6 +23,7 @@ export default {
components: {
DropdownValue,
DropdownContents,
+ EmbeddedLabelsList,
SidebarEditableItem,
},
mixins: [glFeatureFlagsMixin()],
@@ -50,6 +52,11 @@ export default {
required: false,
default: false,
},
+ showEmbeddedLabelsList: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
variant: {
type: String,
required: false,
@@ -106,6 +113,11 @@ export default {
type: String,
required: true,
},
+ selectedLabels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
@@ -124,11 +136,21 @@ export default {
return this.issuableLabels.map((label) => label.id);
},
issuableLabels() {
- return this.issuable?.labels.nodes || [];
+ if (this.iid !== '') {
+ return this.issuable?.labels.nodes || [];
+ }
+
+ return this.selectedLabels || [];
},
issuableId() {
return this.issuable?.id;
},
+ isRealtimeEnabled() {
+ return this.glFeatures.realtimeLabels;
+ },
+ isLabelListEnabled() {
+ return this.showEmbeddedLabelsList && isDropdownVariantEmbedded(this.variant);
+ },
},
apollo: {
issuable: {
@@ -311,7 +333,10 @@ export default {
}
},
handleLabelRemove(labelId) {
- this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ if (this.iid !== '') {
+ this.updateSelectedLabels(this.getRemoveVariables(labelId));
+ }
+
this.$emit('onLabelRemove', labelId);
},
isDropdownVariantSidebar,
@@ -385,22 +410,32 @@ export default {
</template>
</sidebar-editable-item>
</template>
- <dropdown-contents
- v-else
- ref="dropdownContents"
- :allow-multiselect="allowMultiselect"
- :dropdown-button-text="dropdownButtonText"
- :labels-list-title="labelsListTitle"
- :footer-create-label-title="footerCreateLabelTitle"
- :footer-manage-label-title="footerManageLabelTitle"
- :labels-create-title="labelsCreateTitle"
- :selected-labels="issuableLabels"
- :variant="variant"
- :full-path="fullPath"
- :workspace-type="workspaceType"
- :attr-workspace-path="attrWorkspacePath"
- :label-create-type="labelCreateType"
- @setLabels="handleDropdownClose"
- />
+ <template v-else>
+ <dropdown-contents
+ ref="dropdownContents"
+ :allow-multiselect="allowMultiselect"
+ :dropdown-button-text="dropdownButtonText"
+ :labels-list-title="labelsListTitle"
+ :footer-create-label-title="footerCreateLabelTitle"
+ :footer-manage-label-title="footerManageLabelTitle"
+ :labels-create-title="labelsCreateTitle"
+ :selected-labels="issuableLabels"
+ :variant="variant"
+ :full-path="fullPath"
+ :workspace-type="workspaceType"
+ :attr-workspace-path="attrWorkspacePath"
+ :label-create-type="labelCreateType"
+ @setLabels="handleDropdownClose"
+ />
+ <embedded-labels-list
+ v-if="isLabelListEnabled"
+ :disabled="labelsSelectInProgress"
+ :selected-labels="issuableLabels"
+ :allow-label-remove="allowLabelRemove"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
+ @onLabelRemove="handleLabelRemove"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js
index b5cd946a189..b5cd946a189 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/utils.js
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/utils.js
diff --git a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
index d32d8a7b044..cdce6617591 100644
--- a/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
+++ b/app/assets/javascripts/sidebar/components/lock/issuable_lock_form.vue
@@ -4,8 +4,8 @@ import { mapGetters, mapActions } from 'vuex';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { createAlert } from '~/flash';
-import eventHub from '~/sidebar/event_hub';
import toast from '~/vue_shared/plugins/global_toast';
+import eventHub from '../../event_hub';
import EditForm from './edit_form.vue';
export default {
@@ -111,9 +111,9 @@ export default {
</script>
<template>
- <li v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <li v-if="isMergeRequest" class="gl-dropdown-item">
<button type="button" class="dropdown-item" @click="toggleLocked">
- <span class="gl-new-dropdown-item-text-wrapper">
+ <span class="gl-dropdown-item-text-wrapper">
<template v-if="isLocked">
{{ __('Unlock merge request') }}
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
index 02323e5a0c6..02323e5a0c6 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/move/issuable_move_dropdown.vue
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue b/app/assets/javascripts/sidebar/components/move/move_issues_button.vue
index 6e287ac3bb7..ab4ac9500ad 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/move_issues_button.vue
+++ b/app/assets/javascripts/sidebar/components/move/move_issues_button.vue
@@ -1,7 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import IssuableMoveDropdown from '~/vue_shared/components/sidebar/issuable_move_dropdown.vue';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { logError } from '~/lib/logger';
import { s__ } from '~/locale';
import {
@@ -13,7 +12,8 @@ import {
import issuableEventHub from '~/issues/list/eventhub';
import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
import getIssuesCountQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
-import moveIssueMutation from './graphql/mutations/move_issue.mutation.graphql';
+import moveIssueMutation from '../../queries/move_issue.mutation.graphql';
+import IssuableMoveDropdown from './issuable_move_dropdown.vue';
export default {
name: 'MoveIssuesButton',
@@ -130,7 +130,7 @@ export default {
this.moveInProgress = false;
issuableEventHub.$emit('issuables:bulkMoveEnded');
- createFlash({
+ createAlert({
message: s__(`Issues|There was an error while moving the issues.`),
});
});
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 46a04725a49..b0556e22a8d 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 '~/sidebar/constants';
+import { participantsQueries } from '../../constants';
import Participants from './participants.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
index 5e1172ad835..7af8dcb4e3e 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewers.vue
@@ -58,11 +58,21 @@ export default {
<collapsed-reviewer-list :users="sortedReviewers" :issuable-type="issuableType" />
<div class="value hide-collapsed">
- <template v-if="hasNoUsers">
- <span class="no-value">
- {{ __('None') }}
- </span>
- </template>
+ <span v-if="hasNoUsers" class="no-value" data-testid="no-value">
+ {{ __('None') }}
+ <template v-if="editable">
+ -
+ <button
+ type="button"
+ class="gl-button btn-link gl-reset-color!"
+ data-testid="assign-yourself"
+ data-qa-selector="assign_yourself_button"
+ @click="assignSelf"
+ >
+ {{ __('assign yourself') }}
+ </button>
+ </template>
+ </span>
<uncollapsed-reviewer-list
v-else
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index 5f1350690eb..faa36f3d8d2 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -5,12 +5,12 @@ import Vue from 'vue';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import eventHub from '~/sidebar/event_hub';
-import Store from '~/sidebar/stores/sidebar_store';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import getMergeRequestReviewersQuery from '~/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql';
-import mergeRequestReviewersUpdatedSubscription from '~/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import eventHub from '../../event_hub';
+import getMergeRequestReviewersQuery from '../../queries/get_merge_request_reviewers.query.graphql';
+import mergeRequestReviewersUpdatedSubscription from '../../queries/merge_request_reviewers.subscription.graphql';
+import Store from '../../stores/sidebar_store';
import ReviewerTitle from './reviewer_title.vue';
import Reviewers from './reviewers.vue';
@@ -143,6 +143,13 @@ export default {
eventHub.$off('sidebar.saveReviewers', this.saveReviewers);
},
methods: {
+ reviewBySelf() {
+ // Notify gl dropdown that we are now assigning to current user
+ this.$el.parentElement.dispatchEvent(new Event('assignYourself'));
+
+ this.mediator.addSelfReview();
+ this.saveReviewers();
+ },
saveReviewers() {
this.loading = true;
@@ -181,6 +188,7 @@ export default {
:editable="canUpdate"
:issuable-type="issuableType"
@request-review="requestReview"
+ @assign-self="reviewBySelf"
/>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/severity/constants.js b/app/assets/javascripts/sidebar/components/severity/constants.js
deleted file mode 100644
index 4f58ff38121..00000000000
--- a/app/assets/javascripts/sidebar/components/severity/constants.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { __, s__ } from '~/locale';
-
-export const INCIDENT_SEVERITY = {
- CRITICAL: {
- value: 'CRITICAL',
- icon: 'critical',
- label: s__('IncidentManagement|Critical - S1'),
- },
- HIGH: {
- value: 'HIGH',
- icon: 'high',
- label: s__('IncidentManagement|High - S2'),
- },
- MEDIUM: {
- value: 'MEDIUM',
- icon: 'medium',
- label: s__('IncidentManagement|Medium - S3'),
- },
- LOW: {
- value: 'LOW',
- icon: 'low',
- label: s__('IncidentManagement|Low - S4'),
- },
- UNKNOWN: {
- value: 'UNKNOWN',
- icon: 'unknown',
- label: s__('IncidentManagement|Unknown'),
- },
-};
-
-export const ISSUABLE_TYPES = {
- INCIDENT: 'incident',
-};
-
-export const I18N = {
- UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
- TRY_AGAIN: __('Please try again'),
- EDIT: __('Edit'),
- SEVERITY: s__('SeverityWidget|Severity'),
- SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
-};
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index f02e0c783e1..5b624c17b0c 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -8,8 +8,8 @@ import {
GlButton,
} from '@gitlab/ui';
import { createAlert } from '~/flash';
-import { INCIDENT_SEVERITY, ISSUABLE_TYPES, I18N } from './constants';
-import updateIssuableSeverity from './graphql/mutations/update_issuable_severity.mutation.graphql';
+import updateIssuableSeverity from '../../queries/update_issuable_severity.mutation.graphql';
+import { INCIDENT_SEVERITY, ISSUABLE_TYPES, SEVERITY_I18N as I18N } from '../../constants';
import SeverityToken from './severity.vue';
export default {
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index a685929cdea..35667495ace 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -6,7 +6,6 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
dropdowni18nText,
@@ -17,6 +16,7 @@ import {
Tracking,
} from 'ee_else_ce/sidebar/constants';
import SidebarDropdown from './sidebar_dropdown.vue';
+import SidebarEditableItem from './sidebar_editable_item.vue';
export default {
i18n: {
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue b/app/assets/javascripts/sidebar/components/status/status_dropdown.vue
index ba94932289e..7763ec00091 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/status_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/status/status_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { statusDropdownOptions } from '../constants';
+import { statusDropdownOptions } from '../../constants';
export default {
components: {
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 99e7c825b72..0fba1cb5e4e 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -4,10 +4,10 @@ import { createAlert } from '~/flash';
import { IssuableType } from '~/issues/constants';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
-import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import toast from '~/vue_shared/plugins/global_toast';
-import { subscribedQueries, Tracking } from '~/sidebar/constants';
+import { subscribedQueries, Tracking } from '../../constants';
+import SidebarEditableItem from '../sidebar_editable_item.vue';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
@@ -182,7 +182,7 @@ export default {
</script>
<template>
- <gl-dropdown-form v-if="isMergeRequest" class="gl-new-dropdown-item">
+ <gl-dropdown-form v-if="isMergeRequest" class="gl-dropdown-item">
<div class="gl-px-5 gl-pb-2 gl-pt-1">
<gl-toggle
:value="subscribed"
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue
index 8774b065c22..4c3ba76d12d 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/subscriptions_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/subscriptions_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { __ } from '~/locale';
-import { subscriptionsDropdownOptions } from '../constants';
+import { subscriptionsDropdownOptions } from '../../constants';
export default {
subscriptionsDropdownOptions,
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/constants.js b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
new file mode 100644
index 00000000000..56e986e3b27
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/constants.js
@@ -0,0 +1 @@
+export const CREATE_TIMELOG_MODAL_ID = 'create-timelog-modal';
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
new file mode 100644
index 00000000000..ec8e1ee9952
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/time_tracking/create_timelog_form.vue
@@ -0,0 +1,227 @@
+<script>
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlDatepicker,
+ GlFormTextarea,
+ GlModal,
+ GlAlert,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { formatDate } from '~/lib/utils/datetime_utility';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+import { joinPaths } from '~/lib/utils/url_utility';
+import { s__ } from '~/locale';
+import createTimelogMutation from '../../queries/create_timelog.mutation.graphql';
+import { CREATE_TIMELOG_MODAL_ID } from './constants';
+
+export default {
+ components: {
+ GlDatepicker,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlModal,
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ inject: ['issuableType'],
+ props: {
+ issuableId: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ timeSpent: '',
+ spentAt: null,
+ summary: '',
+ isLoading: false,
+ saveError: '',
+ };
+ },
+ computed: {
+ submitDisabled() {
+ return this.isLoading || this.timeSpent.length === 0;
+ },
+ primaryProps() {
+ return {
+ text: s__('CreateTimelogForm|Save'),
+ attributes: [
+ {
+ variant: 'confirm',
+ disabled: this.submitDisabled,
+ loading: this.isLoading,
+ },
+ ],
+ };
+ },
+ cancelProps() {
+ return {
+ text: s__('CreateTimelogForm|Cancel'),
+ };
+ },
+ timeTrackingDocsPath() {
+ return joinPaths(gon.relative_url_root || '', '/help/user/project/time_tracking.md');
+ },
+ issuableTypeName() {
+ return this.isIssue()
+ ? s__('CreateTimelogForm|issue')
+ : s__('CreateTimelogForm|merge request');
+ },
+ },
+ methods: {
+ resetModal() {
+ this.isLoading = false;
+ this.timeSpent = '';
+ this.spentAt = null;
+ this.summary = '';
+ this.saveError = '';
+ },
+ close() {
+ this.resetModal();
+ this.$refs.modal.close();
+ },
+ registerTimeSpent(event) {
+ event.preventDefault();
+
+ if (this.timeSpent.length === 0) {
+ return;
+ }
+
+ this.isLoading = true;
+ this.saveError = '';
+
+ this.$apollo
+ .mutate({
+ mutation: createTimelogMutation,
+ variables: {
+ input: {
+ timeSpent: this.timeSpent,
+ spentAt: this.spentAt
+ ? formatDate(this.spentAt, 'isoDateTime')
+ : formatDate(Date.now(), 'isoDateTime'),
+ summary: this.summary,
+ issuableId: this.getIssuableId(),
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.timelogCreate?.errors.length) {
+ this.saveError = data.timelogCreate.errors[0].message || data.timelogCreate.errors[0];
+ } else {
+ this.close();
+ }
+ })
+ .catch((error) => {
+ this.saveError =
+ error?.message ||
+ s__('CreateTimelogForm|An error occurred while saving the time entry.');
+ })
+ .finally(() => {
+ this.isLoading = false;
+ });
+ },
+ isIssue() {
+ return this.issuableType === 'issue';
+ },
+ getGraphQLEntityType() {
+ return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
+ },
+ updateSpentAtDate(val) {
+ this.spentAt = val;
+ },
+ getIssuableId() {
+ return convertToGraphQLId(this.getGraphQLEntityType(), this.issuableId);
+ },
+ },
+ CREATE_TIMELOG_MODAL_ID,
+};
+</script>
+
+<template>
+ <gl-modal
+ ref="modal"
+ :title="s__('CreateTimelogForm|Add time entry')"
+ :modal-id="$options.CREATE_TIMELOG_MODAL_ID"
+ size="sm"
+ data-testid="create-timelog-modal"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="registerTimeSpent"
+ @cancel="close"
+ @close="close"
+ @hide="close"
+ >
+ <p data-testid="timetracking-docs-link">
+ <gl-sprintf
+ :message="
+ s__(
+ 'CreateTimelogForm|Track time spent on this %{issuableTypeNameStart}%{issuableTypeNameEnd}. %{timeTrackingDocsLinkStart}%{timeTrackingDocsLinkEnd}',
+ )
+ "
+ >
+ <template #issuableTypeName>{{ issuableTypeName }}</template>
+ <template #timeTrackingDocsLink>
+ <gl-link :href="timeTrackingDocsPath" target="_blank">{{
+ s__('CreateTimelogForm|How do I track and estimate time?')
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
+ <form
+ class="gl-display-flex gl-flex-direction-column js-quick-submit"
+ @submit.prevent="registerTimeSpent"
+ >
+ <div class="gl-display-flex gl-gap-3">
+ <gl-form-group
+ key="time-spent"
+ label-for="time-spent"
+ :label="s__(`CreateTimelogForm|Time spent`)"
+ :description="s__(`CreateTimelogForm|Example: 1h 30m`)"
+ >
+ <gl-form-input
+ id="time-spent"
+ ref="timeSpent"
+ v-model="timeSpent"
+ class="gl-form-input-sm"
+ autocomplete="off"
+ />
+ </gl-form-group>
+ <gl-form-group
+ key="spent-at"
+ optional
+ label-for="spent-at"
+ :label="s__(`CreateTimelogForm|Spent at`)"
+ >
+ <gl-datepicker
+ :target="null"
+ :value="spentAt"
+ show-clear-button
+ autocomplete="off"
+ size="small"
+ @input="updateSpentAtDate"
+ @clear="updateSpentAtDate(null)"
+ />
+ </gl-form-group>
+ </div>
+ <gl-form-group
+ :label="s__('CreateTimelogForm|Summary')"
+ optional
+ label-for="summary"
+ class="gl-mb-0"
+ >
+ <gl-form-textarea id="summary" v-model="summary" rows="3" :no-resize="true" />
+ </gl-form-group>
+ <gl-alert v-if="saveError" variant="danger" class="gl-mt-5" :dismissible="false">
+ {{ saveError }}
+ </gl-alert>
+ <!-- This is needed to have the quick-submit behaviour (with Ctrl + Enter or Cmd + Enter) -->
+ <input type="submit" hidden />
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
index 91c15061fb9..6cd9596e43f 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/help_state.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { joinPaths } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
@@ -9,7 +10,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
computed: {
href() {
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index 124464088cf..6f4ced06ddf 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -5,8 +5,8 @@ import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { formatDate, parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { __, s__ } from '~/locale';
-import { timelogQueries } from '~/sidebar/constants';
-import deleteTimelogMutation from './graphql/mutations/delete_timelog.mutation.graphql';
+import { timelogQueries } from '../../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/sidebar_time_tracking.vue b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
index 62b05421884..06adc048942 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/sidebar_time_tracking.vue
@@ -30,6 +30,11 @@ export default {
required: false,
default: false,
},
+ canAddTimeEntries: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
mounted() {
this.listenForQuickActions();
@@ -67,6 +72,7 @@ export default {
:issuable-id="issuableId"
:issuable-iid="issuableIid"
:limit-to-hours="limitToHours"
+ :can-add-time-entries="canAddTimeEntries"
/>
</div>
</template>
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 13981c477c6..b32836dc87d 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -9,15 +9,17 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { IssuableType } from '~/issues/constants';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__, __ } from '~/locale';
-import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '~/sidebar/constants';
+import { HOW_TO_TRACK_TIME, timeTrackingQueries } from '../../constants';
import eventHub from '../../event_hub';
import TimeTrackingCollapsedState from './collapsed_state.vue';
import TimeTrackingComparisonPane from './comparison_pane.vue';
-import TimeTrackingHelpState from './help_state.vue';
import TimeTrackingReport from './report.vue';
import TimeTrackingSpentOnlyPane from './spent_only_pane.vue';
+import { CREATE_TIMELOG_MODAL_ID } from './constants';
+import CreateTimelogForm from './create_timelog_form.vue';
export default {
name: 'IssuableTimeTracker',
@@ -34,8 +36,8 @@ export default {
TimeTrackingCollapsedState,
TimeTrackingSpentOnlyPane,
TimeTrackingComparisonPane,
- TimeTrackingHelpState,
TimeTrackingReport,
+ CreateTimelogForm,
},
directives: {
GlModal: GlModalDirective,
@@ -87,6 +89,11 @@ export default {
default: true,
required: false,
},
+ canAddTimeEntries: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
data() {
return {
@@ -192,12 +199,12 @@ export default {
eventHub.$on('timeTracker:refresh', this.refresh);
},
methods: {
- toggleHelpState(show) {
- this.showHelp = show;
- },
refresh() {
this.$apollo.queries.issuableTimeTracking.refetch();
},
+ openRegisterTimeSpentModal() {
+ this.$root.$emit(BV_SHOW_MODAL, CREATE_TIMELOG_MODAL_ID);
+ },
},
};
</script>
@@ -215,24 +222,21 @@ export default {
:time-estimate-human-readable="humanTimeEstimate"
/>
<div
- class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold gl-mr-3"
+ class="hide-collapsed gl-line-height-20 gl-text-gray-900 gl-display-flex gl-align-items-center gl-font-weight-bold"
>
{{ __('Time tracking') }}
<gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" class="gl-ml-2" inline />
<gl-button
- :data-testid="showHelpState ? 'closeHelpButton' : 'helpButton'"
+ v-if="canAddTimeEntries"
+ v-gl-tooltip.left
category="tertiary"
size="small"
- variant="link"
class="gl-ml-auto"
- @click="toggleHelpState(!showHelpState)"
+ data-testid="add-time-entry-button"
+ :title="__('Add time entry')"
+ @click="openRegisterTimeSpentModal()"
>
- <gl-icon
- v-gl-tooltip.left
- :title="timeTrackingIconTitle"
- :name="timeTrackingIconName"
- class="gl-text-gray-900!"
- />
+ <gl-icon name="plus" class="gl-text-gray-900!" />
</gl-button>
</div>
<div v-if="!isTimeTrackingInfoLoading" class="hide-collapsed">
@@ -272,9 +276,7 @@ export default {
<time-tracking-report :limit-to-hours="limitToHours" :issuable-id="issuableId" />
</gl-modal>
</template>
- <transition name="help-state-toggle">
- <time-tracking-help-state v-if="showHelpState" />
- </transition>
+ <create-timelog-form :issuable-id="issuableId" />
</div>
</div>
</template>
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 5da2d65723a..b86ff279fd8 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
@@ -3,11 +3,11 @@ import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { createAlert } from '~/flash';
import { __, sprintf } from '~/locale';
-import { todoQueries, TodoMutationTypes, todoMutations } from '~/sidebar/constants';
-import { todoLabel } from '~/vue_shared/components/sidebar/todo_toggle//utils';
-import TodoButton from '~/vue_shared/components/sidebar/todo_toggle/todo_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
+import { todoQueries, TodoMutationTypes, todoMutations } from '../../constants';
+import { todoLabel } from '../../utils';
+import TodoButton from './todo_button.vue';
const trackingMixin = Tracking.mixin();
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
index cdc7422c7df..b49b8fc389b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { todoLabel, updateGlobalTodoCount } from './utils';
+import { todoLabel, updateGlobalTodoCount } from '../../utils';
export default {
components: {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
index 6dacf4e10d3..6dacf4e10d3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/toggle_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/toggle/toggle_sidebar.vue
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index 67b9b540e91..825a89daf58 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -4,55 +4,55 @@ import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutatio
import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import userSearchWithMRPermissionsQuery from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { IssuableType, WorkspaceType } from '~/issues/constants';
-import epicConfidentialQuery from '~/sidebar/queries/epic_confidential.query.graphql';
-import epicDueDateQuery from '~/sidebar/queries/epic_due_date.query.graphql';
-import epicParticipantsQuery from '~/sidebar/queries/epic_participants.query.graphql';
-import epicReferenceQuery from '~/sidebar/queries/epic_reference.query.graphql';
-import epicStartDateQuery from '~/sidebar/queries/epic_start_date.query.graphql';
-import epicSubscribedQuery from '~/sidebar/queries/epic_subscribed.query.graphql';
-import epicTodoQuery from '~/sidebar/queries/epic_todo.query.graphql';
-import issuableAssigneesSubscription from '~/sidebar/queries/issuable_assignees.subscription.graphql';
-import issueConfidentialQuery from '~/sidebar/queries/issue_confidential.query.graphql';
-import issueDueDateQuery from '~/sidebar/queries/issue_due_date.query.graphql';
-import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
-import issueSubscribedQuery from '~/sidebar/queries/issue_subscribed.query.graphql';
-import issueTimeTrackingQuery from '~/sidebar/queries/issue_time_tracking.query.graphql';
-import issueTodoQuery from '~/sidebar/queries/issue_todo.query.graphql';
-import mergeRequestMilestone from '~/sidebar/queries/merge_request_milestone.query.graphql';
-import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
-import mergeRequestSubscribed from '~/sidebar/queries/merge_request_subscribed.query.graphql';
-import mergeRequestTimeTrackingQuery from '~/sidebar/queries/merge_request_time_tracking.query.graphql';
-import mergeRequestTodoQuery from '~/sidebar/queries/merge_request_todo.query.graphql';
-import todoCreateMutation from '~/sidebar/queries/todo_create.mutation.graphql';
-import todoMarkDoneMutation from '~/sidebar/queries/todo_mark_done.mutation.graphql';
-import updateEpicConfidentialMutation from '~/sidebar/queries/update_epic_confidential.mutation.graphql';
-import updateEpicDueDateMutation from '~/sidebar/queries/update_epic_due_date.mutation.graphql';
-import updateEpicStartDateMutation from '~/sidebar/queries/update_epic_start_date.mutation.graphql';
-import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscription.mutation.graphql';
-import updateIssueConfidentialMutation from '~/sidebar/queries/update_issue_confidential.mutation.graphql';
-import updateIssueDueDateMutation from '~/sidebar/queries/update_issue_due_date.mutation.graphql';
-import updateIssueSubscriptionMutation from '~/sidebar/queries/update_issue_subscription.mutation.graphql';
-import mergeRequestMilestoneMutation from '~/sidebar/queries/update_merge_request_milestone.mutation.graphql';
-import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
-import updateMergeRequestSubscriptionMutation from '~/sidebar/queries/update_merge_request_subscription.mutation.graphql';
import updateAlertAssigneesMutation from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
-import epicLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_labels.query.graphql';
-import updateEpicLabelsMutation from '~/vue_shared/components/sidebar/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
-import groupLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/group_labels.query.graphql';
-import issueLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql';
-import mergeRequestLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/merge_request_labels.query.graphql';
-import projectLabelsQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
-import getIssueAssignees from '~/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql';
-import issueParticipantsQuery from '~/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql';
-import getIssueTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql';
-import getMergeRequestAssignees from '~/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql';
-import getMergeRequestParticipants from '~/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql';
-import getMrTimelogsQuery from '~/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql';
-import updateIssueAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql';
-import updateMergeRequestAssigneesMutation from '~/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql';
-import getEscalationStatusQuery from '~/sidebar/queries/escalation_status.query.graphql';
-import updateEscalationStatusMutation from '~/sidebar/queries/update_escalation_status.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';
@@ -350,3 +350,94 @@ export const escalationStatusQuery = getEscalationStatusQuery;
export const escalationStatusMutation = updateEscalationStatusMutation;
export const HOW_TO_TRACK_TIME = __('How to track time');
+
+export const statusDropdownOptions = [
+ {
+ text: __('Open'),
+ value: 'reopen',
+ },
+ {
+ text: __('Closed'),
+ value: 'close',
+ },
+];
+
+export const subscriptionsDropdownOptions = [
+ {
+ text: __('Subscribe'),
+ value: 'subscribe',
+ },
+ {
+ text: __('Unsubscribe'),
+ value: 'unsubscribe',
+ },
+];
+
+export const INCIDENT_SEVERITY = {
+ CRITICAL: {
+ value: 'CRITICAL',
+ icon: 'critical',
+ label: s__('IncidentManagement|Critical - S1'),
+ },
+ HIGH: {
+ value: 'HIGH',
+ icon: 'high',
+ label: s__('IncidentManagement|High - S2'),
+ },
+ MEDIUM: {
+ value: 'MEDIUM',
+ icon: 'medium',
+ label: s__('IncidentManagement|Medium - S3'),
+ },
+ LOW: {
+ value: 'LOW',
+ icon: 'low',
+ label: s__('IncidentManagement|Low - S4'),
+ },
+ UNKNOWN: {
+ value: 'UNKNOWN',
+ icon: 'unknown',
+ label: s__('IncidentManagement|Unknown'),
+ },
+};
+
+export const ISSUABLE_TYPES = {
+ INCIDENT: 'incident',
+};
+
+export const MILESTONE_STATE = {
+ ACTIVE: 'active',
+ CLOSED: 'closed',
+};
+
+export const SEVERITY_I18N = {
+ UPDATE_SEVERITY_ERROR: s__('SeverityWidget|There was an error while updating severity.'),
+ TRY_AGAIN: __('Please try again'),
+ EDIT: __('Edit'),
+ SEVERITY: s__('SeverityWidget|Severity'),
+ SEVERITY_VALUE: s__('SeverityWidget|Severity: %{severity}'),
+};
+
+export const STATUS_TRIGGERED = 'TRIGGERED';
+export const STATUS_ACKNOWLEDGED = 'ACKNOWLEDGED';
+export const STATUS_RESOLVED = 'RESOLVED';
+
+export const STATUS_TRIGGERED_LABEL = s__('IncidentManagement|Triggered');
+export const STATUS_ACKNOWLEDGED_LABEL = s__('IncidentManagement|Acknowledged');
+export const STATUS_RESOLVED_LABEL = s__('IncidentManagement|Resolved');
+
+export const STATUS_LABELS = {
+ [STATUS_TRIGGERED]: STATUS_TRIGGERED_LABEL,
+ [STATUS_ACKNOWLEDGED]: STATUS_ACKNOWLEDGED_LABEL,
+ [STATUS_RESOLVED]: STATUS_RESOLVED_LABEL,
+};
+
+export const INCIDENTS_I18N = {
+ fetchError: s__(
+ 'IncidentManagement|An error occurred while fetching the incident status. Please reload the page.',
+ ),
+ title: s__('IncidentManagement|Status'),
+ updateError: s__(
+ 'IncidentManagement|An error occurred while updating the incident status. Please reload the page and try again.',
+ ),
+};
diff --git a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
index afce59d304f..b908cf0cd9e 100644
--- a/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_milestone_sidebar.js
@@ -39,6 +39,7 @@ export default class SidebarMilestone {
humanTimeEstimate,
humanTotalTimeSpent: humanTimeSpent,
},
+ canAddTimeEntries: false,
},
}),
});
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index b37486283ca..a308dc8d13c 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -6,6 +6,7 @@ import { convertToGraphQLId } from '~/graphql_shared/utils';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import { IssuableType } from '~/issues/constants';
+import { gqlClient } from '~/issues/list/graphql';
import {
isInIssuePage,
isInDesignPage,
@@ -14,33 +15,36 @@ import {
parseBoolean,
} from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
-import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
-import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
-import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
-import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
-import SidebarParticipantsWidget from '~/sidebar/components/participants/sidebar_participants_widget.vue';
-import SidebarReferenceWidget from '~/sidebar/components/reference/sidebar_reference_widget.vue';
-import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
-import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import { apolloProvider } from '~/graphql_shared/issuable_client';
-import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
-import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
-import Translate from '../vue_shared/translate';
+import Translate from '~/vue_shared/translate';
+import CollapsedAssigneeList from './components/assignees/collapsed_assignee_list.vue';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
-import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
+import SidebarAssigneesWidget from './components/assignees/sidebar_assignees_widget.vue';
+import SidebarConfidentialityWidget from './components/confidential/sidebar_confidentiality_widget.vue';
+import CopyEmailToClipboard from './components/copy/copy_email_to_clipboard.vue';
+import SidebarDueDateWidget from './components/date/sidebar_date_widget.vue';
import SidebarEscalationStatus from './components/incidents/sidebar_escalation_status.vue';
+import { DropdownVariant } from './components/labels/labels_select_vue/constants';
+import { LabelType } from './components/labels/labels_select_widget/constants';
+import LabelsSelectWidget from './components/labels/labels_select_widget/labels_select_root.vue';
import IssuableLockForm from './components/lock/issuable_lock_form.vue';
+import MilestoneDropdown from './components/milestone/milestone_dropdown.vue';
+import MoveIssuesButton from './components/move/move_issues_button.vue';
+import SidebarParticipantsWidget from './components/participants/sidebar_participants_widget.vue';
+import SidebarReferenceWidget from './components/copy/sidebar_reference_widget.vue';
import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarReviewersInputs from './components/reviewers/sidebar_reviewers_inputs.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
+import SidebarDropdownWidget from './components/sidebar_dropdown_widget.vue';
+import StatusDropdown from './components/status/status_dropdown.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
+import SubscriptionsDropdown from './components/subscriptions/subscriptions_dropdown.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
+import SidebarTodoWidget from './components/todo_toggle/sidebar_todo_widget.vue';
import { IssuableAttributeType } from './constants';
-import SidebarMoveIssue from './lib/sidebar_move_issue';
import CrmContacts from './components/crm_contacts/crm_contacts.vue';
+import SidebarMoveIssue from './lib/sidebar_move_issue';
+import trackShowInviteMemberLink from './track_invite_members';
Vue.use(Translate);
Vue.use(VueApollo);
@@ -540,7 +544,15 @@ function mountSidebarSubscriptionsWidget() {
function mountSidebarTimeTracking() {
const el = document.querySelector('.js-sidebar-time-tracking-root');
- const { id, iid, fullPath, issuableType, timeTrackingLimitToHours } = getSidebarOptions();
+
+ const {
+ id,
+ iid,
+ fullPath,
+ issuableType,
+ timeTrackingLimitToHours,
+ canCreateTimelogs,
+ } = getSidebarOptions();
if (!el) {
return null;
@@ -558,6 +570,7 @@ function mountSidebarTimeTracking() {
issuableId: id.toString(),
issuableIid: iid.toString(),
limitToHours: timeTrackingLimitToHours,
+ canAddTimeEntries: canCreateTimelogs,
},
}),
});
@@ -635,6 +648,59 @@ function mountCopyEmailToClipboard() {
});
}
+export function mountMoveIssuesButton() {
+ const el = document.querySelector('.js-move-issues');
+
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(VueApollo);
+
+ return new Vue({
+ el,
+ name: 'MoveIssuesRoot',
+ apolloProvider: new VueApollo({
+ defaultClient: gqlClient,
+ }),
+ render: (createElement) =>
+ createElement(MoveIssuesButton, {
+ props: {
+ projectFullPath: el.dataset.projectFullPath,
+ projectsFetchPath: el.dataset.projectsFetchPath,
+ },
+ }),
+ });
+}
+
+export function mountStatusDropdown() {
+ const el = document.querySelector('.js-status-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'StatusDropdownRoot',
+ render: (createElement) => createElement(StatusDropdown),
+ });
+}
+
+export function mountSubscriptionsDropdown() {
+ const el = document.querySelector('.js-subscriptions-dropdown');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ name: 'SubscriptionsDropdownRoot',
+ render: (createElement) => createElement(SubscriptionsDropdown),
+ });
+}
+
const isAssigneesWidgetShown =
(isInIssuePage() || isInDesignPage() || isInMRPage()) && gon.features.issueAssigneesWidget;
diff --git a/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql b/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql
new file mode 100644
index 00000000000..a8692387a46
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/create_timelog.mutation.graphql
@@ -0,0 +1,17 @@
+#import "~/graphql_shared/fragments/issue_time_tracking.fragment.graphql"
+#import "~/graphql_shared/fragments/merge_request_time_tracking.fragment.graphql"
+
+mutation createTimelog($input: TimelogCreateInput!) {
+ timelogCreate(input: $input) {
+ errors
+ timelog {
+ id
+ issue {
+ ...IssueTimeTrackingFragment
+ }
+ mergeRequest {
+ ...MergeRequestTimeTrackingFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql b/app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql
index 6e916893b5a..6e916893b5a 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/graphql/mutations/delete_timelog.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/delete_timelog.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql
index bb6c7181e5c..171eca50eab 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_alert_assignees.query.graphql
@@ -9,6 +9,7 @@ query alertAssignees(
workspace: project(fullPath: $fullPath) {
id
issuable: alertManagementAlert(domain: $domain, iid: $iid) {
+ id
iid
assignees {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql
index 4af07366a6d..4af07366a6d 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_assignees.query.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql
index 30a0af10d56..30a0af10d56 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/get_issue_crm_contacts.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_crm_contacts.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql
index eae5e96ac46..eae5e96ac46 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_participants.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql b/app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql
index b127b8ec5a9..b127b8ec5a9 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_issue_timelogs.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_issue_timelogs.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
index f087ca6c982..f087ca6c982 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_merge_request_reviewers.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql
index f70cd723f2e..f70cd723f2e 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_assignees.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_assignees.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql
index 2781ac71f31..2781ac71f31 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_participants.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_participants.query.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql b/app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql
index 17f548b44b5..17f548b44b5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/get_mr_timelogs.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_mr_timelogs.query.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql
index 750e1f1d1af..750e1f1d1af 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.fragment.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.fragment.graphql
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql
index f3b6e4ec06f..f3b6e4ec06f 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/queries/issue_crm_contacts.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/issue_crm_contacts.subscription.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
index a1b16b378b3..a1b16b378b3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/merge_request_reviewers.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql b/app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql
index d350072425b..d350072425b 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/components/graphql/mutations/move_issue.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/move_issue.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql
index c9d36dfdb67..c9d36dfdb67 100644
--- a/app/assets/javascripts/sidebar/components/severity/graphql/mutations/update_issuable_severity.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issuable_severity.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql
index 24de5ea4fe3..24de5ea4fe3 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_issue_assignees.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_assignees.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql
index cb9ee6abc9b..cb9ee6abc9b 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_issue_lock.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql
index 11eb3611006..11eb3611006 100644
--- a/app/assets/javascripts/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_lock.mutation.graphql
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql
index 5fec2ccbdfb..5fec2ccbdfb 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/queries/update_mr_assignees.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/update_mr_assignees.mutation.graphql
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 912f0fdcbef..c6a66ab2275 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,9 +1,9 @@
-import Store from '~/sidebar/stores/sidebar_store';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
-import { visitUrl } from '../lib/utils/url_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
import Service from './services/sidebar_service';
+import Store from './stores/sidebar_store';
export default class SidebarMediator {
constructor(options) {
@@ -31,6 +31,9 @@ export default class SidebarMediator {
assignYourself() {
this.store.addAssignee(this.store.currentUser);
}
+ addSelfReview() {
+ this.store.addReviewer(this.store.currentUser);
+ }
async saveAssignees(field) {
const selected = this.store.assignees.map((u) => u.id);
@@ -56,12 +59,14 @@ export default class SidebarMediator {
}
async saveReviewers(field) {
- const selected = this.store.reviewers.map((u) => u.id);
+ const selectedReviewers = this.store.reviewers;
+ const selectedIds = selectedReviewers.map((u) => u.id);
+ const suggestedSelectedIds = selectedReviewers.filter((u) => u.suggested).map((u) => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
// And it only accepts an array, hence [0]
- const reviewers = selected.length === 0 ? [0] : selected;
- const data = { reviewer_ids: reviewers };
+ const reviewers = selectedIds.length === 0 ? [0] : selectedIds;
+ const data = { reviewer_ids: reviewers, suggested_reviewer_ids: suggestedSelectedIds };
try {
const res = await this.service.update(field, data);
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js b/app/assets/javascripts/sidebar/utils.js
index 098ab72dfb5..6b90fb80abf 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
+++ b/app/assets/javascripts/sidebar/utils.js
@@ -1,4 +1,7 @@
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { STATUS_LABELS } from './constants';
+
+export const getStatusLabel = (status) => STATUS_LABELS[status] ?? s__('IncidentManagement|None');
export const todoLabel = (hasTodo) => {
return hasTodo ? __('Mark as done') : __('Add a to do');
diff --git a/app/assets/javascripts/snippets/components/snippet_description_view.vue b/app/assets/javascripts/snippets/components/snippet_description_view.vue
index 737a131ce7c..ab2ff6e0ef8 100644
--- a/app/assets/javascripts/snippets/components/snippet_description_view.vue
+++ b/app/assets/javascripts/snippets/components/snippet_description_view.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
export default {
diff --git a/app/assets/javascripts/surveys/merge_request_experience/app.vue b/app/assets/javascripts/surveys/merge_request_experience/app.vue
index df114c27908..6e90ad2e0fd 100644
--- a/app/assets/javascripts/surveys/merge_request_experience/app.vue
+++ b/app/assets/javascripts/surveys/merge_request_experience/app.vue
@@ -1,6 +1,7 @@
<script>
-import { GlButton, GlSprintf, GlSafeHtmlDirective, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import gitlabLogo from '@gitlab/svgs/dist/illustrations/gitlab_logo.svg';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SatisfactionRate from '~/surveys/components/satisfaction_rate.vue';
@@ -30,7 +31,7 @@ export default {
SatisfactionRate,
},
directives: {
- safeHtml: GlSafeHtmlDirective,
+ SafeHtml,
tooltip: GlTooltipDirective,
},
mixins: [Tracking.mixin()],
diff --git a/app/assets/javascripts/tags/init_new_tag_ref_selector.js b/app/assets/javascripts/tags/init_new_tag_ref_selector.js
new file mode 100644
index 00000000000..11c7516f16c
--- /dev/null
+++ b/app/assets/javascripts/tags/init_new_tag_ref_selector.js
@@ -0,0 +1,23 @@
+import Vue from 'vue';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+export default function initNewTagRefSelector() {
+ const el = document.querySelector('.js-new-tag-ref-selector');
+
+ if (el) {
+ const { projectId, defaultBranchName, hiddenInputName } = el.dataset;
+ // eslint-disable-next-line no-new
+ new Vue({
+ el,
+ render(createComponent) {
+ return createComponent(RefSelector, {
+ props: {
+ value: defaultBranchName,
+ name: hiddenInputName,
+ projectId,
+ },
+ });
+ },
+ });
+ }
+}
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index a54a198faed..eecf32f83df 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -1,13 +1,13 @@
<script>
-import $ from 'jquery';
-import { GlButton, GlIntersectionObserver, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlIntersectionObserver } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import csrf from '~/lib/utils/csrf';
-import '~/behaviors/markdown/render_gfm';
import { trackTrialAcceptTerms } from '~/google_tag_manager';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
name: 'TermsApp',
@@ -54,7 +54,7 @@ export default {
},
methods: {
renderGFM() {
- $(this.$refs.gfmContainer).renderGFM();
+ renderGFM(this.$refs.gfmContainer);
},
handleBottomReached() {
this.acceptDisabled = false;
@@ -81,7 +81,7 @@ export default {
<template>
<div>
- <div class="gl-card-body gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
+ <div class="gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
<div
class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none"
></div>
@@ -96,7 +96,7 @@ export default {
</gl-intersection-observer>
</div>
</div>
- <div v-if="isLoggedIn" class="gl-card-footer gl-display-flex gl-justify-content-end">
+ <div v-if="isLoggedIn" class="gl-display-flex gl-justify-content-end">
<form v-if="permissions.canDecline" method="post" :action="paths.decline">
<gl-button type="submit">{{ $options.i18n.decline }}</gl-button>
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/terraform/components/init_command_modal.vue b/app/assets/javascripts/terraform/components/init_command_modal.vue
index 2cb10d4ae23..0d8a883972f 100644
--- a/app/assets/javascripts/terraform/components/init_command_modal.vue
+++ b/app/assets/javascripts/terraform/components/init_command_modal.vue
@@ -39,11 +39,13 @@ export default {
},
methods: {
getModalInfoCopyStr() {
+ const stateNameEncoded = encodeURIComponent(this.stateName);
+
return `export GITLAB_ACCESS_TOKEN=<YOUR-ACCESS-TOKEN>
terraform init \\
- -backend-config="address=${this.terraformApiUrl}/${this.stateName}" \\
- -backend-config="lock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
- -backend-config="unlock_address=${this.terraformApiUrl}/${this.stateName}/lock" \\
+ -backend-config="address=${this.terraformApiUrl}/${stateNameEncoded}" \\
+ -backend-config="lock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
+ -backend-config="unlock_address=${this.terraformApiUrl}/${stateNameEncoded}/lock" \\
-backend-config="username=${this.username}" \\
-backend-config="password=$GITLAB_ACCESS_TOKEN" \\
-backend-config="lock_method=POST" \\
diff --git a/app/assets/javascripts/tooltips/components/tooltips.vue b/app/assets/javascripts/tooltips/components/tooltips.vue
index 1ad18508294..a4dc783f1e4 100644
--- a/app/assets/javascripts/tooltips/components/tooltips.vue
+++ b/app/assets/javascripts/tooltips/components/tooltips.vue
@@ -1,6 +1,7 @@
<script>
-import { GlTooltip, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlTooltip } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
const getTooltipTitle = (element) => {
return element.getAttribute('title') || element.dataset.title;
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 2cfeb7a4bcb..eb93f42e2f3 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
@@ -189,8 +189,11 @@ export default {
.then((data) => {
this.mr.setApprovals(data);
- eventHub.$emit('MRWidgetUpdateRequested');
- eventHub.$emit('ApprovalUpdated');
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ eventHub.$emit('ApprovalUpdated');
+ }
+
this.$emit('updated');
})
.catch(errFn)
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 1256b3a8e52..c7d34d45f06 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
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlSprintf, GlLink } from '@gitlab/ui';
import { backOff } from '~/lib/utils/common_utils';
-import statusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import { bytesToMiB } from '~/lib/utils/number_utils';
import { s__ } from '~/locale';
import MemoryGraph from '~/vue_shared/components/memory_graph.vue';
@@ -107,7 +107,7 @@ export default {
backOff((next, stop) => {
MRWidgetService.fetchMetrics(this.metricsUrl)
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
this.backOffRequestCounter += 1;
/* eslint-disable no-unused-expressions */
this.backOffRequestCounter < 3 ? next() : stop(res);
@@ -118,7 +118,7 @@ export default {
.catch(stop);
})
.then((res) => {
- if (res.status === statusCodes.NO_CONTENT) {
+ if (res.status === HTTP_STATUS_NO_CONTENT) {
return res;
}
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 3d03dbd9db3..e8cc9b2eb2a 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
@@ -1,12 +1,7 @@
<script>
-import {
- GlButton,
- GlLoadingIcon,
- GlSafeHtmlDirective,
- GlTooltipDirective,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIntersectionObserver } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { sprintf, s__, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
@@ -40,7 +35,7 @@ export default {
StateContainer,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlTooltip: GlTooltipDirective,
},
data() {
@@ -323,19 +318,23 @@ export default {
@mouseup="onRowMouseUp"
>
<div
+ :class="{ 'gl-h-full': isLoadingSummary }"
class="media-body gl-display-flex gl-flex-direction-row! gl-w-full"
data-testid="widget-extension-top-level"
>
- <div class="gl-flex-grow-1" data-testid="widget-extension-top-level-summary">
+ <div
+ class="gl-flex-grow-1 gl-display-flex gl-align-items-center"
+ data-testid="widget-extension-top-level-summary"
+ >
<template v-if="isLoadingSummary">{{ widgetLoadingText }}</template>
<template v-else-if="hasFetchError">{{ widgetErrorText }}</template>
- <div v-else>
+ <template v-else>
<span v-safe-html="hydratedSummary.subject"></span>
<template v-if="hydratedSummary.meta">
<br />
<span v-safe-html="hydratedSummary.meta" class="gl-font-sm"></span>
</template>
- </div>
+ </template>
</div>
<actions
:widget="$options.label || $options.name"
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 a10e5efa0e7..fa369d23b6c 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
@@ -1,6 +1,7 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective, GlModalDirective } from '@gitlab/ui';
+import { GlBadge, GlLink, GlModalDirective } from '@gitlab/ui';
import { isArray } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Actions from '../action_buttons.vue';
import StatusIcon from './status_icon.vue';
import { generateText } from './utils';
@@ -14,7 +15,7 @@ export default {
Actions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
GlModal: GlModalDirective,
},
props: {
@@ -97,7 +98,12 @@ export default {
<div v-if="data.supportingText">
<p v-safe-html="generateText(data.supportingText)" class="gl-m-0"></p>
</div>
- <gl-badge v-if="data.badge" :variant="data.badge.variant || 'info'">
+ <gl-badge
+ v-if="data.badge"
+ :variant="data.badge.variant || 'info'"
+ size="sm"
+ class="gl-ml-2"
+ >
{{ data.badge.text }}
</gl-badge>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
index f71b1fbc539..79ea2624ec5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_author.vue
@@ -1,8 +1,11 @@
<script>
-import { GlTooltipDirective } from '@gitlab/ui';
+import { GlTooltipDirective, GlLink } from '@gitlab/ui';
export default {
name: 'MrWidgetAuthor',
+ components: {
+ GlLink,
+ },
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -28,13 +31,16 @@ export default {
};
</script>
<template>
- <a
+ <gl-link
v-gl-tooltip
:href="authorUrl"
:title="showAuthorName ? null : author.name"
- class="author-link inline"
+ class="mr-widget-author"
>
- <img :src="avatarUrl" class="avatar avatar-inline s16" />
- <span v-if="showAuthorName" class="author">{{ author.name }}</span>
- </a>
+ <img :src="avatarUrl" :alt="author.name" class="avatar avatar-inline s16" /><span
+ v-if="showAuthorName"
+ class="author"
+ >{{ author.name }}</span
+ >
+ </gl-link>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 97c6de37054..d8a361066f4 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
@@ -7,8 +7,8 @@ import {
GlSprintf,
GlTooltip,
GlTooltipDirective,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
@@ -33,7 +33,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
pipeline: {
@@ -190,7 +190,7 @@ export default {
</template>
<template v-else-if="hasPipeline">
<a :href="status.details_path" class="gl-align-self-center gl-mr-3">
- <ci-icon :status="status" :size="24" />
+ <ci-icon :status="status" :size="24" class="gl-display-flex" />
</a>
<div class="ci-widget-container d-flex">
<div class="ci-widget-content">
@@ -277,9 +277,9 @@ export default {
v-if="pipeline.details.stages"
:downstream-pipelines="pipeline.triggered"
:is-merge-train="isMergeTrain"
+ :pipeline-path="pipeline.path"
:stages="pipeline.details.stages"
:upstream-pipeline="pipeline.triggered_by"
- stages-class="mr-widget-pipeline-stages"
/>
<pipeline-artifacts :pipeline-id="pipeline.id" :artifacts="artifacts" class="gl-ml-3" />
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
index 870972156c5..1fd1e264c25 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_related_links.vue
@@ -1,5 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
export default {
@@ -54,16 +55,16 @@ export default {
</script>
<template>
<section>
- <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.closing" class="gl-display-inline gl-m-0 gl-font-sm!">
{{ closesText }}
<span v-safe-html="relatedLinks.closing"></span>
</p>
- <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0">
+ <p v-if="relatedLinks.mentioned" class="gl-display-inline gl-m-0 gl-font-sm!">
<span v-if="relatedLinks.closing">&middot;</span>
{{ n__('mrWidget|Mentions issue', 'mrWidget|Mentions issues', relatedLinks.mentionedCount) }}
<span v-safe-html="relatedLinks.mentioned"></span>
</p>
- <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0">
+ <p v-if="shouldShowAssignToMeLink" class="gl-display-inline gl-m-0 gl-font-sm!">
<span>
<gl-link rel="nofollow" data-method="post" :href="relatedLinks.assignToMe">{{
assignIssueText
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
index 66e33a08a12..9a3555d3e11 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/state_container.vue
@@ -54,7 +54,7 @@ export default {
<template>
<div
- class="mr-widget-body media mr-widget-body-line-height-1 gl-line-height-normal"
+ class="mr-widget-body media gl-display-flex gl-align-items-center"
:class="wrapperClasses"
v-on="$listeners"
>
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 38b99dae264..e5688091cc7 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
@@ -1,6 +1,6 @@
<script>
import { s__ } from '~/locale';
-import StatusIcon from '../mr_widget_status_icon.vue';
+import StateContainer from '../state_container.vue';
import { DETAILED_MERGE_STATUS } from '../../constants';
export default {
@@ -12,7 +12,7 @@ export default {
externalStatusChecksFailed: s__('mrWidget|Merge blocked: all status checks must pass.'),
},
components: {
- StatusIcon,
+ StateContainer,
},
props: {
mr: {
@@ -37,10 +37,11 @@ export default {
</script>
<template>
- <div class="mr-widget-body media gl-flex-wrap">
- <status-icon status="failed" />
- <p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!">
+ <state-container :mr="mr" status="failed">
+ <span
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
+ >
{{ failedText }}
- </p>
- </div>
+ </span>
+ </state-container>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
index 806f8f939a6..6bcf88713a5 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_closed.vue
@@ -1,7 +1,17 @@
<script>
+import api from '~/api';
+import showGlobalToast from '~/vue_shared/plugins/global_toast';
+
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
import StateContainer from '../state_container.vue';
+import {
+ MR_WIDGET_CLOSED_REOPEN,
+ MR_WIDGET_CLOSED_REOPENING,
+ MR_WIDGET_CLOSED_RELOADING,
+ MR_WIDGET_CLOSED_REOPEN_FAILURE,
+} from '../../i18n';
+
export default {
name: 'MRWidgetClosed',
components: {
@@ -14,10 +24,62 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ isPending: false,
+ isReloading: false,
+ };
+ },
+ computed: {
+ reopenText() {
+ let text = MR_WIDGET_CLOSED_REOPEN;
+
+ if (this.isPending) {
+ text = MR_WIDGET_CLOSED_REOPENING;
+ } else if (this.isReloading) {
+ text = MR_WIDGET_CLOSED_RELOADING;
+ }
+
+ return text;
+ },
+ actions() {
+ if (!window.gon?.current_user_id) {
+ return [];
+ }
+
+ return [
+ {
+ text: this.reopenText,
+ loading: this.isPending || this.isReloading,
+ onClick: this.reopen,
+ testId: 'extension-actions-reopen-button',
+ },
+ ];
+ },
+ },
+ methods: {
+ reopen() {
+ this.isPending = true;
+
+ api
+ .updateMergeRequest(this.mr.targetProjectId, this.mr.iid, { state_event: 'reopen' })
+ .then(() => {
+ this.isReloading = true;
+
+ window.location.reload();
+ })
+ .catch(() => {
+ showGlobalToast(MR_WIDGET_CLOSED_REOPEN_FAILURE);
+ })
+ .finally(() => {
+ this.isPending = false;
+ });
+ },
+ },
};
</script>
<template>
- <state-container :mr="mr" status="closed">
+ <state-container :mr="mr" status="closed" :actions="actions">
<mr-widget-author-time
:action-text="s__('mrWidget|Closed by')"
:author="mr.metrics.closedBy"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 4902c9b45e8..850a4e2fd56 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlSprintf, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import emptyStateSVG from 'icons/_mr_widget_empty_state.svg';
import api from '~/api';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -12,7 +13,7 @@ export default {
GlLink,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
mr: {
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 c54672cd0f8..23b163e2c6a 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
@@ -20,6 +20,8 @@ import simplePoll from '~/lib/utils/simple_poll';
import { __, s__, n__ } from '~/locale';
import SmartInterval from '~/smart_interval';
import { helpPagePath } from '~/helpers/help_page_helper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import readyToMergeSubscription from '~/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql';
import {
AUTO_MERGE_STRATEGIES,
WARNING,
@@ -87,6 +89,31 @@ export default {
this.initPolling();
}
},
+ subscribeToMore: {
+ document() {
+ return readyToMergeSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.state = mergeRequestMergeStatusUpdated;
+ }
+ },
+ },
},
},
components: {
@@ -295,7 +322,7 @@ export default {
return this.mr.divergedCommitsCount > 0;
},
showMergeDetailsHeader() {
- return ['readyToMerge'].indexOf(this.mr.state) >= 0;
+ return !['readyToMerge'].includes(this.mr.state);
},
},
mounted() {
@@ -467,8 +494,9 @@ export default {
<template>
<div
+ :class="{ 'gl-bg-gray-10': mr.state !== 'closed' && mr.state !== 'merged' }"
data-testid="ready_to_merge_state"
- class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7"
+ class="gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-pl-7"
>
<div v-if="loading" class="mr-widget-body">
<div class="gl-w-full mr-ready-to-merge-loader">
@@ -481,7 +509,9 @@ export default {
</div>
</div>
<template v-else>
- <div class="mr-widget-body mr-widget-body-ready-merge media mr-widget-body-line-height-1">
+ <div
+ class="mr-widget-body mr-widget-body-ready-merge media gl-display-flex gl-align-items-center"
+ >
<div class="media-body">
<div class="mr-widget-body-controls gl-display-flex gl-align-items-center gl-flex-wrap">
<template v-if="shouldShowMergeControls">
@@ -555,7 +585,19 @@ export default {
</li>
</ul>
</div>
- <div class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5">
+ <div
+ class="gl-w-full gl-text-gray-500 gl-mb-3 gl-md-mb-0 gl-md-pb-5 mr-widget-merge-details"
+ >
+ <template v-if="sourceHasDivergedFromTarget">
+ <gl-sprintf :message="$options.i18n.sourceDivergedFromTargetText">
+ <template #link>
+ <gl-link :href="mr.targetBranchPath">{{
+ $options.i18n.divergedCommits(mr.divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ &middot;
+ </template>
<added-commit-message
:is-squash-enabled="squashBeforeMerge"
:is-fast-forward-enabled="!shouldShowMergeEdit"
@@ -631,7 +673,7 @@ export default {
class="gl-w-full gl-order-n1 mr-widget-merge-details"
data-qa-selector="merged_status_content"
>
- <p v-if="showMergeDetailsHeader" class="gl-mb-3 gl-text-gray-900">
+ <p v-if="showMergeDetailsHeader" class="gl-mb-2 gl-text-gray-900">
{{ __('Merge details') }}
</p>
<ul class="gl-pl-4 gl-mb-0 gl-ml-3 gl-text-gray-600">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 074758e33b2..9f3748599dc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -26,7 +26,7 @@ export default {
<template>
<state-container :mr="mr" status="failed">
<span
- class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body! gl-align-self-start"
+ class="gl-ml-3 gl-font-weight-bold gl-w-100 gl-flex-grow-1 gl-md-mr-3 gl-ml-0! gl-text-body!"
>
{{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index ef5be0fbfcd..01f9b4757a0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -94,6 +94,7 @@ export default {
errors: [],
mergeRequest: {
__typename: 'MergeRequest',
+ id: this.mr.issuableId,
mergeableDiscussionsState: true,
title: this.mr.title,
draft: false,
@@ -111,7 +112,10 @@ export default {
}) => {
toast(__('Marked as ready. Merging is now allowed.'));
$('.merge-request .detail-page-description .title').text(title);
- eventHub.$emit('MRWidgetUpdateRequested');
+
+ if (!window.gon?.features?.realtimeMrStatusChange) {
+ eventHub.$emit('MRWidgetUpdateRequested');
+ }
},
)
.catch(() =>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
new file mode 100644
index 00000000000..6655af92a55
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/action_buttons.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlButton, GlDropdown, GlDropdownItem, GlTooltipDirective } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ widget: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tertiaryButtons: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data: () => {
+ return {
+ timeout: null,
+ updatingTooltip: false,
+ };
+ },
+ computed: {
+ dropdownLabel() {
+ if (!this.widget) return undefined;
+
+ return sprintf(__('%{widget} options'), { widget: this.widget });
+ },
+ },
+ methods: {
+ onClickAction(action) {
+ this.$emit('clickedAction', action);
+
+ if (action.onClick) {
+ action.onClick();
+ }
+
+ if (action.tooltipOnClick) {
+ this.updatingTooltip = true;
+ this.$root.$emit('bv::show::tooltip', action.id);
+
+ clearTimeout(this.timeout);
+
+ this.timeout = setTimeout(() => {
+ this.updatingTooltip = false;
+ this.$root.$emit('bv::hide::tooltip', action.id);
+ }, 1000);
+ }
+ },
+ setTooltip(btn) {
+ if (this.updatingTooltip && btn.tooltipOnClick) {
+ return btn.tooltipOnClick;
+ }
+
+ return btn.tooltipText;
+ },
+ actionButtonQaSelector(btn) {
+ if (btn.dataQaSelector) {
+ return btn.dataQaSelector;
+ }
+ return 'mr_widget_extension_actions_button';
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-flex-start">
+ <gl-dropdown
+ v-if="tertiaryButtons.length"
+ v-gl-tooltip
+ :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>
+ <template v-if="tertiaryButtons.length">
+ <gl-button
+ v-for="(btn, index) in tertiaryButtons"
+ :id="btn.id"
+ :key="index"
+ v-gl-tooltip.hover
+ :title="setTooltip(btn)"
+ :href="btn.href"
+ :target="btn.target"
+ :class="[{ 'gl-mr-3': index !== tertiaryButtons.length - 1 }, btn.class]"
+ :data-clipboard-text="btn.dataClipboardText"
+ :data-qa-selector="actionButtonQaSelector(btn)"
+ :data-method="btn.dataMethod"
+ :icon="btn.icon"
+ :data-testid="btn.testId || 'extension-actions-button'"
+ :variant="btn.variant || 'confirm'"
+ :loading="btn.loading"
+ :disabled="btn.loading"
+ category="tertiary"
+ size="small"
+ class="gl-display-none gl-md-display-block gl-float-left"
+ @click="onClickAction(btn)"
+ >
+ <template v-if="btn.text">
+ {{ btn.text }}
+ </template>
+ </gl-button>
+ </template>
+ </div>
+</template>
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 2f52ac70833..18aa85484ea 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
@@ -20,13 +20,14 @@ export default {
role="region"
:aria-label="__('Merge request reports')"
data-testid="mr-widget-app"
+ class="mr-widget-section"
>
<component
:is="widget"
v-for="(widget, index) in widgets"
:key="widget.name || index"
:mr="mr"
- :class="{ 'mr-widget-border-top': index === 0 }"
+ class="mr-widget-section"
/>
</section>
</template>
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 4d66c75719b..cdce7c6625a 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
@@ -1,8 +1,9 @@
<script>
-import { GlBadge, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
-import Actions from '../action_buttons.vue';
+import { GlBadge, GlLink } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { generateText } from '../extensions/utils';
import ContentRow from './widget_content_row.vue';
+import Actions from './action_buttons.vue';
export default {
name: 'DynamicContent',
@@ -13,7 +14,7 @@ export default {
ContentRow,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
data: {
@@ -81,10 +82,8 @@ export default {
v-if="data.children && data.children.length > 0 && level === 2"
class="gl-m-0 gl-p-0 gl-list-style-none"
>
- <li>
+ <li v-for="(childData, index) in data.children" :key="childData.id || index">
<dynamic-content
- v-for="(childData, index) in data.children"
- :key="childData.id || index"
:data="childData"
:widget-name="widgetName"
:level="3"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
index 181b8cfad9a..6d17ac98d7f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -48,9 +48,9 @@ export default {
:class="{
[iconClassNameText]: !isLoading,
[`mr-widget-status-icon-level-${level}`]: !isLoading,
- 'gl-mr-3': level === 1,
+ 'gl-w-6 gl-h-6 gl--flex-center': level === 1,
}"
- class="gl-relative gl-w-6 gl-h-6 gl-rounded-full gl--flex-center"
+ class="gl-relative gl-rounded-full gl-mr-3"
>
<gl-loading-icon v-if="isLoading" size="md" inline />
<gl-icon
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 cea7fb8260a..cdf35033021 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
@@ -1,22 +1,18 @@
<script>
-import {
- GlButton,
- GlLink,
- GlTooltipDirective,
- GlLoadingIcon,
- GlSafeHtmlDirective,
-} from '@gitlab/ui';
+import { GlButton, GlLink, GlTooltipDirective, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { normalizeHeaders } from '~/lib/utils/common_utils';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { sprintf, __ } from '~/locale';
import Poll from '~/lib/utils/poll';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import { DynamicScroller, DynamicScrollerItem } from 'vendor/vue-virtual-scroller';
import { EXTENSION_ICONS } from '../../constants';
import { createTelemetryHub } from '../extensions/telemetry';
import ContentRow from './widget_content_row.vue';
import DynamicContent from './dynamic_content.vue';
import StatusIcon from './status_icon.vue';
+import ActionButtons from './action_buttons.vue';
const FETCH_TYPE_COLLAPSED = 'collapsed';
const FETCH_TYPE_EXPANDED = 'expanded';
@@ -31,11 +27,13 @@ export default {
GlLoadingIcon,
ContentRow,
DynamicContent,
+ DynamicScroller,
+ DynamicScrollerItem,
HelpPopover,
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
/**
@@ -258,6 +256,7 @@ export default {
<div class="gl-display-flex">
<help-popover
v-if="helpPopover"
+ icon="information-o"
:options="helpPopover.options"
:class="{ 'gl-mr-3': actionButtons.length > 0 }"
>
@@ -309,7 +308,7 @@ export default {
<div v-if="isLoadingExpandedContent" class="report-block-container gl-text-center">
<gl-loading-icon size="sm" inline /> {{ loadingText }}
</div>
- <div v-else class="gl-px-5 gl-display-flex">
+ <div v-else class="gl-pl-5 gl-display-flex" :class="{ 'gl-pr-5': $scopedSlots.content }">
<content-row
v-if="contentError"
:level="2"
@@ -322,12 +321,25 @@ export default {
</content-row>
<div v-else class="gl-w-full">
<slot name="content">
- <dynamic-content
- v-for="(data, index) in content"
- :key="data.id || index"
- :data="data"
- :widget-name="widgetName"
- />
+ <dynamic-scroller
+ v-if="content"
+ :items="content"
+ :min-item-size="32"
+ :style="{ maxHeight: '170px' }"
+ data-testid="dynamic-content-scroller"
+ class="gl-pr-5"
+ >
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <dynamic-content
+ :key="item.id || index"
+ :data="item"
+ :widget-name="widgetName"
+ :level="2"
+ />
+ </dynamic-scroller-item>
+ </template>
+ </dynamic-scroller>
</slot>
</div>
</div>
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 1fd1e325863..543136dc659 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
@@ -1,10 +1,11 @@
<script>
-import { GlSafeHtmlDirective, GlLink } from '@gitlab/ui';
+import { GlLink } from '@gitlab/ui';
import { __ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import ActionButtons from '../action_buttons.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { EXTENSION_ICONS } from '../../constants';
import { generateText } from '../extensions/utils';
+import ActionButtons from './action_buttons.vue';
import StatusIcon from './status_icon.vue';
export default {
@@ -15,7 +16,7 @@ export default {
ActionButtons,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
level: {
@@ -67,6 +68,9 @@ export default {
shouldShowHeaderActions() {
return Boolean(this.helpPopover) || this.actionButtons?.length > 0;
},
+ hasActionButtons() {
+ return this.actionButtons.length > 0;
+ },
},
i18n: {
learnMore: __('Learn more'),
@@ -75,10 +79,15 @@ export default {
</script>
<template>
<div
- class="gl-w-full gl-display-flex mr-widget-content-row gl-align-items-baseline"
+ class="gl-w-full gl-display-flex gl-align-items-baseline"
:class="{ 'gl-border-t gl-py-3 gl-pl-7': level === 2 }"
>
- <status-icon v-if="statusIconName" :level="2" :name="widgetName" :icon-name="statusIconName" />
+ <status-icon
+ v-if="statusIconName && !header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<div class="gl-w-full">
<div class="gl-display-flex">
<slot name="header">
@@ -95,7 +104,12 @@ export default {
v-if="shouldShowHeaderActions"
class="gl-ml-auto gl-display-flex gl-align-items-baseline"
>
- <help-popover v-if="helpPopover" :options="helpPopover.options">
+ <help-popover
+ v-if="helpPopover"
+ :options="helpPopover.options"
+ :class="{ 'gl-mr-3': hasActionButtons }"
+ icon="information-o"
+ >
<template v-if="helpPopover.content">
<p
v-if="helpPopover.content.text"
@@ -112,14 +126,19 @@ export default {
</template>
</help-popover>
<action-buttons
- v-if="actionButtons.length > 0"
+ v-if="hasActionButtons"
:widget="widgetName"
:tertiary-buttons="actionButtons"
- :class="{ 'gl-ml-2': helpPopover }"
/>
</div>
</div>
<div class="gl-display-flex gl-align-items-baseline gl-w-full">
+ <status-icon
+ v-if="statusIconName && header"
+ :level="2"
+ :name="widgetName"
+ :icon-name="statusIconName"
+ />
<slot name="body"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
new file mode 100644
index 00000000000..03af21a5019
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/constants.js
@@ -0,0 +1,31 @@
+import { n__, s__, sprintf } from '~/locale';
+
+export const i18n = {
+ label: s__('ciReport|Code Quality'),
+ loading: s__('ciReport|Code Quality is loading'),
+ error: s__('ciReport|Code Quality failed to load results'),
+ noChanges: s__(`ciReport|Code Quality hasn't changed.`),
+ prependText: s__(`ciReport|in`),
+ fixed: s__(`ciReport|Fixed`),
+ pluralReport: (errors) =>
+ sprintf(
+ n__(
+ '%{strong_start}%{errors}%{strong_end} point',
+ '%{strong_start}%{errors}%{strong_end} points',
+ errors.length,
+ ),
+ {
+ errors: errors.length,
+ },
+ false,
+ ),
+ singularReport: (errors) => n__('%d point', '%d points', errors.length),
+ improvementAndDegradationCopy: (improvement, degradation) =>
+ sprintf(
+ s__(`ciReport|Code Quality improved on ${improvement} and degraded on ${degradation}.`),
+ ),
+ improvedCopy: (improvements) =>
+ sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`)),
+ degradedCopy: (degradations) =>
+ sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`)),
+};
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 68347ac269e..394f8979a53 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
@@ -1,54 +1,33 @@
-import { n__, s__, sprintf } from '~/locale';
import axios from '~/lib/utils/axios_utils';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
-import { SEVERITY_ICONS_EXTENSION } from '~/reports/codequality_report/constants';
-import { parseCodeclimateMetrics } from '~/reports/codequality_report/store/utils/codequality_parser';
+import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
+import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import { i18n } from './constants';
export default {
name: 'WidgetCodeQuality',
+ enablePolling: true,
props: ['codeQuality', 'blobPath'],
- i18n: {
- label: s__('ciReport|Code Quality'),
- loading: s__('ciReport|Code Quality test metrics results are being parsed'),
- error: s__('ciReport|Code Quality failed loading results'),
- },
+ i18n,
computed: {
- summary() {
- const { newErrors, resolvedErrors, errorSummary } = this.collapsedData;
- if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
- const improvements = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- resolvedErrors.length,
- ),
- {
- errors: resolvedErrors.length,
- },
- false,
- );
+ summary(data) {
+ const { newErrors, resolvedErrors, errorSummary, parsingInProgress } = data;
- const degradations = sprintf(
- n__(
- '%{strong_start}%{errors}%{strong_end} point',
- '%{strong_start}%{errors}%{strong_end} points',
- newErrors.length,
- ),
- { errors: newErrors.length },
- false,
- );
- return sprintf(
- s__(`ciReport|Code Quality improved on ${improvements} and degraded on ${degradations}.`),
+ if (parsingInProgress) {
+ return i18n.loading;
+ } else if (errorSummary.errored >= 1 && errorSummary.resolved >= 1) {
+ return i18n.improvementAndDegradationCopy(
+ i18n.pluralReport(resolvedErrors),
+ i18n.pluralReport(newErrors),
);
} else if (errorSummary.resolved >= 1) {
- const improvements = n__('%d point', '%d points', resolvedErrors.length);
- return sprintf(s__(`ciReport|Code Quality improved on ${improvements}.`));
+ return i18n.improvedCopy(i18n.singularReport(resolvedErrors));
} else if (errorSummary.errored >= 1) {
- const degradations = n__('%d point', '%d points', newErrors.length);
- return sprintf(s__(`ciReport|Code Quality degraded on ${degradations}.`));
+ return i18n.degradedCopy(i18n.singularReport(newErrors));
}
- return s__(`ciReport|No changes to Code Quality.`);
+ return i18n.noChanges;
},
statusIcon() {
if (this.collapsedData.errorSummary?.errored >= 1) {
@@ -59,18 +38,17 @@ export default {
},
methods: {
fetchCollapsedData() {
- return Promise.all([this.fetchReport(this.codeQuality)]).then((values) => {
+ return axios.get(this.codeQuality).then((response) => {
+ const { data = {}, status } = response;
return {
- resolvedErrors: parseCodeclimateMetrics(
- values[0].resolved_errors,
- this.blobPath.head_path,
- ),
- newErrors: parseCodeclimateMetrics(values[0].new_errors, this.blobPath.head_path),
- existingErrors: parseCodeclimateMetrics(
- values[0].existing_errors,
- this.blobPath.head_path,
- ),
- errorSummary: values[0].summary,
+ ...response,
+ data: {
+ parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
+ resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
+ newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
+ existingErrors: parseCodeclimateMetrics(data.existing_errors, this.blobPath.head_path),
+ errorSummary: data.summary,
+ },
};
});
},
@@ -81,12 +59,12 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
},
});
});
@@ -95,12 +73,16 @@ export default {
return fullData.push({
text: `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
subtext: {
- prependText: s__(`ciReport|in`),
+ prependText: i18n.prependText,
text: `${e.file_path}:${e.line}`,
href: e.urlPath,
},
icon: {
- name: SEVERITY_ICONS_EXTENSION[e.severity],
+ name: SEVERITY_ICONS_MR_WIDGET[e.severity],
+ },
+ badge: {
+ variant: 'neutral',
+ text: i18n.fixed,
},
});
});
diff --git a/app/assets/javascripts/vue_merge_request_widget/i18n.js b/app/assets/javascripts/vue_merge_request_widget/i18n.js
index 454a14faabb..5380bcae003 100644
--- a/app/assets/javascripts/vue_merge_request_widget/i18n.js
+++ b/app/assets/javascripts/vue_merge_request_widget/i18n.js
@@ -25,3 +25,10 @@ export const MERGE_TRAIN_BUTTON_TEXT = {
failed: __('Start merge train...'),
passed: __('Start merge train'),
};
+
+export const MR_WIDGET_CLOSED_REOPEN = __('Reopen');
+export const MR_WIDGET_CLOSED_REOPENING = __('Reopening...');
+export const MR_WIDGET_CLOSED_RELOADING = __('Refreshing...');
+export const MR_WIDGET_CLOSED_REOPEN_FAILURE = __(
+ 'An error occurred. Unable to reopen this merge request.',
+);
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 b96bdcb3833..00024a594dc 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
@@ -1,10 +1,10 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -15,6 +15,7 @@ import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { setFaviconOverlay } from '../lib/utils/favicon';
import Loading from './components/loading.vue';
import MrWidgetAlertMessage from './components/mr_widget_alert_message.vue';
@@ -46,18 +47,20 @@ import { STATE_MACHINE, stateToComponentMap } from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
+import getStateSubscription from './queries/get_state.subscription.graphql';
import terraformExtension from './extensions/terraform';
import accessibilityExtension from './extensions/accessibility';
import codeQualityExtension from './extensions/code_quality';
import testReportExtension from './extensions/test_report';
import ReportWidgetContainer from './components/report_widget_container.vue';
+import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
// eslint-disable-next-line @gitlab/require-i18n-strings
name: 'MRWidget',
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
Loading,
@@ -76,7 +79,7 @@ export default {
MrWidgetNothingToMerge: NothingToMergeState,
MrWidgetNotAllowed: NotAllowedState,
MrWidgetMissingBranch: MissingBranchState,
- MrWidgetReadyToMerge: () => import('./components/states/new_ready_to_merge.vue'),
+ MrWidgetReadyToMerge,
ShaMismatch,
MrWidgetChecking: CheckingState,
MrWidgetUnresolvedDiscussions: UnresolvedDiscussionsState,
@@ -108,6 +111,31 @@ export default {
this.loading = false;
}
},
+ subscribeToMore: {
+ document() {
+ return getStateSubscription;
+ },
+ skip() {
+ return !this.mr?.id || this.loading || !window.gon?.features?.realtimeMrStatusChange;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId('MergeRequest', this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.mr.setGraphqlSubscriptionData(mergeRequestMergeStatusUpdated);
+ }
+ },
+ },
},
},
mixins: [mergeRequestQueryVariablesMixin],
@@ -128,6 +156,7 @@ export default {
machineState: store?.machineValue || STATE_MACHINE.definition.initial,
loading: true,
recomputeComponentName: 0,
+ issuableId: false,
};
},
computed: {
@@ -545,6 +574,7 @@ export default {
<mr-widget-approvals v-if="shouldRenderApprovals" :mr="mr" :service="service" />
<report-widget-container>
<extensions-container v-if="hasExtensions" :mr="mr" />
+ <widget-container v-if="mr && shouldShowSecurityExtension" :mr="mr" />
<security-reports-app
v-if="shouldRenderSecurityReport && !shouldShowSecurityExtension"
:pipeline-id="mr.pipeline.id"
@@ -580,8 +610,6 @@ export default {
</mr-widget-alert-message>
</div>
- <widget-container v-if="mr" :mr="mr" />
-
<div class="mr-widget-section" data-qa-selector="mr_widget_content">
<component :is="componentName" :mr="mr" :service="service" />
<ready-to-merge
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
new file mode 100644
index 00000000000..c7b53db1221
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/get_state.subscription.graphql
@@ -0,0 +1,7 @@
+subscription getStateSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ detailedMergeStatus
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
index 54770e6579a..9b0420cc7fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.fragment.graphql
@@ -1,44 +1,11 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
fragment ReadyToMerge on Project {
id
onlyAllowMergeIfPipelineSucceeds
mergeRequestsFfOnlyEnabled
squashReadOnly
mergeRequest(iid: $iid) {
- id
- autoMergeEnabled
- shouldRemoveSourceBranch
- forceRemoveSourceBranch
- defaultMergeCommitMessage
- defaultSquashCommitMessage
- squash
- squashOnMerge
- availableAutoMergeStrategies
- hasCi
- mergeable
- mergeWhenPipelineSucceeds
- commitCount
- diffHeadSha
- userPermissions {
- canMerge
- removeSourceBranch
- updateMergeRequest
- }
- targetBranch
- mergeError
- commitsWithoutMergeCommits {
- nodes {
- id
- sha
- shortId
- title
- message
- }
- }
- headPipeline {
- id
- status
- path
- active
- }
+ ...ReadyToMergeMergeRequest
}
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
new file mode 100644
index 00000000000..8aba172e09c
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge.subscription.graphql
@@ -0,0 +1,9 @@
+#import "./ready_to_merge_merge_request.fragment.graphql"
+
+subscription readyToMergeSubscription($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ ...ReadyToMergeMergeRequest
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
new file mode 100644
index 00000000000..276e2d4d63f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/ready_to_merge_merge_request.fragment.graphql
@@ -0,0 +1,39 @@
+fragment ReadyToMergeMergeRequest on MergeRequest {
+ id
+ detailedMergeStatus
+ autoMergeEnabled
+ shouldRemoveSourceBranch
+ forceRemoveSourceBranch
+ defaultMergeCommitMessage
+ defaultSquashCommitMessage
+ squash
+ squashOnMerge
+ availableAutoMergeStrategies
+ hasCi
+ mergeable
+ mergeWhenPipelineSucceeds
+ commitCount
+ diffHeadSha
+ userPermissions {
+ canMerge
+ removeSourceBranch
+ updateMergeRequest
+ }
+ targetBranch
+ mergeError
+ commitsWithoutMergeCommits {
+ nodes {
+ id
+ sha
+ shortId
+ title
+ message
+ }
+ }
+ headPipeline {
+ id
+ status
+ path
+ active
+ }
+}
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 86ce032ea3d..85df2ea63c8 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
@@ -30,6 +30,7 @@ export default class MergeRequestStore {
this.machineValue = this.stateMachine.value;
this.mergeDetailsCollapsed = window.innerWidth < 768;
this.mergeError = data.mergeError;
+ this.id = data.id;
this.setPaths(data);
@@ -177,6 +178,7 @@ export default class MergeRequestStore {
this.updateStatusState(mergeRequest.state);
+ this.issuableId = mergeRequest.id;
this.projectArchived = project.archived;
this.onlyAllowMergeIfPipelineSucceeds = project.onlyAllowMergeIfPipelineSucceeds;
this.allowMergeOnSkippedPipeline = project.allowMergeOnSkippedPipeline;
@@ -206,6 +208,12 @@ export default class MergeRequestStore {
this.setState();
}
+ setGraphqlSubscriptionData(data) {
+ this.detailedMergeStatus = data.detailedMergeStatus;
+
+ this.setState();
+ }
+
updateStatusState(state) {
if (this.mergeRequestState !== state && badgeState.updateStatus) {
badgeState.updateStatus();
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
index 96c2ffa929c..6803d609dbc 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_details.vue
@@ -9,9 +9,9 @@ import {
GlTabs,
GlTab,
GlButton,
- GlSafeHtmlDirective,
} from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import { fetchPolicies } from '~/lib/graphql';
import { toggleContainerClasses } from '~/lib/utils/dom_utils';
@@ -41,7 +41,7 @@ export default {
reportedAtWithTool: s__('AlertManagement|Reported %{when} by %{tool}'),
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
severityLabels: SEVERITY_LEVELS,
tabsConfig: [
@@ -369,10 +369,10 @@ export default {
<alert-details-table :alert="alert" :loading="loading" :statuses="statuses" />
</gl-tab>
- <metric-images-tab
- :data-testid="$options.tabsConfig[1].id"
- :title="$options.tabsConfig[1].title"
- />
+ <gl-tab :title="$options.tabsConfig[1].title">
+ <metric-images-tab :data-testid="$options.tabsConfig[1].id" />
+ </gl-tab>
+
<gl-tab :data-testid="$options.tabsConfig[2].id" :title="$options.tabsConfig[2].title">
<div v-if="alert.notes.nodes.length > 0" class="issuable-discussion">
<ul class="notes main-notes-list timeline">
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
index 672761af1cf..8d2ef20b381 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/alert_status.vue
@@ -106,7 +106,7 @@ export default {
@keydown.esc.native="$emit('hide-dropdown')"
@hide="$emit('hide-dropdown')"
>
- <p v-if="isSidebar" class="gl-new-dropdown-header-top" data-testid="dropdown-header">
+ <p v-if="isSidebar" class="gl-dropdown-header-top" data-testid="dropdown-header">
{{ s__('AlertManagement|Assign status') }}
</p>
<div class="dropdown-content dropdown-body">
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
index 72dcc16b57a..4ec301b946b 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue
@@ -242,7 +242,7 @@ export default {
@keydown.esc.native="hideDropdown"
@hide="hideDropdown"
>
- <p class="gl-new-dropdown-header-top">
+ <p class="gl-dropdown-header-top">
{{ __('Assign To') }}
</p>
<gl-search-box-by-type v-model.trim="search" :placeholder="__('Search users')" />
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
index 832b154b312..b3ee01f3a24 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_header.vue
@@ -1,5 +1,5 @@
<script>
-import ToggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
+import ToggleSidebar from '~/sidebar/components/toggle/toggle_sidebar.vue';
import SidebarTodo from './sidebar_todo.vue';
export default {
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 6b774b2a734..3c73f42b6b1 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -1,5 +1,6 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import NoteHeader from '~/notes/components/note_header.vue';
export default {
@@ -8,7 +9,7 @@ export default {
GlIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
note: {
diff --git a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
index 33091f1ba5e..b04d5773a37 100644
--- a/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
+++ b/app/assets/javascripts/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql
@@ -8,6 +8,7 @@ mutation alertSetAssignees($fullPath: ID!, $assigneeUsernames: [String!]!, $iid:
) {
errors
issuable: alert {
+ id
iid
assignees {
nodes {
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index c6c22f9c61f..175aef59ae5 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -1,11 +1,5 @@
<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownDivider,
- GlButton,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlDropdownDivider, GlButton, GlTooltip } from '@gitlab/ui';
export default {
components: {
@@ -13,11 +7,14 @@ export default {
GlDropdownItem,
GlDropdownDivider,
GlButton,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
+ GlTooltip,
},
props: {
+ id: {
+ type: String,
+ required: false,
+ default: '',
+ },
actions: {
type: Array,
required: true,
@@ -37,6 +34,11 @@ export default {
required: false,
default: 'default',
},
+ showActionTooltip: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
hasMultipleActions() {
@@ -51,6 +53,7 @@ export default {
this.$emit('select', action.key);
},
handleClick(action, evt) {
+ this.$emit('actionClicked', { action });
return action.handle?.(evt);
},
},
@@ -58,46 +61,51 @@ export default {
</script>
<template>
- <gl-dropdown
- v-if="hasMultipleActions"
- v-gl-tooltip="selectedAction.tooltip"
- :text="selectedAction.text"
- :split-href="selectedAction.href"
- :variant="variant"
- :category="category"
- split
- data-qa-selector="action_dropdown"
- @click="handleClick(selectedAction, $event)"
- >
- <template #button-content>
- <span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
- {{ selectedAction.text }}
- </span>
- </template>
- <template v-for="(action, index) in actions">
- <gl-dropdown-item
- :key="action.key"
- is-check-item
- :is-checked="action.key === selectedAction.key"
- :secondary-text="action.secondaryText"
- :data-qa-selector="`${action.key}_menu_item`"
- :data-testid="`action_${action.key}`"
- @click="handleItemClick(action)"
- >
- <span class="gl-font-weight-bold">{{ action.text }}</span>
- </gl-dropdown-item>
- <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
- </template>
- </gl-dropdown>
- <gl-button
- v-else-if="selectedAction"
- v-gl-tooltip="selectedAction.tooltip"
- v-bind="selectedAction.attrs"
- :variant="variant"
- :category="category"
- :href="selectedAction.href"
- @click="handleClick(selectedAction, $event)"
- >
- {{ selectedAction.text }}
- </gl-button>
+ <span>
+ <gl-dropdown
+ v-if="hasMultipleActions"
+ :id="id"
+ :text="selectedAction.text"
+ :split-href="selectedAction.href"
+ :variant="variant"
+ :category="category"
+ split
+ data-qa-selector="action_dropdown"
+ @click="handleClick(selectedAction, $event)"
+ >
+ <template #button-content>
+ <span class="gl-dropdown-button-text" v-bind="selectedAction.attrs">
+ {{ selectedAction.text }}
+ </span>
+ </template>
+ <template v-for="(action, index) in actions">
+ <gl-dropdown-item
+ :key="action.key"
+ is-check-item
+ :is-checked="action.key === selectedAction.key"
+ :secondary-text="action.secondaryText"
+ :data-qa-selector="`${action.key}_menu_item`"
+ :data-testid="`action_${action.key}`"
+ @click="handleItemClick(action)"
+ >
+ <span class="gl-font-weight-bold">{{ action.text }}</span>
+ </gl-dropdown-item>
+ <gl-dropdown-divider v-if="index != actions.length - 1" :key="action.key + '_divider'" />
+ </template>
+ </gl-dropdown>
+ <gl-button
+ v-else-if="selectedAction"
+ :id="id"
+ v-bind="selectedAction.attrs"
+ :variant="variant"
+ :category="category"
+ :href="selectedAction.href"
+ @click="handleClick(selectedAction, $event)"
+ >
+ {{ selectedAction.text }}
+ </gl-button>
+ <gl-tooltip v-if="selectedAction.tooltip && showActionTooltip" :target="id">
+ {{ selectedAction.tooltip }}
+ </gl-tooltip>
+ </span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index f5d8811e83c..cb38b3e13bb 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -1,6 +1,7 @@
<script>
-import { GlIcon, GlButton, GlTooltipDirective, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
import { groupBy } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import EmojiPicker from '~/emoji/components/picker.vue';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -17,7 +18,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -158,10 +159,7 @@ export default {
return;
}
- // 100 and 1234 emoji are a number. Callback for v-for click sends it as a string
- const parsedName = /^[0-9]+$/.test(awardName) ? Number(awardName) : awardName;
-
- this.$emit('award', parsedName);
+ this.$emit('award', awardName);
if (document.activeElement) document.activeElement.blur();
},
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 ed0eb9cc0b8..49181bb847d 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
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { handleBlobRichViewer } from '~/blob/viewer';
import MarkdownFieldView from '~/vue_shared/components/markdown/field_view.vue';
import ViewerMixin from './mixins';
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
index 0117c06c3d5..c7a76af7f74 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue
@@ -1,5 +1,6 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { HIGHLIGHT_CLASS_NAME } from './constants';
import ViewerMixin from './mixins';
@@ -9,7 +10,7 @@ export default {
GlIcon,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [ViewerMixin],
inject: ['blobHash'],
diff --git a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
index 65b08b608e8..352d03befc3 100644
--- a/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
+++ b/app/assets/javascripts/vue_shared/components/code_block_highlighted.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import CodeBlock from './code_block.vue';
@@ -7,7 +7,7 @@ import CodeBlock from './code_block.vue';
export default {
name: 'CodeBlockHighlighted',
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
components: {
CodeBlock,
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 7a982bc035a..d0a634d8e54 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
@@ -1,12 +1,6 @@
<script>
-import {
- GlAlert,
- GlModal,
- GlFormGroup,
- GlFormInput,
- GlSafeHtmlDirective as SafeHtml,
- GlSprintf,
-} from '@gitlab/ui';
+import { GlAlert, GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import {
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_TITLE,
diff --git a/app/assets/javascripts/vue_shared/components/confirm_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
index 72504e5bc50..664c3578785 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_modal.vue
@@ -1,6 +1,7 @@
<script>
-import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { uniqueId } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import csrf from '~/lib/utils/csrf';
import eventHub, { EVENT_OPEN_CONFIRM_MODAL } from './confirm_modal_eventhub';
import DomElementListener from './dom_element_listener.vue';
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
index 3ecfac10f9c..00d12654ee3 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/markdown_viewer.vue
@@ -1,10 +1,10 @@
<script>
-import { GlSkeletonLoader, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlSkeletonLoader } from '@gitlab/ui';
import { forEach, escape } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
const { CancelToken } = axios;
let axiosSource;
@@ -96,7 +96,7 @@ export default {
this.isLoading = false;
this.$nextTick(() => {
- $(this.$refs.markdownPreview).renderGFM();
+ renderGFM(this.$refs.markdownPreview);
});
})
.catch(() => {
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
index 181c1b89e31..d8a2789a419 100644
--- 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
@@ -265,7 +265,6 @@ export default {
<gl-dropdown-item
v-for="(option, index) in options"
:key="index"
- data-qa-selector="quick_range_item"
:active="isOptionActive(option)"
active-class="active"
@click="setQuickRange(option)"
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 0621ec14c6c..8395bc89790 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -1,5 +1,6 @@
<script>
-import { GlAlert, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlAlert } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'DismissibleAlert',
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 755ce004aa9..993b4c11c0e 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
@@ -8,52 +8,44 @@ export const FILTER_ANY = 'Any';
export const FILTER_CURRENT = 'Current';
export const FILTER_UPCOMING = 'Upcoming';
export const FILTER_STARTED = 'Started';
-export const FILTER_NONE_ANY = [FILTER_NONE, FILTER_ANY];
+
+export const FILTERS_NONE_ANY = [FILTER_NONE, FILTER_ANY];
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
-export const OPERATOR_IS_NOT = '!=';
-export const OPERATOR_IS_NOT_TEXT = __('is not one of');
+export const OPERATOR_NOT = '!=';
+export const OPERATOR_NOT_TEXT = __('is not one of');
export const OPERATOR_OR = '||';
export const OPERATOR_OR_TEXT = __('is one of');
-export const OPERATOR_IS_ONLY = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
-export const OPERATOR_IS_NOT_ONLY = [{ value: OPERATOR_IS_NOT, description: OPERATOR_IS_NOT_TEXT }];
-export const OPERATOR_OR_ONLY = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
-export const OPERATOR_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
-export const OPERATOR_IS_NOT_OR = [
- ...OPERATOR_IS_ONLY,
- ...OPERATOR_IS_NOT_ONLY,
- ...OPERATOR_OR_ONLY,
-];
-
-export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
-export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
-export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY];
+export const OPERATORS_IS = [{ value: OPERATOR_IS, description: OPERATOR_IS_TEXT }];
+export const OPERATORS_NOT = [{ value: OPERATOR_NOT, description: OPERATOR_NOT_TEXT }];
+export const OPERATORS_OR = [{ value: OPERATOR_OR, description: OPERATOR_OR_TEXT }];
+export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
+export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
-export const DEFAULT_MILESTONE_UPCOMING = {
+export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
+export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
+export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') };
+export const OPTION_STARTED = { value: FILTER_STARTED, text: __('Started'), title: __('Started') };
+export const OPTION_UPCOMING = {
value: FILTER_UPCOMING,
text: __('Upcoming'),
title: __('Upcoming'),
};
-export const DEFAULT_MILESTONE_STARTED = {
- value: FILTER_STARTED,
- text: __('Started'),
- title: __('Started'),
-};
-export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([
- DEFAULT_MILESTONE_UPCOMING,
- DEFAULT_MILESTONE_STARTED,
-]);
-export const SortDirection = {
+export const OPTIONS_NONE_ANY = [OPTION_NONE, OPTION_ANY];
+
+export const DEFAULT_MILESTONES = OPTIONS_NONE_ANY.concat([OPTION_UPCOMING, OPTION_STARTED]);
+
+export const SORT_DIRECTION = {
descending: 'descending',
ascending: 'ascending',
};
-export const FILTERED_SEARCH_LABELS = 'labels';
export const FILTERED_SEARCH_TERM = 'filtered-search-term';
+export const TOKEN_TITLE_APPROVED_BY = __('Approved-By');
export const TOKEN_TITLE_ASSIGNEE = s__('SearchToken|Assignee');
export const TOKEN_TITLE_AUTHOR = __('Author');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
@@ -63,11 +55,14 @@ export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
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_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
+export const TOKEN_TYPE_APPROVED_BY = 'approved-by';
export const TOKEN_TYPE_ASSIGNEE = 'assignee';
export const TOKEN_TYPE_AUTHOR = 'author';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
@@ -84,5 +79,11 @@ export const TOKEN_TYPE_MILESTONE = 'milestone';
export const TOKEN_TYPE_MY_REACTION = 'my-reaction';
export const TOKEN_TYPE_ORGANIZATION = 'organization';
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_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
export const TOKEN_TYPE_WEIGHT = 'weight';
+
+export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
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 0d0787e7033..34f64dddc41 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
@@ -15,7 +15,7 @@ import RecentSearchesStore from '~/filtered_search/stores/recent_searches_store'
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { SortDirection } from './constants';
+import { SORT_DIRECTION } from './constants';
import { filterEmptySearchTerm, stripQuotes, uniqueTokens } from './filtered_search_utils';
export default {
@@ -107,7 +107,7 @@ export default {
recentSearches: [],
filterValue: this.initialFilterValue,
selectedSortOption: this.sortOptions[0],
- selectedSortDirection: SortDirection.descending,
+ selectedSortDirection: SORT_DIRECTION.descending,
};
},
computed: {
@@ -130,12 +130,12 @@ export default {
);
},
sortDirectionIcon() {
- return this.selectedSortDirection === SortDirection.ascending
+ return this.selectedSortDirection === SORT_DIRECTION.ascending
? 'sort-lowest'
: 'sort-highest';
},
sortDirectionTooltip() {
- return this.selectedSortDirection === SortDirection.ascending
+ return this.selectedSortDirection === SORT_DIRECTION.ascending
? __('Sort direction: Ascending')
: __('Sort direction: Descending');
},
@@ -267,9 +267,9 @@ export default {
},
handleSortDirectionClick() {
this.selectedSortDirection =
- this.selectedSortDirection === SortDirection.ascending
- ? SortDirection.descending
- : SortDirection.ascending;
+ this.selectedSortDirection === SORT_DIRECTION.ascending
+ ? SORT_DIRECTION.descending
+ : SORT_DIRECTION.ascending;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 6a4ff07c999..b0fa3e4c27e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -9,7 +9,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
-import { DEBOUNCE_DELAY, FILTER_NONE_ANY, OPERATOR_IS_NOT } from '../constants';
+import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT } from '../constants';
import {
getRecentlyUsedSuggestions,
setTokenValueToRecentlyUsed,
@@ -100,9 +100,9 @@ export default {
return this.getActiveTokenValue(this.suggestions, this.value.data);
},
availableDefaultSuggestions() {
- if (this.value.operator === OPERATOR_IS_NOT) {
+ if (this.value.operator === OPERATOR_NOT) {
return this.defaultSuggestions.filter(
- (suggestion) => !FILTER_NONE_ANY.includes(suggestion.value),
+ (suggestion) => !FILTERS_NONE_ANY.includes(suggestion.value),
);
}
return this.defaultSuggestions;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
index d34cfb922a9..e0fa06c159e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_contact_token.vue
@@ -8,7 +8,7 @@ import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmContactsQuery from '../queries/search_crm_contacts.query.graphql';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -39,7 +39,7 @@ export default {
},
computed: {
defaultContacts() {
- return this.config.defaultContacts || DEFAULT_NONE_ANY;
+ return this.config.defaultContacts || OPTIONS_NONE_ANY;
},
namespace() {
return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
index c7c9350ee93..3f030c8698c 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/crm_organization_token.vue
@@ -8,7 +8,7 @@ import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import searchCrmOrganizationsQuery from '../queries/search_crm_organizations.query.graphql';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -39,7 +39,7 @@ export default {
},
computed: {
defaultOrganizations() {
- return this.config.defaultOrganizations || DEFAULT_NONE_ANY;
+ return this.config.defaultOrganizations || OPTIONS_NONE_ANY;
},
namespace() {
return this.config.isProject ? ITEM_TYPE.PROJECT : ITEM_TYPE.GROUP;
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
index 929823f7308..74905dc2ae0 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
export default {
@@ -33,7 +33,7 @@ export default {
},
computed: {
defaultEmojis() {
- return this.config.defaultEmojis || DEFAULT_NONE_ANY;
+ return this.config.defaultEmojis || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
index bce0c11aafd..71c50ef292a 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue
@@ -5,7 +5,7 @@ import { createAlert } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
import BaseToken from './base_token.vue';
@@ -38,7 +38,7 @@ export default {
},
computed: {
defaultLabels() {
- return this.config.defaultLabels || DEFAULT_NONE_ANY;
+ return this.config.defaultLabels || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
index 59701b4959e..6d681aab3ca 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/release_token.vue
@@ -3,7 +3,7 @@ import { GlFilteredSearchSuggestion } from '@gitlab/ui';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
export default {
components: {
@@ -32,7 +32,7 @@ export default {
},
computed: {
defaultReleases() {
- return this.config.defaultReleases || DEFAULT_NONE_ANY;
+ return this.config.defaultReleases || OPTIONS_NONE_ANY;
},
},
methods: {
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index 7c184a3c391..28e65c1185f 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -4,7 +4,7 @@ import { compact } from 'lodash';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
-import { DEFAULT_NONE_ANY } from '../constants';
+import { OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -30,30 +30,30 @@ export default {
},
data() {
return {
- authors: this.config.initialAuthors || [],
+ users: this.config.initialUsers || [],
loading: false,
};
},
computed: {
- defaultAuthors() {
- return this.config.defaultAuthors || DEFAULT_NONE_ANY;
+ defaultUsers() {
+ return this.config.defaultUsers || OPTIONS_NONE_ANY;
},
- preloadedAuthors() {
- return this.config.preloadedAuthors || [];
+ preloadedUsers() {
+ return this.config.preloadedUsers || [];
},
},
methods: {
- getActiveAuthor(authors, data) {
- return authors.find((author) => author.username.toLowerCase() === data.toLowerCase());
+ getActiveUser(users, data) {
+ return users.find((user) => user.username.toLowerCase() === data.toLowerCase());
},
- getAvatarUrl(author) {
- return author.avatarUrl || author.avatar_url;
+ getAvatarUrl(user) {
+ return user.avatarUrl || user.avatar_url;
},
- fetchAuthors(searchTerm) {
+ fetchUsers(searchTerm) {
this.loading = true;
const fetchPromise = this.config.fetchPath
- ? this.config.fetchAuthors(this.config.fetchPath, searchTerm)
- : this.config.fetchAuthors(searchTerm);
+ ? this.config.fetchUsers(this.config.fetchPath, searchTerm)
+ : this.config.fetchUsers(searchTerm);
fetchPromise
.then((res) => {
@@ -62,7 +62,7 @@ export default {
// return response differently
// TODO: rm when completed https://gitlab.com/gitlab-org/gitlab/-/issues/345756
- this.authors = Array.isArray(res) ? compact(res) : compact(res.data);
+ this.users = Array.isArray(res) ? compact(res) : compact(res.data);
})
.catch(() =>
createAlert({
@@ -83,12 +83,12 @@ export default {
:value="value"
:active="active"
:suggestions-loading="loading"
- :suggestions="authors"
- :get-active-token-value="getActiveAuthor"
- :default-suggestions="defaultAuthors"
- :preloaded-suggestions="preloadedAuthors"
+ :suggestions="users"
+ :get-active-token-value="getActiveUser"
+ :default-suggestions="defaultUsers"
+ :preloaded-suggestions="preloadedUsers"
v-bind="$attrs"
- @fetch-suggestions="fetchAuthors"
+ @fetch-suggestions="fetchUsers"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@@ -102,15 +102,15 @@ export default {
</template>
<template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="author in suggestions"
- :key="author.username"
- :value="author.username"
+ v-for="user in suggestions"
+ :key="user.username"
+ :value="user.username"
>
<div class="gl-display-flex">
- <gl-avatar :size="32" :src="getAvatarUrl(author)" />
+ <gl-avatar :size="32" :src="getAvatarUrl(user)" />
<div>
- <div>{{ author.name }}</div>
- <div>@{{ author.username }}</div>
+ <div>{{ user.name }}</div>
+ <div>@{{ user.username }}</div>
</div>
</div>
</gl-filtered-search-suggestion>
diff --git a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
index 1de6c0121bc..5db723e1e5a 100644
--- a/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/group_select/group_select.vue
@@ -1,6 +1,6 @@
<script>
import { debounce } from 'lodash';
-import { GlListbox } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
import { __ } from '~/locale';
@@ -18,7 +18,7 @@ const MINIMUM_QUERY_LENGTH = 3;
export default {
components: {
- GlListbox,
+ GlCollapsibleListbox,
},
props: {
inputName: {
@@ -167,7 +167,7 @@ export default {
<template>
<div>
- <gl-listbox
+ <gl-collapsible-listbox
ref="listbox"
v-model="selected"
:header-text="$options.i18n.selectGroup"
@@ -188,7 +188,7 @@ export default {
</div>
<div class="gl-text-gray-300">{{ item.full_path }}</div>
</template>
- </gl-listbox>
+ </gl-collapsible-listbox>
<div class="flash-container"></div>
<input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
</div>
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index 96f7427dda1..3c4ae08d2f7 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,12 +1,6 @@
<script>
-import {
- GlTooltipDirective,
- GlButton,
- GlSafeHtmlDirective,
- GlAvatarLink,
- GlAvatarLabeled,
- GlTooltip,
-} from '@gitlab/ui';
+import { GlTooltipDirective, GlButton, GlAvatarLink, GlAvatarLabeled, GlTooltip } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
@@ -31,7 +25,7 @@ export default {
},
directives: {
GlTooltip: GlTooltipDirective,
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
EMOJI_REF: 'EMOJI_REF',
props: {
diff --git a/app/assets/javascripts/vue_shared/components/help_popover.vue b/app/assets/javascripts/vue_shared/components/help_popover.vue
index f349aa78bac..92d468cf970 100644
--- a/app/assets/javascripts/vue_shared/components/help_popover.vue
+++ b/app/assets/javascripts/vue_shared/components/help_popover.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlPopover, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlButton, GlPopover } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
/**
* Render a button with a question mark icon
@@ -12,7 +13,7 @@ export default {
GlPopover,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
options: {
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js
new file mode 100644
index 00000000000..4106de371cb
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.stories.js
@@ -0,0 +1,26 @@
+import ListboxInput from './listbox_input.vue';
+
+export default {
+ component: ListboxInput,
+ title: 'vue_shared/listbox_input',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ListboxInput },
+ data() {
+ return { selected: null };
+ },
+ props: Object.keys(argTypes),
+ template: '<listbox-input v-model="selected" v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ name: 'input_name',
+ defaultToggleText: 'Select an option',
+ items: [
+ { text: 'Option 1', value: '1' },
+ { text: 'Option 2', value: '2' },
+ { text: 'Option 3', value: '3' },
+ ],
+};
diff --git a/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
new file mode 100644
index 00000000000..b1809e6a9f3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/listbox_input/listbox_input.vue
@@ -0,0 +1,110 @@
+<script>
+import { GlListbox } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const MIN_ITEMS_COUNT_FOR_SEARCHING = 20;
+
+export default {
+ i18n: {
+ noResultsText: __('No results found'),
+ },
+ components: {
+ GlListbox,
+ },
+ model: GlListbox.model,
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ defaultToggleText: {
+ type: String,
+ required: true,
+ },
+ selected: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ items: {
+ type: GlListbox.props.items.type,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ searchString: '',
+ };
+ },
+ computed: {
+ allOptions() {
+ const allOptions = [];
+
+ const getOptions = (options) => {
+ for (let i = 0; i < options.length; i += 1) {
+ const option = options[i];
+ if (option.options) {
+ getOptions(option.options);
+ } else {
+ allOptions.push(option);
+ }
+ }
+ };
+ getOptions(this.items);
+
+ return allOptions;
+ },
+ isGrouped() {
+ return this.items.some((item) => item.options !== undefined);
+ },
+ isSearchable() {
+ return this.allOptions.length > MIN_ITEMS_COUNT_FOR_SEARCHING;
+ },
+ filteredItems() {
+ const searchString = this.searchString.toLowerCase();
+
+ if (!searchString) {
+ return this.items;
+ }
+
+ if (this.isGrouped) {
+ return this.items
+ .map(({ text, options }) => {
+ return {
+ text,
+ options: options.filter((option) => option.text.toLowerCase().includes(searchString)),
+ };
+ })
+ .filter(({ options }) => options.length);
+ }
+
+ return this.items.filter((item) => item.text.toLowerCase().includes(searchString));
+ },
+ toggleText() {
+ return this.selected
+ ? this.allOptions.find((option) => option.value === this.selected).text
+ : this.defaultToggleText;
+ },
+ },
+ methods: {
+ search(searchString) {
+ this.searchString = searchString;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-listbox
+ :selected="selected"
+ :toggle-text="toggleText"
+ :items="filteredItems"
+ :searchable="isSearchable"
+ :no-results-text="$options.i18n.noResultsText"
+ @search="search"
+ @select="$emit($options.model.event, $event)"
+ />
+ <input ref="input" type="hidden" :name="name" :value="selected" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index caec49c557a..f51ec715678 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -74,7 +74,7 @@ export default {
@submit="onApply"
/>
<gl-button
- class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right"
+ class="gl-w-auto! gl-mt-3 gl-text-center! gl-transition-medium! float-right"
category="primary"
variant="confirm"
data-qa-selector="commit_with_custom_message_button"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 657e4498b53..b5f2602af5e 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -1,15 +1,16 @@
<script>
-import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui';
+import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
import { debounce, unescape } from 'lodash';
import { createAlert } from '~/flash';
import GLForm from '~/gl_form';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
import { __, sprintf } from '~/locale';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import MarkdownHeader from './header.vue';
import MarkdownToolbar from './toolbar.vue';
@@ -25,7 +26,7 @@ export default {
Suggestions,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagsMixin()],
props: {
@@ -313,7 +314,9 @@ export default {
this.markdownPreview = data.body || __('Nothing to preview.');
this.$nextTick()
- .then(() => $(this.$refs['markdown-preview']).renderGFM())
+ .then(() => {
+ renderGFM(this.$refs['markdown-preview']);
+ })
.catch(() =>
createAlert({
message: __('Error rendering Markdown preview'),
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 d77123371f2..84d40db07bb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field_view.vue
@@ -1,15 +1,9 @@
<script>
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
mounted() {
- this.renderGFM();
- },
- methods: {
- renderGFM() {
- $(this.$el).renderGFM();
- },
+ 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 c0712e46613..d01eae0308f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -82,6 +82,11 @@ export default {
required: false,
default: false,
},
+ useBottomToolbar: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -197,6 +202,7 @@ export default {
:uploads-path="uploadsPath"
:markdown="value"
:autofocus="contentEditorAutofocused"
+ :use-bottom-toolbar="useBottomToolbar"
@initialized="setEditorAsAutofocused"
@change="updateMarkdownFromContentEditor"
@loading="disableSwitchEditingControl"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
index a04f8616acb..0b598d3acaf 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'SuggestionDiffRow',
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 30d72332c90..c307601e670 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,6 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { createAlert } from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js
index 03bd64e2a57..03bd64e2a57 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/makrdown_drawer.stories.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.stories.js
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
index a4b509f8656..379f22fdc6f 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/markdown_drawer.vue
@@ -1,9 +1,9 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml, GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlDrawer, GlAlert, GlSkeletonLoader } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import { contentTop } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import { getRenderedMarkdown } from './utils/fetch';
export const cache = {};
@@ -34,13 +34,9 @@ export default {
title: '',
body: null,
open: false,
+ drawerTop: '0px',
};
},
- computed: {
- drawerOffsetTop() {
- return `${contentTop()}px`;
- },
- },
watch: {
documentPath: {
immediate: true,
@@ -76,18 +72,23 @@ export default {
cache[this.documentPath] = { title, body };
}
},
+ getDrawerTop() {
+ this.drawerTop = `${contentTop()}px`;
+ },
renderGLFM() {
this.$nextTick(() => {
- $(this.$refs['content-element']).renderGFM();
+ renderGFM(this.$refs['content-element']);
});
},
closeDrawer() {
this.open = false;
},
toggleDrawer() {
+ this.getDrawerTop();
this.open = !this.open;
},
openDrawer() {
+ this.getDrawerTop();
this.open = true;
},
},
@@ -97,7 +98,7 @@ export default {
};
</script>
<template>
- <gl-drawer :header-height="drawerOffsetTop" :open="open" header-sticky @close="closeDrawer">
+ <gl-drawer :header-height="drawerTop" :open="open" header-sticky @close="closeDrawer">
<template #title>
<h4 data-testid="title-element" class="gl-m-0">{{ title }}</h4>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
index 7c8e1bc160a..27237f2f16b 100644
--- a/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
+++ b/app/assets/javascripts/vue_shared/components/markdown_drawer/utils/fetch.js
@@ -16,7 +16,7 @@ export const getRenderedMarkdown = (documentPath) => {
return axios
.get(helpPagePath(documentPath))
.then(({ data }) => {
- const { body, title } = splitDocument(data.html);
+ const { body, title } = splitDocument(data);
return {
body,
title,
diff --git a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
index e23721da223..2cadc87eca3 100644
--- a/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
+++ b/app/assets/javascripts/vue_shared/components/metric_images/metric_images_tab.vue
@@ -1,5 +1,5 @@
<script>
-import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal, GlTab } from '@gitlab/ui';
+import { GlFormGroup, GlFormInput, GlLoadingIcon, GlModal } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
import { __, s__ } from '~/locale';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
@@ -11,7 +11,6 @@ export default {
GlFormInput,
GlLoadingIcon,
GlModal,
- GlTab,
MetricImagesTable,
UploadDropzone,
},
@@ -82,7 +81,7 @@ export default {
</script>
<template>
- <gl-tab :title="s__('Incident|Metrics')" data-testid="metrics-tab">
+ <div>
<div v-if="isLoadingMetricImages">
<gl-loading-icon class="gl-p-5" size="sm" />
</div>
@@ -117,5 +116,5 @@ export default {
:drop-description-message="$options.i18n.dropDescription"
@change="openMetricDialog"
/>
- </gl-tab>
+ </div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
index cf34a60c363..748d6082abd 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -16,8 +16,9 @@
* :note="{body: 'This is a note'}"
* />
*/
-import { GlSafeHtmlDirective as SafeHtml, GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 1ae5045b34f..1cbbdf0deb0 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -16,22 +16,17 @@
* }"
* />
*/
-import {
- GlButton,
- GlSkeletonLoader,
- GlTooltipDirective,
- GlIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import { mapGetters, mapActions, mapState } from 'vuex';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
-import '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import NoteHeader from '~/notes/components/note_header.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { spriteIcon } from '~/lib/utils/common_utils';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
import TimelineEntryItem from './timeline_entry_item.vue';
const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
@@ -94,7 +89,7 @@ export default {
},
},
mounted() {
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
},
methods: {
...mapActions(['fetchDescriptionVersion', 'softDeleteDescriptionVersion']),
@@ -205,7 +200,7 @@ export default {
<tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
<td
:class="line.type"
- class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0!"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
>
{{ line.old_line }}
</td>
@@ -217,7 +212,7 @@ export default {
</td>
<td
:class="line.type"
- class="line_content gl-display-table-cell!"
+ class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
></td>
</tr>
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 867222279b2..57e3a97244e 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
@@ -1,22 +1,19 @@
<script>
-import {
- GlAlert,
- GlBadge,
- GlPagination,
- GlTab,
- GlTabs,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlAlert, GlBadge, GlPagination, GlTab, GlTabs } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Api from '~/api';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import {
- OPERATOR_IS_ONLY,
+ FILTERED_SEARCH_TERM,
+ OPERATORS_IS,
TOKEN_TITLE_ASSIGNEE,
TOKEN_TITLE_AUTHOR,
+ TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_AUTHOR,
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import { initialPaginationState, defaultI18n, defaultPageSize } from './constants';
import { isAny } from './utils';
@@ -95,7 +92,7 @@ export default {
filterSearchTokens: {
type: Array,
required: false,
- default: () => ['author_username', 'assignee_username'],
+ default: () => [TOKEN_TYPE_AUTHOR, TOKEN_TYPE_ASSIGNEE],
},
},
data() {
@@ -113,26 +110,26 @@ export default {
defaultTokens() {
return [
{
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
icon: 'user',
title: TOKEN_TITLE_AUTHOR,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: this.projectPath,
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
},
{
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
unique: true,
symbol: '@',
- token: AuthorToken,
- operators: OPERATOR_IS_ONLY,
+ token: UserToken,
+ operators: OPERATORS_IS,
fetchPath: this.projectPath,
- fetchAuthors: Api.projectUsers.bind(Api),
+ fetchUsers: Api.projectUsers.bind(Api),
},
];
},
@@ -144,14 +141,14 @@ export default {
if (this.authorUsername) {
value.push({
- type: 'author_username',
+ type: TOKEN_TYPE_AUTHOR,
value: { data: this.authorUsername },
});
}
if (this.assigneeUsername) {
value.push({
- type: 'assignee_username',
+ type: TOKEN_TYPE_ASSIGNEE,
value: { data: this.assigneeUsername },
});
}
@@ -226,13 +223,13 @@ export default {
filters.forEach((filter) => {
if (typeof filter === 'object') {
switch (filter.type) {
- case 'author_username':
+ case TOKEN_TYPE_AUTHOR:
filterParams.authorUsername = isAny(filter.value.data);
break;
- case 'assignee_username':
+ case TOKEN_TYPE_ASSIGNEE:
filterParams.assigneeUsername = isAny(filter.value.data);
break;
- case 'filtered-search-term':
+ case FILTERED_SEARCH_TERM:
if (filter.value.data !== '') filterParams.search = filter.value.data;
break;
default:
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
index 66643ff4026..16bc8070dc1 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue
@@ -1,9 +1,10 @@
<script>
-import { GlButton, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlIcon } from '@gitlab/ui';
import { isString } from 'lodash';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
index 8c9c7c63db1..c990baaa2f3 100644
--- a/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/registry_search.vue
@@ -1,7 +1,7 @@
<script>
import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
-import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import { SORT_DIRECTION_UI } from '~/search/sort/constants';
+import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
const ASCENDING_ORDER = 'asc';
const DESCENDING_ORDER = 'desc';
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
deleted file mode 100644
index 465ee9aa0d4..00000000000
--- a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import TodoButton from './todo_button.vue';
-
-export default {
- component: TodoButton,
- title: 'vue_shared/sidebar/todo_toggle/todo_button',
-};
-
-const Template = (args, { argTypes }) => ({
- components: { TodoButton },
- props: Object.keys(argTypes),
- template: '<todo-button v-bind="$props" v-on="$props" />',
-});
-
-export const Default = Template.bind({});
-Default.argTypes = {
- isTodo: {
- description: 'True if to-do is unresolved (i.e. not "done")',
- control: { type: 'boolean' },
- },
- click: { action: 'clicked' },
-};
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
index a2d8b7cbd15..28a16cd846a 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk.vue
@@ -1,6 +1,6 @@
<script>
-import { GlIntersectionObserver, GlSafeHtmlDirective } from '@gitlab/ui';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import LineHighlighter from '~/blob/line_highlighter';
import ChunkLine from './chunk_line.vue';
/*
@@ -20,9 +20,6 @@ export default {
ChunkLine,
GlIntersectionObserver,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
props: {
isFirstChunk: {
type: Boolean,
@@ -84,12 +81,14 @@ export default {
return;
}
- window.requestIdleCallback(() => {
+ window.requestIdleCallback(async () => {
this.isLoading = false;
const { hash } = this.$route;
if (hash && this.totalChunks > 0 && this.totalChunks === this.chunkIndex + 1) {
// when the last chunk is loaded scroll to the hash
- scrollToElement(hash, { behavior: 'auto' });
+ await this.$nextTick();
+ const lineHighlighter = new LineHighlighter({ scrollBehavior: 'auto' });
+ lineHighlighter.highlightHash(hash);
}
});
},
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
index 0bf19f83d86..ce6741f33b1 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_line.vue
@@ -1,11 +1,11 @@
<script>
-import { GlSafeHtmlDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getPageParamValue, getPageSearchString } from '~/blob/utils';
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [glFeatureFlagMixin()],
props: {
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
index fca2616f069..cd15916851c 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/link_dependencies.js
@@ -4,6 +4,7 @@ import godepsJsonLinker from './utils/godeps_json_linker';
import gemfileLinker from './utils/gemfile_linker';
import podspecJsonLinker from './utils/podspec_json_linker';
import composerJsonLinker from './utils/composer_json_linker';
+import goSumLinker from './utils/go_sum_linker';
const DEPENDENCY_LINKERS = {
package_json: packageJsonLinker,
@@ -12,6 +13,7 @@ const DEPENDENCY_LINKERS = {
gemfile: gemfileLinker,
podspec_json: podspecJsonLinker,
composer_json: composerJsonLinker,
+ go_sum: goSumLinker,
};
/**
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js
new file mode 100644
index 00000000000..b290dfa78b9
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/plugins/utils/go_sum_linker.js
@@ -0,0 +1,34 @@
+import { createLink } from './dependency_linker_util';
+
+const openTag = '<span class="">';
+const closeTag = '</span>';
+const TAG_URL = 'https://sum.golang.org/lookup/';
+const GO_PACKAGE_URL = 'https://pkg.go.dev/';
+
+const DEPENDENCY_REGEX = new RegExp(
+ /*
+ * Detects dependencies inside of content that is highlighted by Highlight.js
+ * Example: '<span class="">cloud.google.com/Go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o=</span>'
+ * Group 1 (packagePath): 'cloud.google.com/Go/bigquery'
+ * Group 2 (version): 'v1.0.1/go.mod'
+ * Group 3 (base64url): 'i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o='
+ */
+ `${openTag}(.*) (v.*) h1:(.*)${closeTag}`,
+ 'gm',
+);
+
+const handleReplace = (packagePath, version, tag) => {
+ const lowercasePath = packagePath.toLowerCase();
+ const packageHref = `${GO_PACKAGE_URL}${lowercasePath}`;
+ const packageLink = createLink(packageHref, packagePath);
+ const tagHref = `${TAG_URL}${lowercasePath}@${version.split('/go.mod')[0]}`;
+ const tagLink = createLink(tagHref, tag);
+
+ return `${openTag}${packageLink} ${version} h1:${tagLink}${closeTag}`;
+};
+
+export default (result) => {
+ return result.value.replace(DEPENDENCY_REGEX, (_, packagePath, version, tag) =>
+ handleReplace(packagePath, version, tag),
+ );
+};
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 f621a23734a..0cfee93ce5d 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
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective, GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import LineHighlighter from '~/blob/line_highlighter';
import eventHub from '~/notes/event_hub';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
@@ -28,9 +28,6 @@ export default {
GlLoadingIcon,
Chunk,
},
- directives: {
- SafeHtml: GlSafeHtmlDirective,
- },
mixins: [Tracking.mixin()],
props: {
blob: {
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 80c1fcbacfa..d06bc7b8f98 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
@@ -4,11 +4,11 @@ import {
GlLink,
GlSkeletonLoader,
GlIcon,
- GlSafeHtmlDirective,
GlSprintf,
GlButton,
GlAvatarLabeled,
} from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { glEmojiTag } from '~/emoji';
import { createAlert } from '~/flash';
import { followUser, unfollowUser } from '~/rest_api';
@@ -44,7 +44,7 @@ export default {
GlAvatarLabeled,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
mixins: [Tracking.mixin()],
props: {
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 6d179b3dc92..383dc27ea5e 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,14 +1,16 @@
<script>
-import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlLink, GlPopover } from '@gitlab/ui';
import { s__, __ } from '~/locale';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-const KEY_EDIT = 'edit';
-const KEY_WEB_IDE = 'webide';
-const KEY_GITPOD = 'gitpod';
-const KEY_PIPELINE_EDITOR = 'pipeline_editor';
+export const KEY_EDIT = 'edit';
+export const KEY_WEB_IDE = 'webide';
+export const KEY_GITPOD = 'gitpod';
+export const KEY_PIPELINE_EDITOR = 'pipeline_editor';
export const i18n = {
modal: {
@@ -25,6 +27,9 @@ export const i18n = {
),
};
+export const PREFERRED_EDITOR_KEY = 'gl-web-ide-button-selected';
+export const PREFERRED_EDITOR_RESET_KEY = 'gl-web-ide-button-selected-reset';
+
export default {
components: {
ActionsButton,
@@ -32,9 +37,12 @@ export default {
GlModal,
GlSprintf,
GlLink,
+ GlPopover,
ConfirmForkModal,
+ UserCalloutDismisser,
},
i18n,
+ mixins: [glFeatureFlagsMixin()],
props: {
isFork: {
type: Boolean,
@@ -131,6 +139,11 @@ export default {
required: false,
default: '',
},
+ webIdePromoPopoverImg: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -296,6 +309,12 @@ export default {
},
};
},
+ displayVscodeWebIdeCallout() {
+ return this.glFeatures.vscodeWebIde && !this.showEditButton;
+ },
+ },
+ mounted() {
+ this.resetPreferredEditor();
},
methods: {
select(key) {
@@ -304,41 +323,109 @@ export default {
showModal(dataKey) {
this[dataKey] = true;
},
+ resetPreferredEditor() {
+ if (!this.glFeatures.vscodeWebIde || this.showEditButton) {
+ return;
+ }
+
+ if (localStorage.getItem(PREFERRED_EDITOR_RESET_KEY) === 'true') {
+ return;
+ }
+
+ localStorage.setItem(PREFERRED_EDITOR_KEY, KEY_WEB_IDE);
+ localStorage.setItem(PREFERRED_EDITOR_RESET_KEY, true);
+
+ this.select(KEY_WEB_IDE);
+ },
+ dismissCalloutOnActionClicked(dismiss) {
+ if (this.displayVscodeWebIdeCallout) {
+ dismiss();
+ }
+ },
},
+ webIdeButtonId: 'web-ide-link',
+ PREFERRED_EDITOR_KEY,
};
</script>
<template>
- <div class="gl-sm-ml-3">
- <actions-button
- :actions="actions"
- :selected-key="selection"
- :variant="isBlob ? 'confirm' : 'default'"
- :category="isBlob ? 'primary' : 'secondary'"
- @select="select"
- />
- <local-storage-sync
- storage-key="gl-web-ide-button-selected"
- :value="selection"
- as-string
- @input="select"
- />
- <gl-modal
- v-if="computedShowGitpodButton && !gitpodEnabled"
- v-model="showEnableGitpodModal"
- v-bind="enableGitpodModalProps"
- >
- <gl-sprintf :message="$options.i18n.modal.content">
- <template #link="{ content }">
- <gl-link :href="userPreferencesGitpodPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </gl-modal>
- <confirm-fork-modal
- v-if="showWebIdeButton || showEditButton"
- v-model="showForkModal"
- :modal-id="forkModalId"
- :fork-path="forkPath"
- />
- </div>
+ <user-callout-dismisser
+ :skip-query="!displayVscodeWebIdeCallout"
+ feature-name="vscode_web_ide_callout"
+ >
+ <template #default="{ dismiss, shouldShowCallout }">
+ <div class="gl-sm-ml-3">
+ <actions-button
+ :id="$options.webIdeButtonId"
+ :actions="actions"
+ :selected-key="selection"
+ :variant="isBlob ? 'confirm' : 'default'"
+ :category="isBlob ? 'primary' : 'secondary'"
+ :show-action-tooltip="!displayVscodeWebIdeCallout || !shouldShowCallout"
+ @select="select"
+ @actionClicked="dismissCalloutOnActionClicked(dismiss)"
+ />
+ <local-storage-sync
+ :storage-key="$options.PREFERRED_EDITOR_KEY"
+ :value="selection"
+ as-string
+ @input="select"
+ />
+ <gl-modal
+ v-if="computedShowGitpodButton && !gitpodEnabled"
+ v-model="showEnableGitpodModal"
+ v-bind="enableGitpodModalProps"
+ >
+ <gl-sprintf :message="$options.i18n.modal.content">
+ <template #link="{ content }">
+ <gl-link :href="userPreferencesGitpodPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ <confirm-fork-modal
+ v-if="showWebIdeButton || showEditButton"
+ v-model="showForkModal"
+ :modal-id="forkModalId"
+ :fork-path="forkPath"
+ />
+ <gl-popover
+ v-if="displayVscodeWebIdeCallout"
+ :target="$options.webIdeButtonId"
+ :show="shouldShowCallout"
+ :css-classes="['web-ide-promo-popover']"
+ :boundary-padding="80"
+ show-close-button
+ triggers="manual"
+ @close-button-clicked="dismiss"
+ >
+ <img
+ :src="webIdePromoPopoverImg"
+ class="web-ide-promo-popover-illustration"
+ width="280"
+ height="140"
+ />
+ <div class="gl-mx-2">
+ <h5 class="gl-mt-3 gl-mb-3">{{ __('The new Web IDE') }}</h5>
+ <p>
+ {{
+ __(
+ 'VS Code in your browser. View code and make changes from the same UI as in your local IDE.',
+ )
+ }}
+ </p>
+ <gl-link
+ class="gl-button btn btn-confirm block gl-mb-4 gl-mt-5"
+ variant="confirm"
+ category="primary"
+ target="_blank"
+ :href="webIdeUrl"
+ block
+ >
+ {{ __('Try it out now') }}
+ </gl-link>
+ </div>
+ </gl-popover>
+ </div>
+ </template>
+ </user-callout-dismisser>
</template>
diff --git a/app/assets/javascripts/vue_shared/constants.js b/app/assets/javascripts/vue_shared/constants.js
index a851f84ed2f..2f85a29fb84 100644
--- a/app/assets/javascripts/vue_shared/constants.js
+++ b/app/assets/javascripts/vue_shared/constants.js
@@ -13,7 +13,9 @@ export const SHORT_DATE_FORMAT = 'd mmm, yyyy';
export const ISO_SHORT_FORMAT = 'yyyy-mm-dd';
-export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT];
+export const LONG_DATE_FORMAT_WITH_TZ = 'yyyy-mm-dd HH:MM:ss Z';
+
+export const DATE_FORMATS = [SHORT_DATE_FORMAT, ISO_SHORT_FORMAT, LONG_DATE_FORMAT_WITH_TZ];
const getTimeLabel = (days) => n__('1 day', '%d days', days);
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 25799171905..2644befc902 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_form.vue
@@ -1,8 +1,8 @@
<script>
import { GlForm, GlFormInput, GlFormGroup } from '@gitlab/ui';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
-import LabelsSelect from '~/vue_shared/components/sidebar/labels_select_vue/labels_select_root.vue';
+import { DropdownVariant } from '~/sidebar/components/labels/labels_select_vue/constants';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
export default {
LabelSelectVariant: DropdownVariant,
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
new file mode 100644
index 00000000000..b3f9c8d9fcd
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
@@ -0,0 +1,92 @@
+<script>
+import { GlFormGroup, GlIcon } from '@gitlab/ui';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import LabelsSelect from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlIcon,
+ LabelsSelect,
+ },
+ inject: [
+ 'allowLabelRemove',
+ 'attrWorkspacePath',
+ 'fieldName',
+ 'fullPath',
+ 'labelsFilterBasePath',
+ 'initialLabels',
+ 'issuableType',
+ 'labelType',
+ 'variant',
+ 'workspaceType',
+ ],
+ data() {
+ return {
+ selectedLabels: this.initialLabels || [],
+ };
+ },
+ methods: {
+ handleUpdateSelectedLabels({ labels }) {
+ this.selectedLabels = labels.map((label) => ({ ...label, id: getIdFromGraphQLId(label.id) }));
+ },
+ handleLabelRemove(labelId) {
+ this.selectedLabels = this.selectedLabels.filter((label) => label.id !== labelId);
+ },
+ },
+ i18n: {
+ fieldLabel: __('Labels'),
+ dropdownButtonText: __('Select label'),
+ listTitle: __('Select label'),
+ createTitle: __('Create project label'),
+ manageTitle: __('Manage project labels'),
+ emptySelection: __('None'),
+ },
+};
+</script>
+
+<template>
+ <gl-form-group class="row" label-class="gl-display-none">
+ <label class="col-12 gl-display-flex gl-align-center gl-mb-1">
+ {{ $options.i18n.fieldLabel }}
+ <div class="gl-ml-3">
+ <gl-icon name="labels" />
+ <span class="gl-font-base gl-line-height-24">{{ selectedLabels.length }}</span>
+ </div>
+ </label>
+ <div class="col-12">
+ <div class="issuable-form-select-holder">
+ <input
+ v-for="selectedLabel in selectedLabels"
+ :key="selectedLabel.id"
+ :value="selectedLabel.id"
+ :name="fieldName"
+ type="hidden"
+ />
+ <labels-select
+ class="block labels"
+ :allow-label-remove="allowLabelRemove"
+ :allow-multiselect="true"
+ :show-embedded-labels-list="true"
+ :full-path="fullPath"
+ :attr-workspace-path="attrWorkspacePath"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :dropdown-button-text="$options.i18n.dropdownButtonText"
+ :labels-list-title="$options.i18n.listTitle"
+ :footer-create-label-title="$options.i18n.createTitle"
+ :footer-manage-label-title="$options.i18n.manageTitle"
+ :variant="variant"
+ :workspace-type="workspaceType"
+ :issuable-type="issuableType"
+ :label-create-type="labelType"
+ :selected-labels="selectedLabels"
+ @updateSelectedLabels="handleUpdateSelectedLabels"
+ @onLabelRemove="handleLabelRemove"
+ >
+ {{ $options.i18n.emptySelection }}
+ </labels-select>
+ </div>
+ </div>
+ </gl-form-group>
+</template>
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 30b7b073ac3..5b303b9a314 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
@@ -318,8 +318,8 @@ export default {
<slot name="statistics"></slot>
<li
v-if="showDiscussions"
- data-testid="issuable-discussions"
- class="issuable-comments gl-display-none gl-sm-display-block"
+ class="gl-display-none gl-sm-display-block"
+ data-testid="issuable-comments"
>
<gl-link
v-gl-tooltip.top
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 dd3d7c8f4d6..5b6c5bf6e03 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
@@ -331,6 +331,7 @@ export default {
<slot name="sidebar-items" :checked-issuables="bulkEditIssuables"></slot>
</template>
</issuable-bulk-edit-sidebar>
+ <slot name="list-body"></slot>
<ul v-if="issuablesLoading" class="content-list">
<li v-for="n in skeletonItemCount" :key="n" class="issue gl-px-5! gl-py-5!">
<gl-skeleton-loader />
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 d4e9120ff17..ce1851ab873 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
@@ -1,7 +1,6 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
export default {
directives: {
@@ -26,12 +25,7 @@ export default {
},
},
mounted() {
- this.renderGFM();
- },
- methods: {
- renderGFM() {
- $(this.$refs.gfmContainer).renderGFM();
- },
+ renderGFM(this.$refs.gfmContainer);
},
};
</script>
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 35124bd15d2..fd94245b7c9 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
@@ -1,12 +1,6 @@
<script>
-import {
- GlIcon,
- GlBadge,
- GlButton,
- GlIntersectionObserver,
- GlTooltipDirective,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
+import { GlIcon, GlBadge, GlButton, GlIntersectionObserver, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { __ } from '~/locale';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
index e42720bf1db..ae40076ca96 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/legacy_container.vue
@@ -1,4 +1,6 @@
<script>
+import projectNew from '~/projects/project_new';
+
export default {
inheritAttrs: false,
props: {
@@ -16,6 +18,7 @@ export default {
this.source = legacyEntry.parentNode;
this.$el.appendChild(legacyEntry);
legacyEntry.classList.add('active');
+ projectNew.bindEvents();
}
},
diff --git a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
index 5cd2018bb8c..b6a459f21e0 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
export default {
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 624ae7027d5..318adec2319 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
@@ -1,5 +1,6 @@
<script>
-import { GlBreadcrumb, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlBreadcrumb, GlIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import NewTopLevelGroupAlert from '~/groups/components/new_top_level_group_alert.vue';
import LegacyContainer from './components/legacy_container.vue';
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index 0e1975e1c09..b739baad5d7 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -2,8 +2,8 @@
import { mapActions, mapGetters } from 'vuex';
import { createAlert } from '~/flash';
import { s__ } from '~/locale';
-import ReportSection from '~/reports/components/report_section.vue';
-import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/reports/constants';
+import ReportSection from '~/ci/reports/components/report_section.vue';
+import { ERROR, SLOT_SUCCESS, SLOT_LOADING, SLOT_ERROR } from '~/ci/reports/constants';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import HelpIcon from './components/help_icon.vue';
import SecurityReportDownloadDropdown from './components/security_report_download_dropdown.vue';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/getters.js b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
index 08f6bcca15b..c274f531139 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/getters.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/getters.js
@@ -1,5 +1,5 @@
import { s__, sprintf } from '~/locale';
-import { LOADING, ERROR, SUCCESS } from '~/reports/constants';
+import { LOADING, ERROR, SUCCESS } from '~/ci/reports/constants';
import { TRANSLATION_IS_LOADING } from './messages';
import { countVulnerabilities, groupedTextBuilder } from './utils';
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/utils.js b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
index a6628fa0f9f..f3cb5fc16f0 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/utils.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/utils.js
@@ -29,7 +29,13 @@ export const fetchDiffData = (state, endpoint, category) => {
*/
export const enrichVulnerabilityWithFeedback = (vulnerability, feedback = []) =>
feedback
- .filter((fb) => fb.project_fingerprint === vulnerability.project_fingerprint)
+ .filter((fb) =>
+ // Some records still have a `finding_uuid` with null, we need to fallback to using `project_fingerprint` in those cases. Once all entries have been fixed, we can remove the fallback.
+ // related epic: https://gitlab.com/groups/gitlab-org/-/epics/2791
+ fb.finding_uuid !== null
+ ? fb.finding_uuid === vulnerability.finding_uuid
+ : fb.project_fingerprint === vulnerability.project_fingerprint,
+ )
.reduce((vuln, fb) => {
if (fb.feedback_type === FEEDBACK_TYPE_DISMISSAL) {
return {
diff --git a/app/assets/javascripts/webhooks/components/push_events.vue b/app/assets/javascripts/webhooks/components/push_events.vue
index 677f06314e0..91d7e21500a 100644
--- a/app/assets/javascripts/webhooks/components/push_events.vue
+++ b/app/assets/javascripts/webhooks/components/push_events.vue
@@ -33,7 +33,7 @@ export default {
<template>
<div>
- <gl-form-checkbox v-model="pushEventsData">{{ s__('Webhooks|Push events') }}</gl-form-checkbox>
+ <gl-form-checkbox v-model="pushEventsData">{{ __('Push events') }}</gl-form-checkbox>
<input type="hidden" :value="pushEventsData" name="hook[push_events]" />
<div v-if="pushEventsData" class="gl-pl-6">
diff --git a/app/assets/javascripts/webhooks/constants.js b/app/assets/javascripts/webhooks/constants.js
index 6710a418117..96632b47e6b 100644
--- a/app/assets/javascripts/webhooks/constants.js
+++ b/app/assets/javascripts/webhooks/constants.js
@@ -7,13 +7,13 @@ export const BRANCH_FILTER_REGEX = 'regex';
export const WILDCARD_CODE_STABLE = '*-stable';
export const WILDCARD_CODE_PRODUCTION = 'production/*';
-export const REGEX_CODE = '(feature|hotfix)/*';
+export const REGEX_CODE = '^(feature|hotfix)/';
export const descriptionText = {
[BRANCH_FILTER_WILDCARD]: s__(
'Webhooks|Wildcards such as %{WILDCARD_CODE_STABLE} or %{WILDCARD_CODE_PRODUCTION} are supported.',
),
- [BRANCH_FILTER_REGEX]: s__('Webhooks|Regex such as %{REGEX_CODE} is supported.'),
+ [BRANCH_FILTER_REGEX]: s__('Webhooks|Regular expressions such as %{REGEX_CODE} are supported.'),
};
export const MASK_ITEM_VALUE_HIDDEN = '************';
diff --git a/app/assets/javascripts/whats_new/components/feature.vue b/app/assets/javascripts/whats_new/components/feature.vue
index c954a86e593..044a6db6d93 100644
--- a/app/assets/javascripts/whats_new/components/feature.vue
+++ b/app/assets/javascripts/whats_new/components/feature.vue
@@ -1,5 +1,6 @@
<script>
-import { GlBadge, GlIcon, GlLink, GlSafeHtmlDirective, GlButton } from '@gitlab/ui';
+import { GlBadge, GlIcon, GlLink, GlButton } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
import { dateInWords, isValidDate } from '~/lib/utils/datetime_utility';
export default {
@@ -10,7 +11,7 @@ export default {
GlButton,
},
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
},
props: {
feature: {
diff --git a/app/assets/javascripts/work_items/components/notes/system_note.vue b/app/assets/javascripts/work_items/components/notes/system_note.vue
new file mode 100644
index 00000000000..92a2fcaf1df
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/notes/system_note.vue
@@ -0,0 +1,229 @@
+<script>
+/**
+ * Common component to render a system note, icon and user information.
+ *
+ * This component need not be used with any store neither has any vuex dependency
+ *
+ * @example
+ * <system-note
+ * :note="{
+ * id: String,
+ * author: Object,
+ * createdAt: String,
+ * bodyHtml: String,
+ * systemNoteIconName: String
+ * }"
+ * />
+ */
+import { GlButton, GlSkeletonLoader, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import $ from 'jquery';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import descriptionVersionHistoryMixin from 'ee_else_ce/notes/mixins/description_version_history';
+import axios from '~/lib/utils/axios_utils';
+import { getLocationHash } from '~/lib/utils/url_utility';
+import { __ } from '~/locale';
+import NoteHeader from '~/notes/components/note_header.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+const MAX_VISIBLE_COMMIT_LIST_COUNT = 3;
+
+export default {
+ i18n: {
+ deleteButtonLabel: __('Remove description history'),
+ },
+ name: 'SystemNote',
+ components: {
+ GlIcon,
+ NoteHeader,
+ TimelineEntryItem,
+ GlButton,
+ GlSkeletonLoader,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ SafeHtml,
+ },
+ mixins: [descriptionVersionHistoryMixin, glFeatureFlagsMixin()],
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ expanded: false,
+ lines: [],
+ showLines: false,
+ loadingDiff: false,
+ isLoadingDescriptionVersion: false,
+ };
+ },
+ computed: {
+ targetNoteHash() {
+ return getLocationHash();
+ },
+ descriptionVersions() {
+ return [];
+ },
+ noteAnchorId() {
+ return `note_${this.note.id}`;
+ },
+ isTargetNote() {
+ return this.targetNoteHash === this.noteAnchorId;
+ },
+ toggleIcon() {
+ return this.expanded ? 'chevron-up' : 'chevron-down';
+ },
+ // following 2 methods taken from code in `collapseLongCommitList` of notes.js:
+ actionTextHtml() {
+ return $(this.note.bodyHtml).unwrap().html();
+ },
+ hasMoreCommits() {
+ return $(this.note.bodyHtml).filter('ul').children().length > MAX_VISIBLE_COMMIT_LIST_COUNT;
+ },
+ descriptionVersion() {
+ return this.descriptionVersions[this.note.description_version_id];
+ },
+ },
+ mounted() {
+ renderGFM(this.$refs['gfm-content']);
+ },
+ methods: {
+ fetchDescriptionVersion() {},
+ softDeleteDescriptionVersion() {},
+
+ async toggleDiff() {
+ this.showLines = !this.showLines;
+
+ if (!this.lines.length) {
+ this.loadingDiff = true;
+ const { data } = await axios.get(this.note.outdated_line_change_path);
+
+ this.lines = data.map((l) => ({
+ ...l,
+ rich_text: l.rich_text.replace(/^[+ -]/, ''),
+ }));
+ this.loadingDiff = false;
+ }
+ },
+ },
+ safeHtmlConfig: {
+ ADD_TAGS: ['use'], // to support icon SVGs
+ },
+ userColorSchemeClass: window.gon.user_color_scheme,
+};
+</script>
+
+<template>
+ <timeline-entry-item
+ :id="noteAnchorId"
+ :class="{ target: isTargetNote, 'pr-0': shouldShowDescriptionVersion }"
+ class="note system-note note-wrapper"
+ >
+ <div class="timeline-icon"><gl-icon :name="note.systemNoteIconName" /></div>
+ <div class="timeline-content">
+ <div class="note-header">
+ <note-header
+ :author="note.author"
+ :created-at="note.createdAt"
+ :note-id="note.id"
+ :is-system-note="true"
+ >
+ <span ref="gfm-content" v-safe-html="actionTextHtml"></span>
+ <template
+ v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
+ #extra-controls
+ >
+ &middot;
+ <gl-button
+ v-if="canSeeDescriptionVersion"
+ variant="link"
+ :icon="descriptionVersionToggleIcon"
+ data-testid="compare-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDescriptionVersion"
+ >{{ __('Compare with previous version') }}</gl-button
+ >
+ <gl-button
+ v-if="note.outdated_line_change_path"
+ :icon="showLines ? 'chevron-up' : 'chevron-down'"
+ variant="link"
+ data-testid="outdated-lines-change-btn"
+ class="gl-vertical-align-text-bottom gl-font-sm!"
+ @click="toggleDiff"
+ >
+ {{ __('Compare changes') }}
+ </gl-button>
+ </template>
+ </note-header>
+ </div>
+ <div class="note-body">
+ <div
+ v-safe-html="note.bodyHtml"
+ :class="{ 'system-note-commit-list': hasMoreCommits, 'hide-shade': expanded }"
+ class="note-text md"
+ ></div>
+ <div v-if="hasMoreCommits" class="flex-list">
+ <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
+ <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <span>{{ __('Toggle commit list') }}</span>
+ </div>
+ </div>
+ <div v-if="shouldShowDescriptionVersion" class="description-version pt-2">
+ <pre v-if="isLoadingDescriptionVersion" class="loading-state">
+ <gl-skeleton-loader />
+ </pre>
+ <pre v-else v-safe-html="descriptionVersion" class="wrapper mt-2"></pre>
+ <gl-button
+ v-if="displayDeleteButton"
+ v-gl-tooltip
+ :title="$options.i18n.deleteButtonLabel"
+ :aria-label="$options.i18n.deleteButtonLabel"
+ variant="default"
+ category="tertiary"
+ icon="remove"
+ class="delete-description-history"
+ data-testid="delete-description-version-button"
+ @click="deleteDescriptionVersion"
+ />
+ </div>
+ <div
+ v-if="lines.length && showLines"
+ class="diff-content outdated-lines-wrapper gl-border-solid gl-border-1 gl-border-gray-200 gl-mt-4 gl-rounded-small gl-overflow-hidden"
+ >
+ <table
+ :class="$options.userColorSchemeClass"
+ class="code js-syntax-highlight"
+ data-testid="outdated-lines"
+ >
+ <tr v-for="line in lines" v-once :key="line.line_code" class="line_holder">
+ <td
+ :class="line.type"
+ class="diff-line-num old_line gl-border-bottom-0! gl-border-top-0! gl-border-0! gl-rounded-0!"
+ >
+ {{ line.old_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="diff-line-num new_line gl-border-bottom-0! gl-border-top-0!"
+ >
+ {{ line.new_line }}
+ </td>
+ <td
+ :class="line.type"
+ class="line_content gl-display-table-cell! gl-border-0! gl-rounded-0!"
+ v-html="line.rich_text /* eslint-disable-line vue/no-v-html */"
+ ></td>
+ </tr>
+ </table>
+ </div>
+ <div v-else-if="showLines" class="mt-4">
+ <gl-skeleton-loader />
+ </div>
+ </div>
+ </div>
+ </timeline-entry-item>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_assignees.vue b/app/assets/javascripts/work_items/components/work_item_assignees.vue
index 4d6a27f61ac..c2980405a19 100644
--- a/app/assets/javascripts/work_items/components/work_item_assignees.vue
+++ b/app/assets/javascripts/work_items/components/work_item_assignees.vue
@@ -202,6 +202,7 @@ export default {
if (!this.allowsMultipleAssignees) {
this.localAssignees = assignees.length > 0 ? [assignees[assignees.length - 1]] : [];
this.isEditing = false;
+ this.setAssignees(this.assigneeIds);
return;
}
this.localAssignees = assignees;
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 57930951856..07da0279b41 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlFormGroup } from '@gitlab/ui';
+import { GlAlert, GlButton, GlFormGroup } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
import { helpPagePath } from '~/helpers/help_page_helper';
import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
@@ -19,6 +19,7 @@ import WorkItemDescriptionRendered from './work_item_description_rendered.vue';
export default {
components: {
EditedAt,
+ GlAlert,
GlButton,
GlFormGroup,
MarkdownEditor,
@@ -54,6 +55,7 @@ export default {
isSubmittingWithKeydown: false,
descriptionText: '',
descriptionHtml: '',
+ conflictedDescription: '',
};
},
apollo: {
@@ -68,11 +70,17 @@ export default {
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
- return !this.workItemId;
+ return !this.queryVariables.id && !this.queryVariables.iid;
},
result() {
- this.descriptionText = this.workItemDescription?.description;
- this.descriptionHtml = this.workItemDescription?.descriptionHtml;
+ if (this.isEditing) {
+ if (this.descriptionText !== this.workItemDescription?.description) {
+ this.conflictedDescription = this.workItemDescription?.description;
+ }
+ } else {
+ this.descriptionText = this.workItemDescription?.description;
+ this.descriptionHtml = this.workItemDescription?.descriptionHtml;
+ }
},
error() {
this.$emit('error', i18n.fetchError);
@@ -94,6 +102,9 @@ export default {
canEdit() {
return this.workItem?.userPermissions?.updateWorkItem || false;
},
+ hasConflicts() {
+ return Boolean(this.conflictedDescription);
+ },
tracking() {
return {
category: TRACKING_CATEGORY_SHOW,
@@ -196,6 +207,7 @@ export default {
this.isEditing = false;
clearDraft(this.autosaveKey);
+ this.conflictedDescription = '';
} catch (error) {
this.$emit('error', error.message);
Sentry.captureException(error);
@@ -224,7 +236,7 @@ export default {
label-for="work-item-description"
>
<markdown-editor
- v-if="glFeatures.workItemsMvc2"
+ v-if="glFeatures.workItemsMvc"
class="gl-my-3 common-note-form"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
@@ -235,6 +247,7 @@ export default {
form-field-name="work-item-description"
enable-autocomplete
init-on-autofocus
+ use-bottom-toolbar
@input="setDescriptionText"
@keydown.meta.enter="updateWorkItem"
@keydown.ctrl.enter="updateWorkItem"
@@ -246,7 +259,7 @@ export default {
:is-submitting="isSubmitting"
:markdown-preview-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
- class="gl-p-3 bordered-box gl-mt-5"
+ class="gl-px-3 bordered-box gl-mt-5"
>
<template #textarea>
<textarea
@@ -267,17 +280,59 @@ export default {
</template>
</markdown-field>
<div class="gl-display-flex">
- <gl-button
- category="primary"
- variant="confirm"
- :loading="isSubmitting"
- data-testid="save-description"
- @click="updateWorkItem"
- >{{ __('Save') }}
- </gl-button>
- <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing"
- >{{ __('Cancel') }}
- </gl-button>
+ <gl-alert
+ v-if="hasConflicts"
+ :dismissible="false"
+ variant="danger"
+ class="gl-w-full"
+ data-testid="work-item-description-conflicts"
+ >
+ <p>
+ {{
+ s__(
+ "WorkItem|Someone edited the description at the same time you did. If you save it will overwrite their changes. Please confirm you'd like to save your edits.",
+ )
+ }}
+ </p>
+ <details class="gl-mb-5">
+ <summary class="gl-text-blue-500">{{ s__('WorkItem|View current version') }}</summary>
+ <textarea
+ class="note-textarea js-gfm-input js-autosize markdown-area gl-p-3"
+ readonly
+ :value="conflictedDescription"
+ ></textarea>
+ </details>
+ <template #actions>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ s__('WorkItem|Save and overwrite') }}
+ </gl-button>
+ <gl-button
+ category="secondary"
+ class="gl-ml-3"
+ data-testid="cancel"
+ @click="cancelEditing"
+ >{{ s__('WorkItem|Discard changes') }}
+ </gl-button>
+ </template>
+ </gl-alert>
+ <template v-else>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :loading="isSubmitting"
+ data-testid="save-description"
+ @click="updateWorkItem"
+ >{{ __('Save') }}
+ </gl-button>
+ <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" @click="cancelEditing"
+ >{{ __('Cancel') }}
+ </gl-button>
+ </template>
</div>
</gl-form-group>
<work-item-description-rendered
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index e6f8a301c5e..d58983c013d 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -1,13 +1,14 @@
<script>
-import { GlButton, GlSafeHtmlDirective } from '@gitlab/ui';
-import $ from 'jquery';
-import '~/behaviors/markdown/render_gfm';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
const isCheckbox = (target) => target?.classList.contains('task-list-item-checkbox');
export default {
directives: {
- SafeHtml: GlSafeHtmlDirective,
+ SafeHtml,
+ GlTooltip: GlTooltipDirective,
},
components: {
GlButton,
@@ -45,7 +46,7 @@ export default {
async renderGFM() {
await this.$nextTick();
- $(this.$refs['gfm-content']).renderGFM();
+ renderGFM(this.$refs['gfm-content']);
if (this.canEdit) {
this.checkboxes = this.$el.querySelectorAll('.task-list-item-checkbox');
@@ -93,14 +94,16 @@ export default {
<template>
<div class="gl-mb-5 gl-border-t gl-pt-5">
- <div class="gl-display-inline-flex gl-align-items-center gl-mb-5">
+ <div class="gl-display-inline-flex gl-align-items-center gl-mb-3">
<label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
<gl-button
v-if="canEdit"
+ v-gl-tooltip
class="gl-ml-auto"
icon="pencil"
data-testid="edit-description"
:aria-label="__('Edit description')"
+ :title="__('Edit description')"
@click="$emit('startEditing')"
/>
</div>
@@ -111,6 +114,7 @@ export default {
ref="gfm-content"
v-safe-html="descriptionHtml"
class="md gl-mb-5 gl-min-h-8"
+ data-testid="work-item-description"
@change="toggleCheckboxes"
></div>
</div>
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 7e9fa24e3f5..cb45a05de89 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,5 +1,6 @@
<script>
import { isEmpty } from 'lodash';
+import { produce } from 'immer';
import {
GlAlert,
GlSkeletonLoader,
@@ -11,10 +12,11 @@ import {
GlEmptyState,
} from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg';
+import * as Sentry from '@sentry/browser';
import { s__ } from '~/locale';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import {
i18n,
@@ -23,10 +25,14 @@ import {
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_START_AND_DUE_DATE,
WIDGET_TYPE_WEIGHT,
+ WIDGET_TYPE_PROGRESS,
WIDGET_TYPE_HIERARCHY,
- WORK_ITEM_VIEWED_STORAGE_KEY,
WIDGET_TYPE_MILESTONE,
WIDGET_TYPE_ITERATION,
+ WIDGET_TYPE_HEALTH_STATUS,
+ WORK_ITEM_TYPE_VALUE_ISSUE,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_NOTES,
} from '../constants';
import workItemDatesSubscription from '../graphql/work_item_dates.subscription.graphql';
@@ -37,6 +43,7 @@ import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql
import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import { getWorkItemQuery } from '../utils';
+import WorkItemTree from './work_item_links/work_item_tree.vue';
import WorkItemActions from './work_item_actions.vue';
import WorkItemState from './work_item_state.vue';
import WorkItemTitle from './work_item_title.vue';
@@ -45,7 +52,7 @@ import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
-import WorkItemInformation from './work_item_information.vue';
+import WorkItemNotes from './work_item_notes.vue';
export default {
i18n,
@@ -68,11 +75,14 @@ export default {
WorkItemTitle,
WorkItemState,
WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
- WorkItemInformation,
- LocalStorageSync,
+ WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemTypeIcon,
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
+ WorkItemHealthStatus: () =>
+ import('ee_component/work_items/components/work_item_health_status.vue'),
WorkItemMilestone,
+ WorkItemTree,
+ WorkItemNotes,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath'],
@@ -87,7 +97,7 @@ export default {
required: false,
default: null,
},
- iid: {
+ workItemIid: {
type: String,
required: false,
default: null,
@@ -103,7 +113,6 @@ export default {
error: undefined,
updateError: undefined,
workItem: {},
- showInfoBanner: true,
updateInProgress: false,
};
},
@@ -201,17 +210,31 @@ export default {
fullPath() {
return this.workItem?.project.fullPath;
},
+ workItemsMvcEnabled() {
+ return this.glFeatures.workItemsMvc;
+ },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
parentWorkItem() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
},
+ parentWorkItemType() {
+ return this.parentWorkItem?.workItemType?.name;
+ },
+ parentWorkItemIconName() {
+ return this.parentWorkItem?.workItemType?.iconName;
+ },
parentWorkItemConfidentiality() {
return this.parentWorkItem?.confidential;
},
parentUrl() {
- return `../../issues/${this.parentWorkItem?.iid}`;
+ // Once more types are moved to have Work Items involved
+ // we need to handle this properly.
+ if (this.parentWorkItemType === WORK_ITEM_TYPE_VALUE_ISSUE) {
+ return `../../issues/${this.parentWorkItem?.iid}`;
+ }
+ return this.parentWorkItem?.webUrl;
},
workItemIconName() {
return this.workItem?.workItemType?.iconName;
@@ -234,41 +257,48 @@ export default {
workItemWeight() {
return this.isWidgetPresent(WIDGET_TYPE_WEIGHT);
},
+ workItemProgress() {
+ return this.isWidgetPresent(WIDGET_TYPE_PROGRESS);
+ },
workItemHierarchy() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY);
},
workItemIteration() {
return this.isWidgetPresent(WIDGET_TYPE_ITERATION);
},
+ workItemHealthStatus() {
+ return this.isWidgetPresent(WIDGET_TYPE_HEALTH_STATUS);
+ },
workItemMilestone() {
return this.isWidgetPresent(WIDGET_TYPE_MILESTONE);
},
+ workItemNotes() {
+ return this.isWidgetPresent(WIDGET_TYPE_NOTES);
+ },
fetchByIid() {
- return this.glFeatures.useIidInWorkItemsPath && parseBoolean(this.$route.query.iid_path);
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
},
queryVariables() {
return this.fetchByIid
? {
fullPath: this.fullPath,
- iid: this.iid,
+ iid: this.workItemIid,
}
: {
id: this.workItemId,
};
},
- },
- beforeDestroy() {
- /** make sure that if the user has not even dismissed the alert ,
- * should no be able to see the information next time and update the local storage * */
- this.dismissBanner();
+ children() {
+ const widgetHierarchy = this.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+ return widgetHierarchy.children.nodes;
+ },
},
methods: {
isWidgetPresent(type) {
return this.workItem?.widgets?.find((widget) => widget.type === type);
},
- dismissBanner() {
- this.showInfoBanner = false;
- },
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
let updateMutation = updateWorkItemMutation;
@@ -321,8 +351,76 @@ export default {
this.error = this.$options.i18n.fetchError;
document.title = s__('404|Not found');
},
+ addChild(child) {
+ const { defaultClient: client } = this.$apollo.provider.clients;
+ this.toggleChildFromCache(child, child.id, client);
+ },
+ toggleChildFromCache(workItem, childId, store) {
+ const sourceData = store.readQuery({
+ query: getWorkItemQuery(this.fetchByIid),
+ variables: this.queryVariables,
+ });
+
+ const newData = produce(sourceData, (draftState) => {
+ const widgetHierarchy = draftState.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_HIERARCHY,
+ );
+
+ const index = widgetHierarchy.children.nodes.findIndex((child) => child.id === childId);
+
+ if (index >= 0) {
+ widgetHierarchy.children.nodes.splice(index, 1);
+ } else {
+ widgetHierarchy.children.nodes.unshift(workItem);
+ }
+ });
+
+ store.writeQuery({
+ query: getWorkItemQuery(this.fetchByIid),
+ variables: this.queryVariables,
+ data: newData,
+ });
+ },
+ async updateWorkItem(workItem, childId, parentId) {
+ return this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: { input: { id: childId, hierarchyWidget: { parentId } } },
+ update: (store) => this.toggleChildFromCache(workItem, childId, store),
+ });
+ },
+ async undoChildRemoval(workItem, childId) {
+ try {
+ const { data } = await this.updateWorkItem(workItem, childId, this.workItem.id);
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast?.hide();
+ }
+ } catch (error) {
+ this.updateError = s__('WorkItem|Something went wrong while undoing child removal.');
+ Sentry.captureException(error);
+ } finally {
+ this.activeToast?.hide();
+ }
+ },
+ async removeChild(childId) {
+ try {
+ const { data } = await this.updateWorkItem(null, childId, null);
+
+ if (data.workItemUpdate.errors.length === 0) {
+ this.activeToast = this.$toast.show(s__('WorkItem|Child removed'), {
+ action: {
+ text: s__('WorkItem|Undo'),
+ onClick: this.undoChildRemoval.bind(this, data.workItemUpdate.workItem, childId),
+ },
+ });
+ }
+ } catch (error) {
+ this.updateError = s__('WorkItem|Something went wrong while removing child.');
+ Sentry.captureException(error);
+ }
+ },
},
- WORK_ITEM_VIEWED_STORAGE_KEY,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
};
</script>
@@ -347,14 +445,14 @@ export default {
<div class="gl-display-flex gl-align-items-center" data-testid="work-item-body">
<ul
v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0"
+ class="list-unstyled gl-display-flex gl-mr-auto gl-max-w-26 gl-md-max-w-50p gl-min-w-0 gl-mb-0 gl-z-index-0"
data-testid="work-item-parent"
>
<li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-overflow-hidden">
<gl-button
v-gl-tooltip.hover
class="gl-text-truncate gl-max-w-full"
- icon="issues"
+ :icon="parentWorkItemIconName"
category="tertiary"
:href="parentUrl"
:title="parentWorkItem.title"
@@ -411,16 +509,6 @@ export default {
@click="$emit('close')"
/>
</div>
- <local-storage-sync
- v-model="showInfoBanner"
- :storage-key="$options.WORK_ITEM_VIEWED_STORAGE_KEY"
- >
- <work-item-information
- v-if="showInfoBanner && !error"
- :show-info-banner="showInfoBanner"
- @work-item-banner-dismissed="dismissBanner"
- />
- </local-storage-sync>
<work-item-title
v-if="workItem.title"
:work-item-id="workItem.id"
@@ -465,19 +553,17 @@ export default {
:work-item-type="workItemType"
@error="updateError = $event"
/>
- <template v-if="workItemsMvc2Enabled">
- <work-item-milestone
- v-if="workItemMilestone"
- :work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.milestone"
- :work-item-type="workItemType"
- :fetch-by-iid="fetchByIid"
- :query-variables="queryVariables"
- :can-update="canUpdate"
- :full-path="fullPath"
- @error="updateError = $event"
- />
- </template>
+ <work-item-milestone
+ v-if="workItemMilestone"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
<work-item-weight
v-if="workItemWeight"
class="gl-mb-5"
@@ -489,20 +575,38 @@ export default {
:query-variables="queryVariables"
@error="updateError = $event"
/>
- <template v-if="workItemsMvc2Enabled">
- <work-item-iteration
- v-if="workItemIteration"
- class="gl-mb-5"
- :iteration="workItemIteration.iteration"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- :fetch-by-iid="fetchByIid"
- :query-variables="queryVariables"
- :full-path="fullPath"
- @error="updateError = $event"
- />
- </template>
+ <work-item-progress
+ v-if="workItemProgress"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :progress="workItemProgress.progress"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ @error="updateError = $event"
+ />
+ <work-item-iteration
+ v-if="workItemIteration"
+ class="gl-mb-5"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :fetch-by-iid="fetchByIid"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ @error="updateError = $event"
+ />
+ <work-item-health-status
+ v-if="workItemHealthStatus"
+ class="gl-mb-5"
+ :health-status="workItemHealthStatus.healthStatus"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ @error="updateError = $event"
+ />
<work-item-description
v-if="hasDescriptionWidget"
:work-item-id="workItem.id"
@@ -512,6 +616,27 @@ export default {
class="gl-pt-5"
@error="updateError = $event"
/>
+ <work-item-tree
+ v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
+ :work-item-type="workItemType"
+ :work-item-id="workItem.id"
+ :children="children"
+ :can-update="canUpdate"
+ :project-path="fullPath"
+ @addWorkItemChild="addChild"
+ @removeChild="removeChild"
+ />
+ <template v-if="workItemsMvc2Enabled">
+ <work-item-notes
+ v-if="workItemNotes"
+ :work-item-id="workItem.id"
+ :query-variables="queryVariables"
+ :full-path="fullPath"
+ :fetch-by-iid="fetchByIid"
+ class="gl-pt-5"
+ @error="updateError = $event"
+ />
+ </template>
<gl-empty-state
v-if="error"
:title="$options.i18n.fetchErrorTitle"
diff --git a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
index 39a662a6c54..e8726814aaf 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail_modal.vue
@@ -20,6 +20,11 @@ export default {
required: false,
default: null,
},
+ workItemIid: {
+ type: String,
+ required: false,
+ default: null,
+ },
issueGid: {
type: String,
required: false,
@@ -134,6 +139,7 @@ export default {
size="lg"
modal-id="work-item-detail-modal"
header-class="gl-p-0 gl-pb-2!"
+ scrollable
@hide="closeModal"
>
<gl-alert v-if="error" variant="danger" @dismiss="error = false">
@@ -144,6 +150,7 @@ export default {
is-modal
:work-item-parent-id="issueGid"
:work-item-id="workItemId"
+ :work-item-iid="workItemIid"
class="gl-p-5 gl-mt-n3"
@close="hide"
@deleteWorkItem="deleteWorkItem"
diff --git a/app/assets/javascripts/work_items/components/work_item_information.vue b/app/assets/javascripts/work_items/components/work_item_information.vue
deleted file mode 100644
index ce75cc98a75..00000000000
--- a/app/assets/javascripts/work_items/components/work_item_information.vue
+++ /dev/null
@@ -1,53 +0,0 @@
-<script>
-import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-export default {
- i18n: {
- learnTasksLinkText: s__('WorkItem|Learn about tasks.'),
- tasksInformationTitle: s__('WorkItem|Introducing tasks'),
- tasksInformationBody: s__(
- 'WorkItem|Use tasks to break down your work in an issue into smaller pieces. %{learnMoreLink}',
- ),
- },
- helpPageLinks: {
- tasksDocLinkPath: helpPagePath('user/tasks'),
- },
- components: {
- GlAlert,
- GlSprintf,
- GlLink,
- },
- props: {
- showInfoBanner: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- emits: ['work-item-banner-dismissed'],
-};
-</script>
-
-<template>
- <section class="gl-display-block gl-mb-2">
- <gl-alert
- v-if="showInfoBanner"
- variant="tip"
- :title="$options.i18n.tasksInformationTitle"
- data-testid="work-item-information"
- class="gl-mt-3"
- @dismiss="$emit('work-item-banner-dismissed')"
- >
- <gl-sprintf :message="$options.i18n.tasksInformationBody">
- <template #learnMoreLink>
- <gl-link :href="$options.helpPageLinks.tasksDocLinkPath">{{
- $options.i18n.learnTasksLinkText
- }}</gl-link>
- </template>
- ></gl-sprintf
- >
- </gl-alert>
- </section>
-</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_labels.vue b/app/assets/javascripts/work_items/components/work_item_labels.vue
index 22af3c653e9..45fb0f7f21a 100644
--- a/app/assets/javascripts/work_items/components/work_item_labels.vue
+++ b/app/assets/javascripts/work_items/components/work_item_labels.vue
@@ -3,8 +3,8 @@ import { GlTokenSelector, GlLabel, GlSkeletonLoader } from '@gitlab/ui';
import { debounce, uniqueId, without } from 'lodash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
-import labelSearchQuery from '~/vue_shared/components/sidebar/labels_select_widget/graphql/project_labels.query.graphql';
-import LabelItem from '~/vue_shared/components/sidebar/labels_select_widget/label_item.vue';
+import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
+import LabelItem from '~/sidebar/components/labels/labels_select_widget/label_item.vue';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
@@ -83,7 +83,7 @@ export default {
return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
},
skip() {
- return !this.workItemId;
+ return !this.queryVariables.id && !this.queryVariables.iid;
},
error() {
this.$emit('error', i18n.fetchError);
diff --git a/app/assets/javascripts/work_items/components/work_item_links/index.js b/app/assets/javascripts/work_items/components/work_item_links/index.js
index 0251dcc33fa..edad0e9b616 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/index.js
+++ b/app/assets/javascripts/work_items/components/work_item_links/index.js
@@ -17,6 +17,7 @@ export default function initWorkItemLinks() {
wiHasIssueWeightsFeature,
iid,
wiHasIterationsFeature,
+ wiHasIssuableHealthStatusFeature,
} = workItemLinksRoot.dataset;
// eslint-disable-next-line no-new
@@ -33,6 +34,7 @@ export default function initWorkItemLinks() {
fullPath: projectPath,
hasIssueWeightsFeature: wiHasIssueWeightsFeature,
hasIterationsFeature: wiHasIterationsFeature,
+ hasIssuableHealthStatusFeature: wiHasIssuableHealthStatusFeature,
},
render: (createElement) =>
createElement('work-item-links', {
diff --git a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
new file mode 100644
index 00000000000..dc5bcdc3dcc
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
@@ -0,0 +1,66 @@
+<script>
+import { GlDropdown, GlDropdownSectionHeader, GlDropdownItem, GlDropdownDivider } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+
+const objectiveActionItems = [
+ {
+ title: s__('OKR|New objective'),
+ eventName: 'showCreateObjectiveForm',
+ },
+ {
+ title: s__('OKR|Existing objective'),
+ eventName: 'showAddObjectiveForm',
+ },
+];
+
+const keyResultActionItems = [
+ {
+ title: s__('OKR|New key result'),
+ eventName: 'showCreateKeyResultForm',
+ },
+ {
+ title: s__('OKR|Existing key result'),
+ eventName: 'showAddKeyResultForm',
+ },
+];
+
+export default {
+ keyResultActionItems,
+ objectiveActionItems,
+ components: {
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlDropdownDivider,
+ },
+ methods: {
+ change({ eventName }) {
+ this.$emit(eventName);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown :text="__('Add')" size="small" right>
+ <gl-dropdown-section-header>{{ __('Objective') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in $options.objectiveActionItems"
+ :key="item.eventName"
+ @click="change(item)"
+ >
+ {{ item.title }}
+ </gl-dropdown-item>
+
+ <gl-dropdown-divider />
+ <gl-dropdown-section-header>{{ __('Key result') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="item in $options.keyResultActionItems"
+ :key="item.eventName"
+ @click="change(item)"
+ >
+ {{ item.title }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
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 34874908f9b..763f2f338a3 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
@@ -1,19 +1,35 @@
<script>
-import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlButton, GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
-import { STATE_OPEN } from '../../constants';
+import {
+ STATE_OPEN,
+ TASK_TYPE_NAME,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WIDGET_TYPE_MILESTONE,
+ WIDGET_TYPE_HIERARCHY,
+ WIDGET_TYPE_ASSIGNEES,
+ WIDGET_TYPE_LABELS,
+ WORK_ITEM_NAME_TO_ICON_MAP,
+} from '../../constants';
+import getWorkItemTreeQuery from '../../graphql/work_item_tree.query.graphql';
+import WorkItemLinkChildMetadata from './work_item_link_child_metadata.vue';
import WorkItemLinksMenu from './work_item_links_menu.vue';
+import WorkItemTreeChildren from './work_item_tree_children.vue';
export default {
components: {
+ GlLink,
GlButton,
GlIcon,
RichTimestampTooltip,
+ WorkItemLinkChildMetadata,
WorkItemLinksMenu,
+ WorkItemTreeChildren,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -35,16 +51,48 @@ export default {
type: Object,
required: true,
},
+ hasIndirectChildren: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isExpanded: false,
+ children: [],
+ isLoadingChildren: false,
+ };
},
computed: {
+ canHaveChildren() {
+ return this.workItemType === WORK_ITEM_TYPE_VALUE_OBJECTIVE;
+ },
+ allowsScopedLabels() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.allowsScopedLabels;
+ },
isItemOpen() {
return this.childItem.state === STATE_OPEN;
},
- iconClass() {
- return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ childItemType() {
+ return this.childItem.workItemType.name;
},
iconName() {
- return this.isItemOpen ? 'issue-open-m' : 'issue-close';
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isItemOpen ? 'issue-open-m' : 'issue-close';
+ }
+ return WORK_ITEM_NAME_TO_ICON_MAP[this.childItemType];
+ },
+ iconClass() {
+ if (this.childItemType === TASK_TYPE_NAME) {
+ return this.isItemOpen ? 'gl-text-green-500' : 'gl-text-blue-500';
+ }
+ return '';
},
stateTimestamp() {
return this.isItemOpen ? this.childItem.createdAt : this.childItem.closedAt;
@@ -55,55 +103,161 @@ export default {
childPath() {
return `/${this.projectPath}/-/work_items/${getIdFromGraphQLId(this.childItem.id)}`;
},
+ hasChildren() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_HIERARCHY)?.hasChildren;
+ },
+ chevronType() {
+ return this.isExpanded ? 'chevron-down' : 'chevron-right';
+ },
+ chevronTooltip() {
+ return this.isExpanded ? __('Collapse') : __('Expand');
+ },
+ hasMetadata() {
+ return this.milestone || this.assignees.length > 0 || this.labels.length > 0;
+ },
+ milestone() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_MILESTONE)?.milestone;
+ },
+ assignees() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_ASSIGNEES)?.assignees?.nodes || [];
+ },
+ labels() {
+ return this.getWidgetByType(this.childItem, WIDGET_TYPE_LABELS)?.labels?.nodes || [];
+ },
+ },
+ methods: {
+ toggleItem() {
+ this.isExpanded = !this.isExpanded;
+ if (this.children.length === 0 && this.hasChildren) {
+ this.fetchChildren();
+ }
+ },
+ getWidgetByType(workItem, widgetType) {
+ return workItem?.widgets?.find((widget) => widget.type === widgetType);
+ },
+ async fetchChildren() {
+ this.isLoadingChildren = true;
+ try {
+ const { data } = await this.$apollo.query({
+ query: getWorkItemTreeQuery,
+ variables: {
+ id: this.childItem.id,
+ },
+ });
+ this.children = this.getWidgetByType(data?.workItem, WIDGET_TYPE_HIERARCHY).children.nodes;
+ } catch (error) {
+ this.isExpanded = !this.isExpanded;
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while fetching children.'),
+ captureError: true,
+ error,
+ });
+ } finally {
+ this.isLoadingChildren = false;
+ }
+ },
},
};
</script>
<template>
- <div
- class="gl-relative gl-display-flex gl-overflow-break-word gl-min-w-0 gl-bg-white gl-mb-3 gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
- data-testid="links-child"
- >
- <div class="gl-overflow-hidden gl-display-flex gl-align-items-center gl-flex-grow-1">
- <span :id="`stateIcon-${childItem.id}`" class="gl-mr-3" data-testid="item-status-icon">
- <gl-icon :name="iconName" :class="iconClass" :aria-label="stateTimestampTypeText" />
- </span>
- <rich-timestamp-tooltip
- :target="`stateIcon-${childItem.id}`"
- :raw-timestamp="stateTimestamp"
- :timestamp-type-text="stateTimestampTypeText"
- />
- <gl-icon
- v-if="childItem.confidential"
- v-gl-tooltip.top
- name="eye-slash"
- class="gl-mr-2 gl-text-orange-500"
- data-testid="confidential-icon"
- :aria-label="__('Confidential')"
- :title="__('Confidential')"
- />
- <gl-button
- :href="childPath"
- category="tertiary"
- variant="link"
- class="gl-text-truncate gl-max-w-80 gl-text-black-normal!"
- @click="$emit('click', childItem.id, $event)"
- @mouseover="$emit('mouseover', childItem.id, $event)"
- @mouseout="$emit('mouseout', childItem.id, $event)"
- >
- {{ childItem.title }}
- </gl-button>
- </div>
+ <div>
<div
- v-if="canUpdate"
- class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
+ class="gl-display-flex gl-align-items-flex-start gl-mb-3"
+ :class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
>
- <work-item-links-menu
- :work-item-id="childItem.id"
- :parent-work-item-id="issuableGid"
- data-testid="links-menu"
- @removeChild="$emit('remove', childItem.id)"
+ <gl-button
+ v-if="hasChildren"
+ v-gl-tooltip.viewport
+ :title="chevronTooltip"
+ :aria-label="chevronTooltip"
+ :icon="chevronType"
+ category="tertiary"
+ :loading="isLoadingChildren"
+ class="gl-px-0! gl-py-3! gl-mr-3"
+ data-testid="expand-child"
+ @click="toggleItem"
/>
+ <div
+ class="gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-bg-white gl-py-3 gl-px-4 gl-border gl-border-gray-100 gl-rounded-base gl-line-height-32"
+ data-testid="links-child"
+ >
+ <span
+ :id="`stateIcon-${childItem.id}`"
+ class="gl-mr-3"
+ :class="{ 'gl-display-flex': hasMetadata }"
+ data-testid="item-status-icon"
+ >
+ <gl-icon
+ class="gl-text-secondary"
+ :class="iconClass"
+ :name="iconName"
+ :aria-label="stateTimestampTypeText"
+ />
+ </span>
+ <div
+ class="gl-display-flex gl-flex-grow-1"
+ :class="{
+ 'gl-flex-direction-column gl-align-items-flex-start': hasMetadata,
+ 'gl-align-items-center': !hasMetadata,
+ }"
+ >
+ <div class="gl-display-flex">
+ <rich-timestamp-tooltip
+ :target="`stateIcon-${childItem.id}`"
+ :raw-timestamp="stateTimestamp"
+ :timestamp-type-text="stateTimestampTypeText"
+ />
+ <gl-icon
+ v-if="childItem.confidential"
+ v-gl-tooltip.top
+ name="eye-slash"
+ class="gl-mr-2 gl-text-orange-500"
+ data-testid="confidential-icon"
+ :aria-label="__('Confidential')"
+ :title="__('Confidential')"
+ />
+ <gl-link
+ :href="childPath"
+ class="gl-overflow-wrap-break gl-line-height-normal gl-text-black-normal! gl-font-weight-bold"
+ data-testid="item-title"
+ @click="$emit('click', $event)"
+ @mouseover="$emit('mouseover')"
+ @mouseout="$emit('mouseout')"
+ >
+ {{ childItem.title }}
+ </gl-link>
+ </div>
+ <work-item-link-child-metadata
+ v-if="hasMetadata"
+ :allows-scoped-labels="allowsScopedLabels"
+ :milestone="milestone"
+ :assignees="assignees"
+ :labels="labels"
+ class="gl-mt-3"
+ />
+ </div>
+ <div
+ v-if="canUpdate"
+ class="gl-ml-0 gl-sm-ml-auto! gl-display-inline-flex gl-align-items-center"
+ >
+ <work-item-links-menu
+ :work-item-id="childItem.id"
+ :parent-work-item-id="issuableGid"
+ data-testid="links-menu"
+ @removeChild="$emit('removeChild', childItem.id)"
+ />
+ </div>
+ </div>
</div>
+ <work-item-tree-children
+ v-if="isExpanded"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :work-item-id="issuableGid"
+ :work-item-type="workItemType"
+ :children="children"
+ @removeChild="fetchChildren"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
new file mode 100644
index 00000000000..7be7e1f3496
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child_metadata.vue
@@ -0,0 +1,123 @@
+<script>
+import { GlLabel, GlAvatar, GlAvatarLink, GlAvatarsInline, GlTooltipDirective } from '@gitlab/ui';
+
+import { s__, sprintf } from '~/locale';
+import { isScopedLabel } from '~/lib/utils/common_utils';
+
+import ItemMilestone from '~/issuable/components/issue_milestone.vue';
+
+export default {
+ components: {
+ GlLabel,
+ GlAvatar,
+ GlAvatarLink,
+ GlAvatarsInline,
+ ItemMilestone,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ allowsScopedLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ milestone: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ assignees: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ labels: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ assigneesCollapsedTooltip() {
+ if (this.assignees.length > 2) {
+ return sprintf(s__('WorkItem|%{count} more assignees'), {
+ count: this.assignees.length - 2,
+ });
+ }
+ return '';
+ },
+ assigneesContainerClass() {
+ if (this.assignees.length === 2) {
+ return 'fixed-width-avatars-2';
+ } else if (this.assignees.length > 2) {
+ return 'fixed-width-avatars-3';
+ }
+ return '';
+ },
+ labelsContainerClass() {
+ if (this.milestone || this.assignees.length) {
+ return 'gl-sm-ml-5';
+ }
+ return '';
+ },
+ },
+ methods: {
+ showScopedLabel(label) {
+ return isScopedLabel(label) && this.allowsScopedLabels;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-wrap gl-align-items-center">
+ <item-milestone
+ v-if="milestone"
+ :milestone="milestone"
+ class="gl-display-flex gl-align-items-center gl-mr-5 gl-max-w-15 gl-text-secondary! gl-cursor-help! gl-text-decoration-none!"
+ />
+ <gl-avatars-inline
+ v-if="assignees.length"
+ :avatars="assignees"
+ :collapsed="true"
+ :max-visible="2"
+ :avatar-size="24"
+ badge-tooltip-prop="name"
+ :badge-sr-only-text="assigneesCollapsedTooltip"
+ :class="assigneesContainerClass"
+ >
+ <template #avatar="{ avatar }">
+ <gl-avatar-link v-gl-tooltip target="blank" :href="avatar.webUrl" :title="avatar.name">
+ <gl-avatar :src="avatar.avatarUrl" :size="24" />
+ </gl-avatar-link>
+ </template>
+ </gl-avatars-inline>
+ <div v-if="labels.length" class="gl-display-flex gl-flex-wrap" :class="labelsContainerClass">
+ <gl-label
+ v-for="label in labels"
+ :key="label.id"
+ :title="label.title"
+ :background-color="label.color"
+ :description="label.description"
+ :scoped="showScopedLabel(label)"
+ class="gl-mt-2 gl-sm-mt-0 gl-mr-2 gl-mb-auto gl-label-sm"
+ tooltip-placement="top"
+ />
+ </div>
+ </div>
+</template>
+
+<style scoped>
+/**
+ * These overrides are needed to address https://gitlab.com/gitlab-org/gitlab-ui/-/issues/865
+ */
+.fixed-width-avatars-2 {
+ width: 42px !important;
+}
+
+.fixed-width-avatars-3 {
+ width: 67px !important;
+}
+</style>
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 3d469b790a1..faadb5fa6fa 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
@@ -9,13 +9,15 @@ import {
GlTooltipDirective,
} from '@gitlab/ui';
import { produce } from 'immer';
+import { isEmpty } from 'lodash';
import { s__ } from '~/locale';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
-import { isMetaKey } from '~/lib/utils/common_utils';
-import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
+import { isMetaKey, parseBoolean } from '~/lib/utils/common_utils';
+import { setUrlParams, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
import {
FORM_TYPES,
@@ -26,6 +28,7 @@ import {
import getWorkItemLinksQuery from '../../graphql/work_item_links.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import workItemQuery from '../../graphql/work_item.query.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
import WorkItemDetailModal from '../work_item_detail_modal.vue';
import WorkItemLinkChild from './work_item_link_child.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
@@ -45,6 +48,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagMixin()],
inject: ['projectPath', 'iid'],
props: {
workItemId: {
@@ -72,6 +76,18 @@ export default {
error(e) {
this.error = e.message || this.$options.i18n.fetchError;
},
+ async result() {
+ const { id, iid } = this.childUrlParams;
+ this.activeChild = this.fetchByIid
+ ? this.children.find((child) => child.iid === iid) ?? {}
+ : this.children.find((child) => child.id === id) ?? {};
+ await this.$nextTick();
+ if (!isEmpty(this.activeChild)) {
+ this.$refs.modal.show();
+ return;
+ }
+ this.updateWorkItemIdUrlQuery();
+ },
},
parentIssue: {
query: getIssueDetailsQuery,
@@ -90,7 +106,7 @@ export default {
return {
isShownAddForm: false,
isOpen: true,
- activeChildId: null,
+ activeChild: {},
activeToast: null,
prefetchedWorkItem: null,
error: undefined,
@@ -139,6 +155,29 @@ export default {
childrenCountLabel() {
return this.isLoading && this.children.length === 0 ? '...' : this.children.length;
},
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
+ },
+ childUrlParams() {
+ const params = {};
+ if (this.fetchByIid) {
+ const iid = getParameterByName('work_item_iid');
+ if (iid) {
+ params.iid = iid;
+ }
+ } else {
+ const workItemId = getParameterByName('work_item_id');
+ if (workItemId) {
+ params.id = convertToGraphQLId(TYPE_WORK_ITEM, workItemId);
+ }
+ }
+ return params;
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.childUrlParams)) {
+ this.addWorkItemQuery(this.childUrlParams);
+ }
},
methods: {
toggle() {
@@ -159,29 +198,29 @@ export default {
const { defaultClient: client } = this.$apollo.provider.clients;
this.toggleChildFromCache(child, child.id, client);
},
- openChild(childItemId, e) {
+ openChild(child, e) {
if (isMetaKey(e)) {
return;
}
e.preventDefault();
- this.activeChildId = childItemId;
+ this.activeChild = child;
this.$refs.modal.show();
- this.updateWorkItemIdUrlQuery(childItemId);
+ this.updateWorkItemIdUrlQuery(child);
},
- closeModal() {
- this.activeChildId = null;
- this.updateWorkItemIdUrlQuery(undefined);
+ async closeModal() {
+ this.activeChild = {};
+ this.updateWorkItemIdUrlQuery();
},
handleWorkItemDeleted(childId) {
const { defaultClient: client } = this.$apollo.provider.clients;
this.toggleChildFromCache(null, childId, client);
this.activeToast = this.$toast.show(s__('WorkItem|Task deleted'));
},
- updateWorkItemIdUrlQuery(childItemId) {
- updateHistory({
- url: setUrlParams({ work_item_id: getIdFromGraphQLId(childItemId) }),
- replace: true,
- });
+ updateWorkItemIdUrlQuery({ id, iid } = {}) {
+ const params = this.fetchByIid
+ ? { work_item_iid: iid }
+ : { work_item_id: getIdFromGraphQLId(id) };
+ updateHistory({ url: setUrlParams(params), replace: true });
},
toggleChildFromCache(workItem, childId, store) {
const sourceData = store.readQuery({
@@ -235,16 +274,31 @@ export default {
});
}
},
- prefetchWorkItem(id) {
+ addWorkItemQuery({ id, iid }) {
+ const variables = this.fetchByIid
+ ? {
+ fullPath: this.projectPath,
+ iid,
+ }
+ : {
+ id,
+ };
+ this.$apollo.addSmartQuery('prefetchedWorkItem', {
+ query() {
+ return this.fetchByIid ? workItemByIidQuery : workItemQuery;
+ },
+ variables,
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ });
+ },
+ prefetchWorkItem({ id, iid }) {
this.prefetch = setTimeout(
- () =>
- this.$apollo.addSmartQuery('prefetchedWorkItem', {
- query: workItemQuery,
- variables: {
- id,
- },
- update: (data) => data.workItem,
- }),
+ () => this.addWorkItemQuery({ id, iid }),
DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
);
},
@@ -355,16 +409,17 @@ export default {
:can-update="canUpdate"
:issuable-gid="issuableGid"
:child-item="child"
- @click="openChild"
- @mouseover="prefetchWorkItem"
+ @click="openChild(child, $event)"
+ @mouseover="prefetchWorkItem(child)"
@mouseout="clearPrefetching"
- @remove="removeChild"
+ @removeChild="removeChild"
/>
<work-item-detail-modal
ref="modal"
- :work-item-id="activeChildId"
+ :work-item-id="activeChild.id"
+ :work-item-iid="activeChild.iid"
@close="closeModal"
- @workItemDeleted="handleWorkItemDeleted(activeChildId)"
+ @workItemDeleted="handleWorkItemDeleted(activeChild.id)"
/>
</template>
</div>
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 095ea86e0d8..5cf0c4154bb 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
@@ -9,7 +9,16 @@ import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_ty
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 { FORM_TYPES, TASK_TYPE_NAME } from '../../constants';
+import {
+ FORM_TYPES,
+ 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,
+ sprintfWorkItem,
+} from '../../constants';
export default {
components: {
@@ -52,6 +61,11 @@ export default {
type: String,
required: true,
},
+ childrenType: {
+ type: String,
+ required: false,
+ default: WORK_ITEM_TYPE_ENUM_TASK,
+ },
},
apollo: {
workItemTypes: {
@@ -71,7 +85,7 @@ export default {
return {
projectPath: this.projectPath,
searchTerm: this.search?.title || this.search,
- types: ['TASK'],
+ types: [this.childrenType],
in: this.search ? 'TITLE' : undefined,
};
},
@@ -79,7 +93,9 @@ export default {
return !this.searchStarted;
},
update(data) {
- return data.workspace.workItems.nodes.filter((wi) => !this.childrenIds.includes(wi.id));
+ return data.workspace.workItems.nodes.filter(
+ (wi) => !this.childrenIds.includes(wi.id) && this.issuableGid !== wi.id,
+ );
},
},
},
@@ -99,14 +115,14 @@ export default {
let workItemInput = {
title: this.search?.title || this.search,
projectPath: this.projectPath,
- workItemTypeId: this.taskWorkItemType,
+ workItemTypeId: this.childWorkItemType,
hierarchyWidget: {
parentId: this.issuableGid,
},
confidential: this.parentConfidential,
};
- if (this.associateMilestone) {
+ if (this.parentMilestoneId) {
workItemInput = {
...workItemInput,
milestoneWidget: {
@@ -114,46 +130,62 @@ export default {
},
};
}
+
+ if (this.associateIteration) {
+ workItemInput = {
+ ...workItemInput,
+ iterationWidget: {
+ iterationId: this.parentIterationId,
+ },
+ };
+ }
+
return workItemInput;
},
+ workItemsMvcEnabled() {
+ return this.glFeatures.workItemsMvc;
+ },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
isCreateForm() {
return this.formType === FORM_TYPES.create;
},
+ childrenTypeName() {
+ return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
+ },
addOrCreateButtonLabel() {
if (this.isCreateForm) {
- return this.$options.i18n.createChildOptionLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_CREATE_BUTTON_LABEL, this.childrenTypeName);
} else if (this.workItemsToAdd.length > 1) {
- return this.$options.i18n.addTasksButtonLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL, this.childrenTypeName);
}
- return this.$options.i18n.addTaskButtonLabel;
+ return sprintfWorkItem(I18N_WORK_ITEM_ADD_BUTTON_LABEL, this.childrenTypeName);
},
addOrCreateMethod() {
return this.isCreateForm ? this.createChild : this.addChild;
},
- taskWorkItemType() {
- return this.workItemTypes.find((type) => type.name === TASK_TYPE_NAME)?.id;
+ childWorkItemType() {
+ return this.workItemTypes.find((type) => type.name === this.childrenTypeName)?.id;
},
parentIterationId() {
return this.parentIteration?.id;
},
associateIteration() {
- return this.parentIterationId && this.hasIterationsFeature && this.workItemsMvc2Enabled;
+ return this.parentIterationId && this.hasIterationsFeature;
},
parentMilestoneId() {
return this.parentMilestone?.id;
},
- associateMilestone() {
- return this.parentMilestoneId && this.workItemsMvc2Enabled;
- },
isSubmitButtonDisabled() {
return this.isCreateForm ? this.search.length === 0 : this.workItemsToAdd.length === 0;
},
isLoading() {
return this.$apollo.queries.availableWorkItems.loading;
},
+ addInputPlaceholder() {
+ return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ },
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
@@ -206,13 +238,6 @@ export default {
} else {
this.unsetError();
this.$emit('addWorkItemChild', data.workItemCreate.workItem);
- /**
- * call update mutation only when there is an iteration associated with the issue
- */
- // TODO: setting the iteration should be moved to the creation mutation once the backend is done
- if (this.associateIteration) {
- this.addIterationToWorkItem(data.workItemCreate.workItem.id);
- }
}
})
.catch(() => {
@@ -223,19 +248,6 @@ export default {
this.childToCreateTitle = null;
});
},
- async addIterationToWorkItem(workItemId) {
- await this.$apollo.mutate({
- mutation: updateWorkItemMutation,
- variables: {
- input: {
- id: workItemId,
- iterationWidget: {
- iterationId: this.parentIterationId,
- },
- },
- },
- });
- },
setSearchKey(value) {
this.search = value;
},
@@ -253,17 +265,13 @@ export default {
},
i18n: {
inputLabel: __('Title'),
- addTaskButtonLabel: s__('WorkItem|Add task'),
- addTasksButtonLabel: s__('WorkItem|Add tasks'),
addChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to add a child. Please try again.',
),
- createChildOptionLabel: s__('WorkItem|Create task'),
createChildErrorMessage: s__(
'WorkItem|Something went wrong when trying to create a child. Please try again.',
),
createPlaceholder: s__('WorkItem|Add a title'),
- addPlaceholder: s__('WorkItem|Search existing tasks'),
fieldValidationMessage: __('Maximum of 255 characters'),
},
};
@@ -296,7 +304,7 @@ export default {
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
- :placeholder="$options.i18n.addPlaceholder"
+ :placeholder="addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
class="gl-mb-4"
data-testid="work-item-token-select-input"
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
new file mode 100644
index 00000000000..f06de2ca048
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -0,0 +1,244 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+import { __ } from '~/locale';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_WORK_ITEM } from '~/graphql_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
+import {
+ FORM_TYPES,
+ WIDGET_TYPE_HIERARCHY,
+ WORK_ITEMS_TREE_TEXT_MAP,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+} from '../../constants';
+import workItemQuery from '../../graphql/work_item.query.graphql';
+import workItemByIidQuery from '../../graphql/work_item_by_iid.query.graphql';
+import OkrActionsSplitButton from './okr_actions_split_button.vue';
+import WorkItemLinksForm from './work_item_links_form.vue';
+import WorkItemLinkChild from './work_item_link_child.vue';
+
+export default {
+ FORM_TYPES,
+ WORK_ITEMS_TREE_TEXT_MAP,
+ WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+ components: {
+ GlButton,
+ OkrActionsSplitButton,
+ WorkItemLinksForm,
+ WorkItemLinkChild,
+ },
+ mixins: [glFeatureFlagMixin()],
+ props: {
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ children: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isShownAddForm: false,
+ isOpen: true,
+ error: null,
+ formType: null,
+ childType: null,
+ prefetchedWorkItem: null,
+ };
+ },
+ computed: {
+ toggleIcon() {
+ return this.isOpen ? 'chevron-lg-up' : 'chevron-lg-down';
+ },
+ toggleLabel() {
+ return this.isOpen ? __('Collapse') : __('Expand');
+ },
+ fetchByIid() {
+ return this.glFeatures.useIidInWorkItemsPath && parseBoolean(getParameterByName('iid_path'));
+ },
+ childrenIds() {
+ return this.children.map((c) => c.id);
+ },
+ hasIndirectChildren() {
+ return this.children
+ .map(
+ (child) => child.widgets?.find((widget) => widget.type === WIDGET_TYPE_HIERARCHY) || {},
+ )
+ .some((hierarchy) => hierarchy.hasChildren);
+ },
+ childUrlParams() {
+ const params = {};
+ if (this.fetchByIid) {
+ const iid = getParameterByName('work_item_iid');
+ if (iid) {
+ params.iid = iid;
+ }
+ } else {
+ const workItemId = getParameterByName('work_item_id');
+ if (workItemId) {
+ params.id = convertToGraphQLId(TYPE_WORK_ITEM, workItemId);
+ }
+ }
+ return params;
+ },
+ },
+ mounted() {
+ if (!isEmpty(this.childUrlParams)) {
+ this.addWorkItemQuery(this.childUrlParams);
+ }
+ },
+ methods: {
+ toggle() {
+ this.isOpen = !this.isOpen;
+ },
+ showAddForm(formType, childType) {
+ this.isOpen = true;
+ this.isShownAddForm = true;
+ this.formType = formType;
+ this.childType = childType;
+ this.$nextTick(() => {
+ this.$refs.wiLinksForm.$refs.wiTitleInput?.$el.focus();
+ });
+ },
+ hideAddForm() {
+ this.isShownAddForm = false;
+ },
+ addWorkItemQuery({ id, iid }) {
+ const variables = this.fetchByIid
+ ? {
+ fullPath: this.projectPath,
+ iid,
+ }
+ : {
+ id,
+ };
+ this.$apollo.addSmartQuery('prefetchedWorkItem', {
+ query() {
+ return this.fetchByIid ? workItemByIidQuery : workItemQuery;
+ },
+ variables,
+ update(data) {
+ return this.fetchByIid ? data.workspace.workItems.nodes[0] : data.workItem;
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ });
+ },
+ prefetchWorkItem({ id, iid }) {
+ if (this.workItemType !== WORK_ITEM_TYPE_VALUE_OBJECTIVE) {
+ this.prefetch = setTimeout(
+ () => this.addWorkItemQuery({ id, iid }),
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ );
+ }
+ },
+ clearPrefetching() {
+ if (this.prefetch) {
+ clearTimeout(this.prefetch);
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100 gl-bg-gray-10 gl-mt-4"
+ data-testid="work-item-tree"
+ >
+ <div
+ class="gl-px-5 gl-py-3 gl-display-flex gl-justify-content-space-between"
+ :class="{ 'gl-border-b-1 gl-border-b-solid gl-border-b-gray-100': isOpen }"
+ >
+ <div class="gl-display-flex gl-flex-grow-1">
+ <h5 class="gl-m-0 gl-line-height-24">
+ {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].title }}
+ </h5>
+ </div>
+ <okr-actions-split-button
+ @showCreateObjectiveForm="
+ showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
+ "
+ @showAddObjectiveForm="
+ showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
+ "
+ @showCreateKeyResultForm="
+ showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
+ "
+ @showAddKeyResultForm="
+ showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
+ "
+ />
+ <div class="gl-border-l-1 gl-border-l-solid gl-border-l-gray-100 gl-pl-3 gl-ml-3">
+ <gl-button
+ category="tertiary"
+ size="small"
+ :icon="toggleIcon"
+ :aria-label="toggleLabel"
+ data-testid="toggle-tree"
+ @click="toggle"
+ />
+ </div>
+ </div>
+ <div
+ v-if="isOpen"
+ class="gl-bg-gray-10 gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
+ :class="{ 'gl-p-5 gl-pb-3': !error }"
+ data-testid="tree-body"
+ >
+ <div v-if="!isShownAddForm && !error && children.length === 0" data-testid="tree-empty">
+ <p class="gl-mb-3">
+ {{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
+ </p>
+ </div>
+ <work-item-links-form
+ v-if="isShownAddForm"
+ ref="wiLinksForm"
+ data-testid="add-tree-form"
+ :issuable-gid="workItemId"
+ :form-type="formType"
+ :children-type="childType"
+ :children-ids="childrenIds"
+ @addWorkItemChild="$emit('addWorkItemChild', $event)"
+ @cancel="hideAddForm"
+ />
+ <work-item-link-child
+ v-for="child in children"
+ :key="child.id"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :issuable-gid="workItemId"
+ :child-item="child"
+ :work-item-type="workItemType"
+ :has-indirect-children="hasIndirectChildren"
+ @mouseover="prefetchWorkItem(child)"
+ @mouseout="clearPrefetching"
+ @removeChild="$emit('removeChild', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
new file mode 100644
index 00000000000..911cac4de88
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree_children.vue
@@ -0,0 +1,68 @@
+<script>
+import { createAlert } from '~/flash';
+import { s__ } from '~/locale';
+
+import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
+
+export default {
+ components: {
+ WorkItemLinkChild: () => import('./work_item_link_child.vue'),
+ },
+ props: {
+ workItemType: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ children: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ methods: {
+ async updateWorkItem(childId) {
+ try {
+ await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: { input: { id: childId, hierarchyWidget: { parentId: null } } },
+ });
+ this.$emit('removeChild');
+ } catch (error) {
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while removing a child item.'),
+ captureError: true,
+ error,
+ });
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-ml-6">
+ <work-item-link-child
+ v-for="child in children"
+ :key="child.id"
+ :project-path="projectPath"
+ :can-update="canUpdate"
+ :issuable-gid="workItemId"
+ :child-item="child"
+ :work-item-type="workItemType"
+ @removeChild="updateWorkItem"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index a8d3b57aae0..6ed230b8ad4 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -13,6 +13,7 @@ import { debounce } from 'lodash';
import Tracking from '~/tracking';
import { s__, __ } from '~/locale';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { MILESTONE_STATE } from '~/sidebar/constants';
import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import {
@@ -118,6 +119,7 @@ export default {
return {
'gl-text-gray-500!': this.canUpdate && this.isNoMilestone,
'is-not-focused': !this.isFocused,
+ 'gl-min-w-20': true,
};
},
},
@@ -139,6 +141,7 @@ export default {
return {
fullPath: this.fullPath,
title: this.searchTerm,
+ state: MILESTONE_STATE.ACTIVE,
first: 20,
};
},
@@ -214,9 +217,10 @@ export default {
<template>
<gl-form-group
- class="work-item-dropdown"
+ class="work-item-dropdown gl-flex-nowrap"
:label="$options.i18n.MILESTONE"
- label-class="gl-pb-0! gl-overflow-wrap-break gl-mt-3"
+ label-for="milestone-value"
+ label-class="gl-pb-0! gl-mt-3 gl-overflow-wrap-break"
label-cols="3"
label-cols-lg="2"
>
@@ -229,6 +233,8 @@ export default {
</span>
<gl-dropdown
v-else
+ id="milestone-value"
+ class="gl-pl-0 gl-max-w-full"
:toggle-class="dropdownClasses"
:text="dropdownText"
:loading="updateInProgress"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
new file mode 100644
index 00000000000..91e90589a93
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -0,0 +1,109 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import SystemNote from '~/work_items/components/notes/system_note.vue';
+import { i18n, DEFAULT_PAGE_SIZE_NOTES } from '~/work_items/constants';
+import { getWorkItemNotesQuery } from '~/work_items/utils';
+
+export default {
+ i18n: {
+ ACTIVITY_LABEL: s__('WorkItem|Activity'),
+ },
+ loader: {
+ repeat: 10,
+ width: 1000,
+ height: 40,
+ },
+ components: {
+ SystemNote,
+ GlSkeletonLoader,
+ },
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ queryVariables: {
+ type: Object,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ fetchByIid: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ areNotesLoading() {
+ return this.$apollo.queries.workItemNotes.loading;
+ },
+ notes() {
+ return this.workItemNotes?.nodes;
+ },
+ pageInfo() {
+ return this.workItemNotes?.pageInfo;
+ },
+ },
+ apollo: {
+ workItemNotes: {
+ query() {
+ return getWorkItemNotesQuery(this.fetchByIid);
+ },
+ context: {
+ isSingleRequest: true,
+ },
+ variables() {
+ return {
+ ...this.queryVariables,
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ };
+ },
+ update(data) {
+ const workItemWidgets = this.fetchByIid
+ ? data.workspace?.workItems?.nodes[0]?.widgets
+ : data.workItem?.widgets;
+ return workItemWidgets.find((widget) => widget.type === 'NOTES').discussions || [];
+ },
+ skip() {
+ return !this.queryVariables.id && !this.queryVariables.iid;
+ },
+ error() {
+ this.$emit('error', i18n.fetchError);
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-border-t gl-mt-5">
+ <label class="gl-mb-0">{{ $options.i18n.ACTIVITY_LABEL }}</label>
+ <div v-if="areNotesLoading" class="gl-mt-5">
+ <gl-skeleton-loader
+ v-for="index in $options.loader.repeat"
+ :key="index"
+ :width="$options.loader.width"
+ :height="$options.loader.height"
+ preserve-aspect-ratio="xMinYMax meet"
+ >
+ <circle cx="20" cy="20" r="16" />
+ <rect width="500" x="45" y="15" height="10" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <div v-else class="issuable-discussion gl-mb-5 work-item-notes">
+ <template v-if="notes && notes.length">
+ <ul class="notes main-notes-list timeline">
+ <system-note
+ v-for="note in notes"
+ :key="note.notes.nodes[0].id"
+ :note="note.notes.nodes[0]"
+ />
+ </ul>
+ </template>
+ </div>
+ </div>
+</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 96a6493357c..32678e29fa4 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
@@ -33,6 +33,11 @@ export default {
},
computed: {
iconName() {
+ // TODO: Remove this once https://gitlab.com/gitlab-org/gitlab-svgs/-/merge_requests/865
+ // is merged and updated in GitLab repo.
+ if (this.workItemIconName === 'issue-type-keyresult') {
+ return 'issue-type-key-result';
+ }
return (
this.workItemIconName || WORK_ITEMS_TYPE_MAP[this.workItemType]?.icon || 'issue-type-issue'
);
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 8b47c24de7d..3cd17f4d360 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -16,17 +16,23 @@ export const WIDGET_TYPE_DESCRIPTION = 'DESCRIPTION';
export const WIDGET_TYPE_LABELS = 'LABELS';
export const WIDGET_TYPE_START_AND_DUE_DATE = 'START_AND_DUE_DATE';
export const WIDGET_TYPE_WEIGHT = 'WEIGHT';
+export const WIDGET_TYPE_PROGRESS = 'PROGRESS';
export const WIDGET_TYPE_HIERARCHY = 'HIERARCHY';
export const WIDGET_TYPE_MILESTONE = 'MILESTONE';
export const WIDGET_TYPE_ITERATION = 'ITERATION';
-
-export const WORK_ITEM_VIEWED_STORAGE_KEY = 'gl-show-work-item-banner';
+export const WIDGET_TYPE_NOTES = 'NOTES';
+export const WIDGET_TYPE_HEALTH_STATUS = 'HEALTH_STATUS';
export const WORK_ITEM_TYPE_ENUM_INCIDENT = 'INCIDENT';
export const WORK_ITEM_TYPE_ENUM_ISSUE = 'ISSUE';
export const WORK_ITEM_TYPE_ENUM_TASK = 'TASK';
export const WORK_ITEM_TYPE_ENUM_TEST_CASE = 'TEST_CASE';
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_ISSUE = 'Issue';
+export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
export const i18n = {
fetchErrorTitle: s__('WorkItem|Work item not found'),
@@ -61,6 +67,13 @@ export const I18N_WORK_ITEM_FETCH_ITERATIONS_ERROR = s__(
'WorkItem|Something went wrong when fetching iterations. Please try again.',
);
+export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
+export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
+export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
+ 'WorkItem|Search existing %{workItemType}s',
+);
+
export const sprintfWorkItem = (msg, workItemTypeArg) => {
const workItemType = workItemTypeArg || s__('WorkItem|Work item');
return capitalizeFirstCharacter(
@@ -100,11 +113,45 @@ export const WORK_ITEMS_TYPE_MAP = {
icon: `issue-type-requirements`,
name: s__('WorkItem|Requirements'),
},
+ [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
+ icon: `issue-type-objective`,
+ name: s__('WorkItem|Objective'),
+ },
+ [WORK_ITEM_TYPE_ENUM_KEY_RESULT]: {
+ icon: `issue-type-issue`,
+ name: s__('WorkItem|Key Result'),
+ },
+};
+
+export const WORK_ITEMS_TREE_TEXT_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: {
+ title: s__('WorkItem|Child objectives and key results'),
+ empty: s__('WorkItem|No objectives or key results are currently assigned.'),
+ },
+ [WORK_ITEM_TYPE_VALUE_ISSUE]: {
+ title: s__('WorkItem|Tasks'),
+ empty: s__(
+ 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
+ ),
+ },
+};
+
+export const WORK_ITEM_NAME_TO_ICON_MAP = {
+ Issue: 'issue-type-issue',
+ Task: 'issue-type-task',
+ Objective: 'issue-type-objective',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'Key Result': 'issue-type-key-result',
};
export const FORM_TYPES = {
create: 'create',
add: 'add',
+ [WORK_ITEM_TYPE_ENUM_OBJECTIVE]: {
+ icon: `issue-type-issue`,
+ name: s__('WorkItem|Objective'),
+ },
};
export const DEFAULT_PAGE_SIZE_ASSIGNEES = 10;
+export const DEFAULT_PAGE_SIZE_NOTES = 100;
diff --git a/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
new file mode 100644
index 00000000000..62ced6bdfea
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/discussion.fragment.graphql
@@ -0,0 +1,12 @@
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment Discussion on Note {
+ id
+ body
+ bodyHtml
+ systemNoteIconName
+ createdAt
+ author {
+ ...User
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
index 58140aff89e..5c93370aac9 100644
--- a/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/milestone.fragment.graphql
@@ -2,4 +2,7 @@ fragment MilestoneFragment on Milestone {
expired
id
title
+ state
+ startDate
+ dueDate
}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
index 7b63d9c7ca3..7fcf622cdb2 100644
--- a/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item_links.query.graphql
@@ -20,9 +20,12 @@ query workItemLinksQuery($id: WorkItemID!) {
children {
nodes {
id
+ iid
confidential
workItemType {
id
+ name
+ iconName
}
title
state
diff --git a/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
new file mode 100644
index 00000000000..baefcdaea93
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_metadata_widgets.fragment.graphql
@@ -0,0 +1,29 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/work_items/graphql/milestone.fragment.graphql"
+
+fragment WorkItemMetadataWidgets on WorkItemWidget {
+ ... on WorkItemWidgetMilestone {
+ type
+ milestone {
+ ...MilestoneFragment
+ }
+ }
+ ... on WorkItemWidgetAssignees {
+ type
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ }
+ ... on WorkItemWidgetLabels {
+ type
+ allowsScopedLabels
+ labels {
+ nodes {
+ ...Label
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
new file mode 100644
index 00000000000..9439f22f955
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes.query.graphql
@@ -0,0 +1,27 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotes($id: WorkItemID!, $after: String, $pageSize: Int) {
+ workItem(id: $id) {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
new file mode 100644
index 00000000000..3e0960f3f54
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_notes_by_iid.query.graphql
@@ -0,0 +1,32 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "~/work_items/graphql/discussion.fragment.graphql"
+
+query workItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: project(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ONLY_ACTIVITY) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...Discussion
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
new file mode 100644
index 00000000000..006ca29e01c
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_tree.query.graphql
@@ -0,0 +1,53 @@
+#import "~/graphql_shared/fragments/label.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "./work_item_metadata_widgets.fragment.graphql"
+
+query workItemTreeQuery($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ userPermissions {
+ deleteWorkItem
+ updateWorkItem
+ }
+ confidential
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ }
+ children {
+ nodes {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ... on WorkItemWidgetHierarchy {
+ type
+ hasChildren
+ }
+ ...WorkItemMetadataWidgets
+ }
+ }
+ }
+ }
+ ...WorkItemMetadataWidgets
+ }
+ }
+}
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 b9715c21c27..cf3374e1737 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
@@ -1,6 +1,7 @@
#import "~/graphql_shared/fragments/label.fragment.graphql"
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/work_items/graphql/milestone.fragment.graphql"
+#import "./work_item_metadata_widgets.fragment.graphql"
fragment WorkItemWidgets on WorkItemWidget {
... on WorkItemWidgetDescription {
@@ -38,15 +39,39 @@ fragment WorkItemWidgets on WorkItemWidget {
}
... on WorkItemWidgetHierarchy {
type
+ hasChildren
parent {
id
iid
title
confidential
+ webUrl
+ workItemType {
+ id
+ name
+ iconName
+ }
}
children {
nodes {
id
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ createdAt
+ closedAt
+ widgets {
+ ... on WorkItemWidgetHierarchy {
+ type
+ hasChildren
+ }
+ ...WorkItemMetadataWidgets
+ }
}
}
}
@@ -56,4 +81,7 @@ fragment WorkItemWidgets on WorkItemWidget {
...MilestoneFragment
}
}
+ ... on WorkItemWidgetNotes {
+ type
+ }
}
diff --git a/app/assets/javascripts/work_items/index.js b/app/assets/javascripts/work_items/index.js
index 4fbcdfe2b96..a056fde6928 100644
--- a/app/assets/javascripts/work_items/index.js
+++ b/app/assets/javascripts/work_items/index.js
@@ -6,7 +6,14 @@ import { createRouter } from './router';
export const initWorkItemsRoot = () => {
const el = document.querySelector('#js-work-items');
- const { fullPath, hasIssueWeightsFeature, issuesListPath, hasIterationsFeature } = el.dataset;
+ const {
+ fullPath,
+ hasIssueWeightsFeature,
+ issuesListPath,
+ hasIterationsFeature,
+ hasOkrsFeature,
+ hasIssuableHealthStatusFeature,
+ } = el.dataset;
return new Vue({
el,
@@ -15,9 +22,12 @@ export const initWorkItemsRoot = () => {
apolloProvider,
provide: {
fullPath,
+ projectPath: fullPath,
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasOkrsFeature: parseBoolean(hasOkrsFeature),
issuesListPath,
hasIterationsFeature: parseBoolean(hasIterationsFeature),
+ hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 1c00bd16263..d04d4942253 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -70,6 +70,10 @@ export default {
<template>
<div>
<gl-alert v-if="error" variant="danger" @dismiss="error = ''">{{ error }}</gl-alert>
- <work-item-detail :work-item-id="gid" :iid="id" @deleteWorkItem="deleteWorkItem($event)" />
+ <work-item-detail
+ :work-item-id="gid"
+ :work-item-iid="id"
+ @deleteWorkItem="deleteWorkItem($event)"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index 17f9c882c2d..e58fd19ea31 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -1,6 +1,12 @@
import workItemQuery from './graphql/work_item.query.graphql';
import workItemByIidQuery from './graphql/work_item_by_iid.query.graphql';
+import workItemNotesIdQuery from './graphql/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from './graphql/work_item_notes_by_iid.query.graphql';
export function getWorkItemQuery(isFetchedByIid) {
return isFetchedByIid ? workItemByIidQuery : workItemQuery;
}
+
+export function getWorkItemNotesQuery(isFetchedByIid) {
+ return isFetchedByIid ? workItemNotesByIidQuery : workItemNotesIdQuery;
+}
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index 6878e9a10d7..fa5d2bf7972 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -4,20 +4,16 @@
@import './pages/events';
@import './pages/groups';
@import './pages/hierarchy';
-@import './pages/issuable';
@import './pages/issues';
@import './pages/labels';
@import './pages/login';
@import './pages/ml_experiment_tracking';
@import './pages/merge_requests';
-@import './pages/monitor';
@import './pages/note_form';
@import './pages/notes';
@import './pages/pipelines';
@import './pages/profile';
@import './pages/projects';
@import './pages/registry';
-@import './pages/search';
@import './pages/settings';
@import './pages/storage_quota';
-@import './pages/users';
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 1b6a0208ca7..44b06c0ff12 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -130,6 +130,18 @@
background-color: var(--gl-color-chip-color);
}
+.content-editor-comment {
+ &::before {
+ content: '<!--';
+ }
+
+ &::after {
+ content: '-->';
+ }
+}
+
+
+
.bubble-menu-form {
width: 320px;
}
diff --git a/app/assets/stylesheets/components/ref_selector.scss b/app/assets/stylesheets/components/ref_selector.scss
index ded911c2492..f7a9367499e 100644
--- a/app/assets/stylesheets/components/ref_selector.scss
+++ b/app/assets/stylesheets/components/ref_selector.scss
@@ -6,7 +6,7 @@
width: 20rem;
&,
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $dropdown-max-height-lg;
}
}
diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss
new file mode 100644
index 00000000000..a6ecca88bd4
--- /dev/null
+++ b/app/assets/stylesheets/fonts.scss
@@ -0,0 +1,32 @@
+/* -------------------------------------------------------
+Inter variable font.
+
+Usage:
+ html { font-family: 'GitLab Sans', sans-serif; }
+*/
+@font-face {
+ font-family: 'GitLab Sans';
+ font-weight: 100 900;
+ font-display: optional;
+ font-style: normal;
+ font-named-instance: 'Regular'; /* stylelint-disable property-no-unknown */
+ src: font-url('gitlab-sans/GitLabSans.woff2') format('woff2');
+}
+
+/* -------------------------------------------------------
+Monospaced font: JetBrains Mono.
+
+Usage:
+ html { font-family: 'JetBrains Mono', sans-serif; }
+*/
+@font-face {
+ font-family: 'JetBrains Mono';
+ font-display: optional;
+ font-style: normal;
+ src: font-url('jetbrains-mono/JetBrainsMono.woff2') format('woff2');
+}
+
+:root {
+ --default-mono-font: 'JetBrains Mono', 'Menlo';
+ --default-regular-font: 'GitLab Sans', -apple-system;
+}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index be8a890320f..14e756a5c21 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -1,9 +1,9 @@
/** COLORS **/
.cgray { color: $gl-text-color; }
-.clgray { color: $common-gray-light; }
+.clgray { color: $gray-200; }
.cred { color: $red-500; }
.cgreen { color: $green-600; }
-.cdark { color: $common-gray-dark; }
+.cdark { color: $gray-800; }
.fwhite { fill: $white; }
.fgray { fill: $gray-500; }
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index c5a34ca4b31..0acda85f527 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -5,7 +5,7 @@
// for Snippets is introduced and Clone button is relocated, we won't
// need this style.
// Issue for the refactoring: https://gitlab.com/gitlab-org/gitlab/-/issues/213327
- &.gl-new-dropdown button.dropdown-toggle {
+ &.gl-dropdown button.dropdown-toggle {
@include gl-display-inline-flex;
}
@@ -41,7 +41,7 @@
max-height: $extended-max-height;
// See comment below for explanation
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $extended-max-height - 2px;
}
}
@@ -54,12 +54,12 @@
width: 100%;
}
- // `GlDropdown` specifies the `max-height` of `.gl-new-dropdown-inner`
+ // `GlDropdown` specifies the `max-height` of `.gl-dropdown-inner`
// as `$dropdown-max-height`, but the `max-height` rule above forces
// the parent `.dropdown-menu` to be _slightly_ too small because of
// the 1px borders. The workaround below overrides the @gitlab/ui style
// to avoid a double scroll bar.
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: $dropdown-max-height - 2px;
}
}
@@ -285,7 +285,7 @@
list-style: none;
> a,
- button,
+ > button,
.gl-button.btn-link,
.menu-item {
@include dropdown-link;
@@ -1027,7 +1027,7 @@
// This class won't be needed once we can add a prop for this in the GitLab UI component
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966
-.gl-new-dropdown {
+.gl-dropdown {
.gl-dropdown-menu-wide {
width: $gl-dropdown-width-wide;
}
@@ -1035,7 +1035,7 @@
// This class won't be needed once we can add a prop for this in the GitLab UI component
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/966
-.gl-new-dropdown.gl-dropdown-menu-full-width {
+.gl-dropdown.gl-dropdown-menu-full-width {
.dropdown-menu {
width: 100%;
}
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index 68a3493670d..16ad6f62c64 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -36,7 +36,7 @@ gl-emoji {
}
}
-.emoji-picker .gl-new-dropdown .dropdown-menu {
+.emoji-picker .gl-dropdown .dropdown-menu {
width: 350px;
}
@@ -48,6 +48,6 @@ gl-emoji {
border-bottom-color: var(--gl-theme-accent, $theme-indigo-500);
}
-.emoji-picker .gl-new-dropdown-inner > :last-child {
+.emoji-picker .gl-dropdown-inner > :last-child {
padding-bottom: 0;
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 37b61d36911..b35175f4ef6 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -252,7 +252,7 @@
z-index: 1;
&:hover .clear-search-icon {
- color: $common-gray-dark;
+ color: $gray-800;
}
}
}
@@ -433,8 +433,7 @@
.search-token-target-branch {
.value {
- font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-monospace;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index bba995a6de3..e86edff3f13 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -14,6 +14,28 @@ input[type='text'].danger {
text-shadow: 0 1px 1px $white;
}
+/**
+ * When form input type is number, Firefox & Safari show the up/down arrows
+ * on the right side of the input persistently, while Chrome shows it only
+ * on hover or focus, this fix allows us to hide the arrows in all browsers.
+ * You can conditionally add/remove `hide-spinners` class to have consistent
+ * behaviour across browsers.
+ */
+
+/* stylelint-disable property-no-vendor-prefix */
+input[type='number'].hide-spinners {
+ -moz-appearance: textfield;
+ appearance: textfield;
+
+ &::-webkit-inner-spin-button,
+ &::-webkit-outer-spin-button {
+ -webkit-appearance: none;
+ appearance: none;
+ margin: 0;
+ }
+}
+/* stylelint-enable property-no-vendor-prefix */
+
.datetime-controls {
select {
width: 100px;
@@ -204,6 +226,22 @@ label {
}
}
+.show-password-complexity-errors {
+ .form-control:not(textarea) {
+ height: 34px;
+ }
+
+ .password-complexity-error-outline {
+ border: 1px solid $red-500;
+
+ &:focus {
+ box-shadow: 0 0 0 1px $red-500 inset, 0 1px 1px $gl-field-focus-shadow inset,
+ 0 0 4px 0 $gl-field-focus-shadow-error;
+ border: 0 none;
+ }
+ }
+}
+
.input-icon-wrapper,
.select-wrapper {
position: relative;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index ed41d10f3b2..4b1efcc1e9a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,3 +1,7 @@
+$search-input-field-min-width: 320px;
+$search-input-field-max-width: 640px;
+$search-input-field-x-min-width: 200px;
+
.navbar-gitlab {
padding: 0 16px;
z-index: $header-zindex;
@@ -76,6 +80,57 @@
.navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+
+ .header-search-new {
+ max-width: $search-input-field-max-width;
+ }
+
+ &.header-search-is-active {
+ .global-search-container {
+ flex-grow: 1;
+ }
+ }
+ }
+
+ .header-search {
+ min-width: $search-input-field-min-width;
+
+ // This is a temporary workaround!
+ // the button in GitLab UI Search components need to be updated to not be the small size
+ // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
+ .gl-search-box-by-type-clear.btn-sm {
+ padding: 0.5rem !important;
+ }
+
+ @include media-breakpoint-between(md, lg) {
+ min-width: $search-input-field-x-min-width;
+ }
+
+ &.is-searching {
+ .in-search-scope-help {
+ position: absolute;
+ top: $gl-spacing-scale-2;
+ right: 2.125rem;
+ z-index: 2;
+ }
+ }
+
+ &.is-not-focused {
+ .gl-search-box-by-type-clear {
+ display: none;
+ }
+ }
+
+ .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+ }
+ }
+
+ .header-search-dropdown-menu {
+ max-height: $dropdown-max-height;
+ top: 100%;
}
.navbar-collapse {
@@ -555,7 +610,7 @@
}
.top-nav-container-view {
- .gl-new-dropdown & .gl-search-box-by-type {
+ .gl-dropdown & .gl-search-box-by-type {
@include gl-m-0;
}
diff --git a/app/assets/stylesheets/framework/kbd.scss b/app/assets/stylesheets/framework/kbd.scss
index 7dd0ae47834..16e0214c703 100644
--- a/app/assets/stylesheets/framework/kbd.scss
+++ b/app/assets/stylesheets/framework/kbd.scss
@@ -1,10 +1,10 @@
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: $gl-font-size-monospace-sm;
+ @include gl-font-sm;
line-height: 10px;
color: var(--gray-700, $gray-700);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, $gray-10);
border-width: 1px;
border-style: solid;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 47856f1a0d3..628406d5889 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -450,7 +450,7 @@
}
@mixin avatar-counter($border-radius: 1em) {
- background-color: $gray-darkest;
+ background-color: $gray-400;
color: $white;
border: 1px solid $gray-normal;
border-radius: $border-radius;
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 7878e08e549..eb34d91476b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -198,17 +198,17 @@
}
}
+ $line-height: map-get($spacers, 4) + px-to-rem(2px);
+
&-icon {
/**
* 2px extra is to give a little more height than needed
* to hide timeline line before/after the element starts/ends
*/
- height: map-get($spacers, 4) + px-to-rem(2px);
+ height: $line-height;
z-index: 1;
position: relative;
- top: -3px;
padding: $gl-padding-4 0;
- background-color: $body-bg;
&.opened {
color: $green-500;
@@ -220,7 +220,7 @@
}
&-content {
- line-height: initial;
+ line-height: $line-height;
margin-left: $gl-padding-8;
}
}
@@ -280,3 +280,639 @@
grid-area: user;
}
}
+
+@mixin right-sidebar {
+ position: fixed;
+ top: $header-height;
+ // Default value for CSS var must contain a unit
+ // stylelint-disable-next-line length-zero-no-unit
+ bottom: var(--review-bar-height, 0px);
+ right: 0;
+ transition: width $gl-transition-duration-medium;
+ background-color: $white;
+ z-index: 200;
+ overflow: hidden;
+
+}
+
+.right-sidebar {
+ &:not(.right-sidebar-merge-requests) {
+ @include right-sidebar;
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-down(md) {
+ @include right-sidebar;
+ z-index: 251;
+ }
+ }
+
+ @include media-breakpoint-down(sm) {
+ z-index: 251;
+ }
+
+ a:not(.btn) {
+ color: inherit;
+
+ &:hover {
+ color: $blue-800;
+ }
+ }
+
+ .gl-label .gl-label-link:hover {
+ color: inherit;
+ }
+
+ .btn-link {
+ color: inherit;
+ }
+
+ .issuable-header-text {
+ margin-top: 7px;
+ }
+
+ .gutter-toggle {
+ display: flex;
+ align-items: center;
+ margin-left: 20px;
+ padding: 4px;
+ border-radius: 4px;
+ height: 24px;
+
+ &:hover {
+ color: $gl-text-color;
+ background: $gray-50;
+ }
+
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ }
+ }
+
+ &.right-sidebar-merge-requests {
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ width: 100%;
+ }
+
+ .block {
+ @include media-breakpoint-up(lg) {
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ }
+
+ &.participants {
+ border-bottom: 0;
+ }
+ }
+ }
+
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ @include clearfix;
+ padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
+ border-bottom: 1px solid $border-gray-normal;
+ // This prevents the mess when resizing the sidebar
+ // of elements repositioning themselves..
+ width: $gutter-inner-width;
+ // --
+
+ &:last-child {
+ border: 0;
+ }
+
+ &.assignee {
+ .author-link {
+ display: block;
+ position: relative;
+
+ &:hover {
+ .author {
+ text-decoration: underline;
+ }
+ }
+ }
+ }
+
+ &.time-tracking,
+ &.participants,
+ &.subscriptions,
+ &.with-sub-blocks {
+ padding-top: $gl-spacing-scale-5;
+ }
+ }
+
+ .block-first {
+ padding-top: 0;
+ }
+
+ .title {
+ color: $gl-text-color;
+ line-height: $gl-line-height-20;
+
+ .avatar {
+ margin-left: 0;
+ }
+ }
+
+ .selectbox {
+ display: none;
+
+ &.show {
+ display: block;
+ }
+ }
+
+ .btn-clipboard:hover {
+ color: $gl-text-color;
+ }
+
+ .issuable-sidebar {
+ height: 100%;
+
+ &:not(.is-merge-request) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+
+ &.is-merge-request {
+ @include media-breakpoint-down(sm) {
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+ }
+ }
+ }
+
+ &.right-sidebar-expanded {
+ &:not(.right-sidebar-merge-requests) {
+ width: $gutter-width;
+ }
+
+ .value {
+ line-height: 1;
+ }
+
+ .issuable-sidebar {
+ padding: 0 20px;
+
+ &.is-merge-request {
+ @include media-breakpoint-up(lg) {
+ padding: 0;
+
+ .issuable-context-form {
+ --initial-top: calc(#{$header-height} + 76px);
+ --top: var(--initial-top);
+
+ @include gl-sticky;
+ @include gl-overflow-auto;
+
+ top: var(--top);
+ height: calc(100vh - var(--top));
+ padding: 0 15px;
+ margin-bottom: calc(var(--top) * -1);
+
+ .with-performance-bar & {
+ --top: calc(var(--initial-top) + #{$performance-bar-height});
+ }
+
+ .with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height});
+ }
+
+ .with-performance-bar.with-system-header & {
+ --top: calc(var(--initial-top) + #{$system-header-height} + #{$performance-bar-height});
+ }
+ }
+ }
+ }
+ }
+
+ &:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
+ .issuable-sidebar-header {
+ display: none;
+ }
+ }
+
+ .light {
+ font-weight: $gl-font-weight-normal;
+ }
+
+ .no-value {
+ color: $gl-text-color-secondary;
+ }
+
+ .sidebar-collapsed-icon {
+ display: none;
+ }
+
+ .gutter-toggle {
+ text-align: center;
+ }
+
+ .title .gutter-toggle {
+ margin-top: 0;
+ }
+
+ .assignee .user-list .avatar {
+ margin: 0;
+ }
+
+ .hide-expanded {
+ display: none;
+ }
+ }
+
+ &.right-sidebar-collapsed {
+ /* Extra small devices (phones, less than 768px) */
+ display: none;
+ /* Small devices (tablets, 768px and up) */
+
+ &:not(.right-sidebar-merge-requests) {
+ @include media-breakpoint-up(sm) {
+ display: block;
+ }
+ }
+
+ &.right-sidebar-merge-requests {
+ @include media-breakpoint-up(lg) {
+ display: block;
+ }
+ }
+
+ width: $gutter-collapsed-width;
+ padding: 0;
+
+ .block,
+ .sidebar-contained-width,
+ .issuable-sidebar-header {
+ width: $gutter-collapsed-width - 2px;
+ padding: 0;
+ border-bottom: 0;
+ overflow: hidden;
+ }
+
+ .block,
+ .gutter-toggle,
+ .sidebar-collapsed-container {
+ &.with-sub-blocks .sub-block:hover,
+ &:not(.with-sub-blocks):hover {
+ background-color: $gray-100;
+ }
+ }
+
+ .participants {
+ border-bottom: 1px solid $border-gray-normal;
+ }
+
+ .hide-collapsed {
+ display: none;
+ }
+
+ .gutter-toggle {
+ width: 100%;
+ height: $sidebar-toggle-height;
+ margin-left: 0;
+ border-bottom: 1px solid $border-white-normal;
+ border-radius: 0;
+ }
+
+ a.gutter-toggle {
+ display: flex;
+ justify-content: center;
+ flex-direction: column;
+ text-align: center;
+ }
+
+ .merge-icon {
+ height: 12px;
+ width: 12px;
+ bottom: -5px;
+ right: 4px;
+ }
+
+ .sidebar-collapsed-icon {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ align-items: center;
+ width: 100%;
+ height: $sidebar-toggle-height;
+ text-align: center;
+ color: $gl-text-color-secondary;
+
+ > svg {
+ fill: $gl-text-color-secondary;
+ }
+
+ &:hover:not(.disabled),
+ &:hover .todo-undone {
+ color: $gl-text-color;
+
+ > svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .todo-undone {
+ color: $blue-600;
+ fill: $blue-600;
+ }
+
+ .author {
+ display: none;
+ }
+
+ .btn-clipboard {
+ /*
+ This change should be temporary, because the DOM currently gets
+ generated from a ruby definition in `app/helpers/button_helper.rb`.
+ As soon as the `copy to clipboard` button will be transferred to
+ Vue this should be adjusted as well.
+ */
+ flex: 1;
+ align-self: stretch;
+ padding: 0;
+
+ border: 0;
+ background: transparent;
+ color: $gl-text-color-secondary;
+
+ &:hover {
+ color: $gl-text-color;
+ }
+ }
+
+ &.multiple-users {
+ display: flex;
+ justify-content: center;
+ }
+ }
+
+ .sidebar-avatar-counter {
+ width: 24px;
+ height: 24px;
+ border-radius: 12px;
+
+ ~.merge-icon {
+ bottom: 0;
+ }
+ }
+
+ .sidebar-collapsed-user {
+ padding-bottom: 0;
+
+ .author-link {
+ padding-left: 0;
+
+ .avatar {
+ position: static;
+ margin: 0;
+ }
+ }
+ }
+
+ .issuable-header-btn {
+ display: none;
+ }
+
+ .multiple-users {
+ .btn-link {
+ padding: 0;
+ border: 0;
+
+ .avatar {
+ margin: 0;
+ }
+ }
+
+ .btn-link:first-child {
+ position: absolute;
+ left: 10px;
+ z-index: 1;
+ }
+
+ .btn-link:last-child {
+ position: absolute;
+ right: 10px;
+
+ &:hover {
+ text-decoration: none;
+ }
+ }
+ }
+
+ .milestone-title span,
+ .collapse-truncated-title {
+ @include str-truncated(100%);
+ display: block;
+ margin: 0 4px;
+ }
+ }
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ padding-top: 6px;
+ }
+
+ .dropdown-menu {
+ width: 100%;
+
+ /*
+ * Overwrite hover style for dropdown items, so that they are not blue
+ * This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-foss/issues/44040
+ */
+ li a {
+ &:hover,
+ &:active,
+ &:focus,
+ &.is-focused {
+ @include dropdown-item-hover;
+ }
+ }
+
+ }
+}
+
+.with-performance-bar .right-sidebar {
+ top: calc(#{$header-height} + #{$performance-bar-height});
+}
+
+.sidebar-move-issue-confirmation-button {
+ width: 100%;
+
+ &.is-loading {
+ .sidebar-move-issue-confirmation-loading-icon {
+ display: inline-block;
+ }
+ }
+}
+
+.sidebar-move-issue-confirmation-loading-icon {
+ display: none;
+}
+
+.issuable-show-labels {
+ .gl-label {
+ margin-bottom: 5px;
+ margin-right: 5px;
+ }
+
+ a {
+ display: inline-block;
+
+ .color-label {
+ padding: 4px $grid-size;
+ border-radius: $label-border-radius;
+ margin-right: 4px;
+ margin-bottom: 4px;
+ }
+
+ &:hover .color-label {
+ text-decoration: underline;
+ }
+ }
+
+ &.has-labels {
+ // this font size is a fix to
+ // prevent unintended spacing between labels
+ // which shows up when rendering markup has white-space
+ // characters present.
+ // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
+ font-size: 0;
+ margin-bottom: -5px;
+ }
+}
+
+.assignee,
+.reviewer {
+ .merge-icon {
+ color: $orange-400;
+ position: absolute;
+ bottom: -3px;
+ right: -3px;
+ filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
+ }
+}
+
+.participants-author {
+ &:nth-of-type(8n) {
+ padding-right: 0;
+ }
+
+ .avatar.avatar-inline {
+ margin: 0;
+ }
+}
+
+.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;
+ padding: 16px 20px;
+ }
+
+ .help-state-toggle-enter-active {
+ transition: all 0.8s ease;
+ }
+
+ .help-state-toggle-leave-active {
+ transition: all 0.5s ease;
+ }
+
+ .help-state-toggle-enter,
+ .help-state-toggle-leave-active {
+ opacity: 0;
+ }
+}
+
+.time-tracker {
+ .sidebar-collapsed-icon {
+ > .stopwatch-svg {
+ display: inline-block;
+ }
+
+ svg {
+ width: 16px;
+ height: 16px;
+ fill: $gl-text-color-secondary;
+ }
+
+ &:hover svg {
+ fill: $gl-text-color;
+ }
+ }
+
+ .compare-meter {
+ &.over_estimate {
+ .time-remaining,
+ .compare-value.spent {
+ color: $red-500;
+ }
+ }
+ }
+
+ .compare-display-container {
+ font-size: 13px;
+ }
+}
+
+/*
+ * Following overrides are done to prevent
+ * legacy dropdown styles from influencing
+ * GitLab UI components used within GlDropdown
+ */
+.right-sidebar-collapsed {
+ .sidebar-grouped-item {
+ .sidebar-collapsed-icon {
+ margin-bottom: 0;
+ }
+
+ .sidebar-collapsed-divider {
+ line-height: 5px;
+ font-size: 12px;
+ color: $gray-500;
+
+ + .sidebar-collapsed-icon {
+ padding-top: 0;
+ }
+ }
+ }
+}
+
+@include media-breakpoint-down(sm) {
+ // Overriding the following rule with the negative margin
+ // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
+ .container-fluid {
+ .issuable-list,
+ .issues-filters,
+ .epics-filters {
+ margin: 0 (-$gl-padding);
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 2c2d8a2b592..0a475845fd3 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -81,6 +81,7 @@
word-wrap: break-word;
overflow-wrap: break-word;
word-break: keep-all;
+ @include gl-font-base;
}
h1 {
@@ -322,7 +323,6 @@
pre {
margin-bottom: 16px;
- font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
border-radius: $border-radius-default;
@@ -587,7 +587,7 @@
}
}
- .gl-new-dropdown-item {
+ .gl-dropdown-item {
margin: 0;
padding: 0;
line-height: 1rem;
@@ -658,7 +658,7 @@ pre {
display: block;
padding: $gl-padding-8 $input-horizontal-padding;
margin: 0 0 $gl-padding-8;
- font-size: $gl-font-size-monospace;
+ @include gl-font-base;
word-break: break-all;
word-wrap: break-word;
color: $gl-text-color;
@@ -680,7 +680,7 @@ code {
}
.monospace {
- font-family: $monospace-font;
+ @include gl-font-monospace;
}
.weight-normal {
@@ -706,7 +706,7 @@ code {
*/
textarea.js-gfm-input {
font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-base;
}
h1,
@@ -772,3 +772,8 @@ textarea {
wbr {
display: inline-block;
}
+
+// The font used in Monaco editor - Web IDE, Snippets, single file editor
+:root {
+ --code-editor-font: #{$monospace-font};
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 99284ea0a64..ec8ffaf8c53 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -83,18 +83,9 @@ $darken-dark-factor: 10% !default;
$darken-border-factor: 5% !default;
$darken-border-dashed-factor: 25% !default;
-$white: #fff !default;
-$white-normal: #f0f0f0 !default;
-$white-dark: #eaeaea !default;
-$white-transparent: rgba($white, 0.8) !default;
-
$purple: #6d49cb !default;
$purple-light: #ede8fb !default;
-$black: #000 !default;
-$black-transparent: rgba(0, 0, 0, 0.3) !default;
-$almost-black: #242424 !default;
-
$green-50: #ecf4ee !default;
$green-100: #c3e6cd !default;
$green-200: #91d4a8 !default;
@@ -183,6 +174,15 @@ $t-gray-a-08: rgba($gray-950, 0.08) !default;
$t-gray-a-16: rgba($gray-950, 0.16) !default;
$t-gray-a-24: rgba($gray-950, 0.24) !default;
+$white: #fff !default;
+$white-normal: $gray-50 !default;
+$white-dark: darken($gray-50, 2) !default;
+$white-transparent: rgba($white, 0.8) !default;
+
+$black: #000 !default;
+$black-transparent: $t-gray-a-24 !default;
+$almost-black: $gray-950 !default;
+
$greens: (
'50': $green-50,
'100': $green-100,
@@ -350,17 +350,17 @@ $theme-light-red-700: #a62e21;
// Data visualization color palette
-$data-viz-blue-50: #e9ebff;
-$data-viz-blue-100: #d4dcfa;
-$data-viz-blue-200: #b7c6ff;
-$data-viz-blue-300: #97acff;
-$data-viz-blue-400: #748eff;
-$data-viz-blue-500: #5772ff;
-$data-viz-blue-600: #445cf2;
-$data-viz-blue-700: #3547de;
-$data-viz-blue-800: #232fcf;
-$data-viz-blue-900: #1e23a8;
-$data-viz-blue-950: #11118a;
+$data-viz-blue-50: #e9ebff !default;
+$data-viz-blue-100: #d2dcff !default;
+$data-viz-blue-200: #b7c6ff !default;
+$data-viz-blue-300: #97acff !default;
+$data-viz-blue-400: #7992f5 !default;
+$data-viz-blue-500: #617ae2 !default;
+$data-viz-blue-600: #4e65cd !default;
+$data-viz-blue-700: #3f51ae !default;
+$data-viz-blue-800: #374291 !default;
+$data-viz-blue-900: #303470 !default;
+$data-viz-blue-950: #2a2b59 !default;
$border-white-light: darken($white, $darken-border-factor) !default;
$border-white-normal: darken($white-normal, $darken-border-factor) !default;
@@ -380,7 +380,7 @@ $well-expand-item: #e8f2f7 !default;
$well-inner-border: #eef0f2 !default;
$well-light-border: #f1f1f1;
$well-light-text-color: #5b6169;
-$nav-active-bg: rgba($black, 0.08);
+$nav-active-bg: $t-gray-a-08;
/*
* Text
@@ -555,11 +555,13 @@ $diff-jagged-border-gradient-color: darken($white-normal, 8%);
/*
* Fonts
*/
-$monospace-font: 'Menlo', 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas', 'Ubuntu Mono',
- 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular-font: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans', Ubuntu, Cantarell,
- 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol',
- 'Noto Color Emoji';
+$monospace-font: var(--default-mono-font, 'Menlo'), 'DejaVu Sans Mono', 'Liberation Mono', 'Consolas',
+ 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
+$regular-font: var(--default-regular-font, -apple-system), BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans',
+ Ubuntu, Cantarell, 'Helvetica Neue', sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji',
+ 'Segoe UI Symbol', 'Noto Color Emoji';
+$gl-monospace-font: $monospace-font;
+$gl-regular-font: $regular-font;
/*
* Dropdowns
@@ -730,12 +732,6 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0);
$commit-stat-summary-height: 36px;
/*
-* Common
-*/
-$common-gray-light: #bbb;
-$common-gray-dark: #444;
-
-/*
* Files
*/
$logs-li-color: #888;
@@ -784,16 +780,6 @@ $fade-mask-transition-curve: ease-in-out;
$login-brand-holder-color: #888;
/*
-* Projects
-*/
-$project-option-descr-color: #54565b;
-
-/*
- * Monitor Charts
- */
-$chart-tooltip-max-width: 512px;
-
-/*
Stat Graph
*/
$stat-graph-common-bg: #f3f3f3;
@@ -822,7 +808,6 @@ Pipeline Graph
$ci-action-icon-size: 22px;
$ci-action-icon-size-lg: 24px;
$pipeline-dropdown-line-height: 20px;
-$pipeline-dropdown-status-icon-size: 18px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 12px;
diff --git a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
index 613d27a2f39..ed15e352b7d 100644
--- a/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_pipeline_mixins.scss
@@ -33,8 +33,8 @@
.ci-action-icon-container {
position: absolute;
- right: 8px;
- top: 8px;
+ right: 11px;
+ top: 7px;
&.ci-action-icon-wrapper {
height: $ci-action-dropdown-button-size;
@@ -84,25 +84,6 @@
&.non-details-job-component {
padding: $gl-padding-8 $gl-btn-horz-padding;
}
-
- .ci-job-name-component {
- align-items: center;
- display: flex;
- flex: 1;
- }
-
- .ci-status-icon {
- position: relative;
-
- > svg {
- width: $pipeline-dropdown-status-icon-size;
- height: $pipeline-dropdown-status-icon-size;
- margin: 3px 0;
- position: relative;
- overflow: visible;
- display: block;
- }
- }
}
// ensure .mini-pipeline-graph-dropdown-item has hover style when action-icon is hovered
diff --git a/app/assets/stylesheets/page_bundles/alert_management_details.scss b/app/assets/stylesheets/page_bundles/alert_management_details.scss
index 2eaf4517710..d67dadafa9e 100644
--- a/app/assets/stylesheets/page_bundles/alert_management_details.scss
+++ b/app/assets/stylesheets/page_bundles/alert_management_details.scss
@@ -28,7 +28,7 @@
@include gl-pt-8;
}
- .gl-new-dropdown-item-text-wrapper {
+ .gl-dropdown-item-text-wrapper {
@include gl-py-0;
}
}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 0cc1fb40e4a..bdbcf7ab58c 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -185,13 +185,15 @@
}
.issue-boards-content.is-focused {
+ $focus-mode-z-index: 9000;
+
position: fixed;
width: 100%;
height: 100%;
top: 0;
left: 0;
background: var(--white, $white);
- z-index: 9000;
+ z-index: $focus-mode-z-index;
@include media-breakpoint-down(sm) {
padding-top: 10px;
@@ -201,13 +203,24 @@
height: calc(100vh - #{$issue-boards-filter-height});
}
- .boards-sidebar {
- height: 100%;
- top: 0;
+ // Use !important for these as top and z-index are set on style attribute
+ // in gitlab-ui; https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1805
+ ~ #js-right-sidebar-portal .boards-sidebar {
+ top: 0 !important;
+ z-index: calc(#{$focus-mode-z-index} + 1) !important;
}
}
.boards-sidebar {
+ top: $header-height !important;
+ height: auto;
+ bottom: 0;
+ padding-bottom: 0.5rem;
+
+ .with-performance-bar & {
+ top: calc(#{$header-height} + #{$performance-bar-height}) !important;
+ }
+
.sidebar-collapsed-icon {
@include gl-display-none;
}
diff --git a/app/assets/stylesheets/page_bundles/clusters.scss b/app/assets/stylesheets/page_bundles/clusters.scss
index 4f29ff4b1ad..4d75159e87a 100644
--- a/app/assets/stylesheets/page_bundles/clusters.scss
+++ b/app/assets/stylesheets/page_bundles/clusters.scss
@@ -6,7 +6,7 @@
@include gl-w-full;
order: -1;
- .gl-new-dropdown,
+ .gl-dropdown,
.split-content-button {
@include gl-w-full;
}
@@ -24,3 +24,9 @@
.cluster-button-container:focus-within {
@include gl-focus;
}
+
+.select-agent-dropdown {
+ .gl-button-text {
+ @include gl-flex-grow-1;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index ec75c53d026..c3688f4a138 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -27,13 +27,6 @@ $ide-commit-header-height: 48px;
@include str-truncated(250px);
}
-.ide-layout {
- // Fix for iOS 13+, the height of the page is actually less than
- // 100vh because of the presence of the bottom bar
- max-height: 100%;
- position: fixed;
-}
-
.ide-view {
position: relative;
margin-top: 0;
@@ -522,13 +515,6 @@ $ide-commit-header-height: 48px;
}
}
-.ide-loading {
- display: flex;
- height: 100%;
- align-items: center;
- justify-content: center;
-}
-
.ide-empty-state {
display: flex;
height: 100vh;
diff --git a/app/assets/stylesheets/page_bundles/incidents.scss b/app/assets/stylesheets/page_bundles/incidents.scss
index de246fa14b9..e807c4c0bbb 100644
--- a/app/assets/stylesheets/page_bundles/incidents.scss
+++ b/app/assets/stylesheets/page_bundles/incidents.scss
@@ -4,13 +4,10 @@
.main-notes-list::before {
content: none;
}
+}
- .timeline-event-note {
- p {
- margin-bottom: 0;
- font-size: 0.875rem;
- }
- }
+.timeline-event {
+ grid-template-columns: #{$gl-spacing-scale-9} minmax(0, 1fr) #{$gl-spacing-scale-7};
}
/**
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
new file mode 100644
index 00000000000..f364170c99f
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -0,0 +1,183 @@
+@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-icon {
+ background-color: var(--orange-50, $orange-50);
+ border-radius: $border-radius-default;
+ color: var(--orange-600, $orange-600);
+ width: $issuable-warning-size;
+ height: $issuable-warning-size;
+ text-align: center;
+ margin-right: $issuable-warning-icon-margin;
+ line-height: $gl-line-height-24;
+ flex: 0 0 auto;
+}
+
+.limit-container-width {
+ .flash-container,
+ .detail-page-header,
+ .page-content-header,
+ .commit-box,
+ .info-well,
+ .commit-ci-menu,
+ .files-changed-inner,
+ .limited-header-width,
+ .limited-width-notes {
+ @include fixed-width-container;
+ }
+
+ .issuable-details {
+ .detail-page-description,
+ .mr-source-target,
+ .mr-state-widget,
+ .merge-manually {
+ @include fixed-width-container;
+ }
+ }
+
+ .merge-request-details {
+ .emoji-list-container {
+ @include fixed-width-container;
+ }
+ }
+}
+
+.issuable-details {
+ section {
+ .issuable-discussion {
+ margin-right: 1px;
+ }
+ }
+
+ .title-container {
+ display: flex;
+ align-items: flex-start;
+ }
+
+ .title {
+ padding: 0;
+ margin-bottom: $gl-padding;
+ border-bottom: 0;
+ word-wrap: break-word;
+ overflow-wrap: break-word;
+ min-width: 0;
+ width: 100%;
+ text-align: initial;
+ }
+
+ .btn-edit {
+ margin-left: auto;
+ }
+}
+
+.detail-page-description {
+ padding: 16px 0;
+
+ small {
+ color: var(--gray-500, $gray-500);
+ }
+}
+
+.edited-text {
+ color: var(--gray-500, $gray-500);
+ display: block;
+ margin: 16px 0 0;
+ font-size: 85%;
+
+ .author-link {
+ color: var(--gray-500, $gray-500);
+ }
+}
+
+.user-item {
+ padding: 5px;
+ flex-basis: 20%;
+
+ .user-link {
+ display: inline-block;
+ }
+}
+
+.issuable-gutter-toggle {
+ @include media-breakpoint-down(sm) {
+ margin-left: $btn-side-margin;
+ }
+}
+
+.issuable-meta {
+ flex: 1;
+ display: inline-block;
+ font-size: 14px;
+ align-self: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ .user-status-emoji {
+ margin-left: $gl-padding-4;
+ margin-right: 0;
+ }
+}
+
+.js-issuable-selector-wrap {
+ .js-issuable-selector {
+ width: 100%;
+ }
+
+ @include media-breakpoint-down(sm) {
+ margin-bottom: $gl-padding;
+ }
+}
+
+.add-issuable-form-input-wrapper {
+ &.focus {
+ border-color: var(--gray-700, $gray-700);
+ @include gl-focus;
+
+ input {
+ @include gl-shadow-none;
+ }
+ }
+
+ .gl-show-field-errors &.form-control:not(textarea) {
+ height: auto;
+ }
+}
+
+/*
+ * Following overrides are done to prevent
+ * legacy dropdown styles from influencing
+ * GitLab UI components used within GlDropdown
+ */
+.issuable-move-dropdown {
+ .b-dropdown-form {
+ @include gl-p-0;
+ }
+
+ .gl-search-box-by-type button.gl-clear-icon-button:hover {
+ @include gl-bg-transparent;
+ }
+
+ .issuable-move-button:not(.disabled):hover {
+ @include gl-text-white;
+ }
+}
+
+.suggestion-footer {
+ font-size: 12px;
+ line-height: 15px;
+
+ .avatar {
+ margin-top: -3px;
+ border: 0;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/issuable_list.scss b/app/assets/stylesheets/page_bundles/issuable_list.scss
new file mode 100644
index 00000000000..b08e129a805
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/issuable_list.scss
@@ -0,0 +1,96 @@
+@import 'mixins_and_variables_and_functions';
+
+.issuable-list {
+ li {
+ .issuable-info-container {
+ flex: 1;
+ display: flex;
+ }
+
+ .issuable-main-info {
+ flex: 1 auto;
+ margin-right: 10px;
+ min-width: 0;
+
+ .issue-weight-icon,
+ .issue-estimate-icon {
+ vertical-align: sub;
+ }
+ }
+
+ .issuable-meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ flex: 1 0 auto;
+
+ .controls {
+ margin-bottom: 2px;
+ line-height: 20px;
+ padding: 0;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
+ .issuable-meta {
+ .controls li {
+ margin-right: 0;
+ }
+ }
+ }
+
+ .issue-check {
+ min-width: 15px;
+ }
+
+ .issuable-milestone,
+ .issuable-info,
+ .task-status,
+ .issuable-timestamp {
+ font-weight: $gl-font-weight-normal;
+ color: var(--gray-500, $gl-text-color-secondary);
+
+ a {
+ color: var(--gl-text-color, $gl-text-color);
+ }
+
+ .gl-label-link {
+ color: inherit;
+
+ &:hover {
+ text-decoration: none;
+
+ .gl-label-text:last-of-type {
+ text-decoration: underline;
+ }
+ }
+ }
+
+ .milestone {
+ color: var(--gray-700, $gray-700);
+ }
+ }
+
+ @media(max-width: map-get($grid-breakpoints, lg)-1) {
+ .task-status,
+ .issuable-due-date,
+ .issuable-weight,
+ .project-ref-path {
+ display: none;
+ }
+ }
+ }
+}
+
+.issuable-list li,
+.issuable-info-container .controls {
+ .avatar-counter {
+ display: inline-block;
+ vertical-align: middle;
+ min-width: 16px;
+ line-height: 14px;
+ height: 16px;
+ padding-left: 2px;
+ padding-right: 2px;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 771428b49e0..4950561bcb7 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -329,6 +329,8 @@ $tabs-holder-z-index: 250;
top: 0;
// !important is required to override inline styles of resizable sidebar
width: 100% !important;
+ // avoid sticky elements overlap header and other elements
+ z-index: 1;
}
.tree-list-holder {
@@ -339,11 +341,13 @@ $tabs-holder-z-index: 250;
}
.ci-widget-container {
+ align-items: center;
justify-content: space-between;
flex: 1;
flex-direction: row;
@include media-breakpoint-down(sm) {
+ align-items: initial;
flex-direction: column;
.dropdown .mini-pipeline-graph-dropdown-menu.dropdown-menu {
@@ -632,13 +636,6 @@ $tabs-holder-z-index: 250;
margin: 3px 0;
}
- .ci-status-icon svg {
- margin: 3px 0;
- position: relative;
- overflow: visible;
- display: block;
- }
-
.normal {
flex: 1;
flex-basis: auto;
@@ -673,10 +670,6 @@ $tabs-holder-z-index: 250;
}
.mr-widget-body {
- &:not(.mr-widget-body-line-height-1) {
- line-height: 24px;
- }
-
@include clearfix;
.approve-btn {
@@ -1003,7 +996,7 @@ $tabs-holder-z-index: 250;
max-width: 650px;
max-height: calc(100vh - 50px);
- .gl-new-dropdown-inner {
+ .gl-dropdown-inner {
max-height: none !important;
}
@@ -1038,7 +1031,7 @@ $tabs-holder-z-index: 250;
}
}
- .gl-new-dropdown-contents {
+ .gl-dropdown-contents {
padding: $gl-spacing-scale-4 !important;
}
@@ -1048,20 +1041,28 @@ $tabs-holder-z-index: 250;
}
.mr-widget-merge-details {
+ *,
+ & {
+ @include gl-font-sm;
+ }
+
+ p {
+ @include gl-font-base;
+ }
+
li:not(:last-child) {
- @include gl-mb-3;
+ @include gl-mb-2;
}
}
-.mr-ready-merge-related-links,
-.mr-widget-merge-details {
- a {
- @include gl-text-decoration-underline;
+.mr-ready-merge-related-links a,
+.mr-widget-merge-details a,
+.mr-widget-author {
+ @include gl-text-decoration-underline;
- &:hover,
- &:focus {
- @include gl-text-decoration-none;
- }
+ &:hover,
+ &:focus {
+ @include gl-text-decoration-none;
}
}
@@ -1075,36 +1076,20 @@ $tabs-holder-z-index: 250;
}
}
-.detail-page-header-actions {
- .gl-toggle {
- @include gl-ml-auto;
- @include gl-rounded-pill;
- @include gl-w-9;
-
- &.is-checked:hover {
- background-color: $blue-500;
- }
- }
-}
-
.page-with-icon-sidebar .issue-sticky-header {
--width: calc(100% - #{$contextual-sidebar-collapsed-width});
}
.merge-request-notification-toggle {
+ .gl-toggle {
+ @include gl-ml-auto;
+ }
+
.gl-toggle-label {
@include gl-font-weight-normal;
}
}
-.dropdown-menu li button.gl-toggle:not(.is-checked) {
- background: $gray-400;
-}
-
-.mr-widget-content-row:first-child {
- border-top: 0;
-}
-
.mr-widget-status-icon-level-1::before {
@include gl-content-empty;
@include gl-absolute;
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 63bcb83e747..9ee6d17cb50 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -1,17 +1,7 @@
@import 'page_bundles/mixins_and_variables_and_functions';
-$status-box-line-height: 26px;
-
-.issues-sortable-list .str-truncated {
- max-width: 90%;
-}
-
.milestones {
.milestone {
- h4 {
- font-weight: $gl-font-weight-bold;
- }
-
.progress {
width: 100%;
height: 6px;
diff --git a/app/assets/stylesheets/page_bundles/oncall_schedules.scss b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
index 91fd2d42657..b995724ec7c 100644
--- a/app/assets/stylesheets/page_bundles/oncall_schedules.scss
+++ b/app/assets/stylesheets/page_bundles/oncall_schedules.scss
@@ -9,7 +9,7 @@
@include gl-w-full;
}
- .gl-new-dropdown-item-text-primary {
+ .gl-dropdown-item-text-primary {
@include gl-overflow-hidden;
@include gl-text-overflow-ellipsis;
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index 4946bbbebe5..f9c49b0e6ca 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -70,33 +70,20 @@
}
}
-// Mini Pipelines
-
-.stage-cell {
- .stage-container {
- &:last-child {
- margin-right: 0;
- }
-
- // Hack to show a button tooltip inline
- button.has-tooltip + .tooltip {
- min-width: 105px;
- }
-
- // Bootstrap way of showing the content inline for anchors.
- a.has-tooltip {
- white-space: nowrap;
- }
+// Pipeline mini graph
+.pipeline-mini-graph-stage-container {
+ &:last-child {
+ margin-right: 0;
+ }
- &:not(:last-child) {
- &::after {
- content: '';
- border-bottom: 2px solid $gray-200;
- position: absolute;
- right: -4px;
- top: 11px;
- width: 4px;
- }
+ &:not(:last-child) {
+ &::after {
+ content: '';
+ border-bottom: 2px solid $gray-200;
+ position: absolute;
+ right: -4px;
+ top: 11px;
+ width: 4px;
}
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index 1bca04e5eb1..10da541ed8d 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -1,17 +1,16 @@
+@import 'mixins_and_variables_and_functions';
+
$search-dropdown-max-height: 400px;
$search-avatar-size: 16px;
$search-sidebar-min-width: 240px;
$search-sidebar-max-width: 300px;
-$search-input-field-x-min-width: 200px;
-$search-input-field-min-width: 320px;
-$search-input-field-max-width: 640px;
$search-keyboard-shortcut: '/';
$border-radius-medium: 3px;
.search-results {
.search-result-row {
- border-bottom: 1px solid $border-color;
+ border-bottom: 1px solid var(--border-color, $border-color);
padding-bottom: $gl-padding;
margin-bottom: $gl-padding;
@@ -28,74 +27,6 @@ $border-radius-medium: 3px;
}
}
-.search form:hover,
-.file-finder-input:hover,
-.issuable-search-form:hover,
-.search-text-input:hover,
-.form-control:hover,
-:not[readonly] {
- border-color: lighten($blue-300, 20%);
- box-shadow: 0 0 4px lighten($dropdown-input-focus-shadow, 20%);
-}
-
-input[type='checkbox']:hover {
- box-shadow: 0 0 2px 2px lighten($dropdown-input-focus-shadow, 20%),
- 0 0 0 1px lighten($dropdown-input-focus-shadow, 20%);
-}
-
-.header-content {
- .header-search-new {
- max-width: $search-input-field-max-width;
- }
-
- &.header-search-is-active {
- .global-search-container {
- flex-grow: 1;
- }
- }
-}
-
-.header-search {
- min-width: $search-input-field-min-width;
-
- // This is a temporary workaround!
- // the button in GitLab UI Search components need to be updated to not be the small size
- // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
- .gl-search-box-by-type-clear.btn-sm {
- padding: 0.5rem !important;
- }
-
- @include media-breakpoint-between(md, lg) {
- min-width: $search-input-field-x-min-width;
- }
-
- &.is-searching {
- .in-search-scope-help {
- position: absolute;
- top: $gl-spacing-scale-2;
- right: 2.125rem;
- z-index: 2;
- }
- }
-
- &.is-not-focused {
- .gl-search-box-by-type-clear {
- display: none;
- }
- }
-
- .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
- }
-}
-
-.header-search-dropdown-menu {
- max-height: $dropdown-max-height;
- top: 100%;
-}
-
.search {
margin: 0 8px;
@@ -171,7 +102,7 @@ input[type='checkbox']:hover {
.dropdown-header {
// Necessary because deprecatedJQueryDropdown doesn't support a second style of headers
font-weight: $gl-font-weight-bold;
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
font-size: $gl-font-size;
line-height: 16px;
}
@@ -194,24 +125,24 @@ input[type='checkbox']:hover {
&.search-active {
form {
- border-color: $blue-300;
+ border-color: var(--blue-300, $blue-300);
box-shadow: none;
.search-input-wrap {
.search-icon,
.clear-icon {
- color: $gl-text-color-tertiary;
+ color: var(--gray-400, $gl-text-color-tertiary);
transition: color ease-in-out $default-transition-duration;
}
}
.search-input {
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
transition: color ease-in-out $default-transition-duration;
}
.search-input::placeholder {
- color: $gl-text-color-tertiary;
+ color: var(--gray-400, $gl-text-color-tertiary);
}
}
}
@@ -230,7 +161,7 @@ input[type='checkbox']:hover {
.inline-search-icon {
position: relative;
margin-right: 4px;
- color: $gl-text-color-secondary;
+ color: var(--gray-500, $gl-text-color-secondary);
}
.identicon,
@@ -244,7 +175,7 @@ input[type='checkbox']:hover {
width: $search-avatar-size;
height: $search-avatar-size;
border-radius: 50%;
- border: 1px solid $gray-normal;
+ border: 1px solid var(--gray-50, $gray-normal);
}
}
@@ -265,7 +196,7 @@ input[type='checkbox']:hover {
position: absolute;
left: 10px;
top: 9px;
- color: $gray-darkest;
+ color: var(--gray-700, $gray-darkest);
pointer-events: none;
}
@@ -283,7 +214,7 @@ input[type='checkbox']:hover {
.btn-search,
.btn-success,
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
width: 100%;
margin-top: 5px;
@@ -302,7 +233,7 @@ input[type='checkbox']:hover {
}
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
@include media-breakpoint-up(sm) {
width: 180px;
margin-top: 0;
@@ -317,7 +248,7 @@ input[type='checkbox']:hover {
}
.dropdown-menu-toggle,
- .gl-new-dropdown {
+ .gl-dropdown {
@include media-breakpoint-up(lg) {
width: 240px;
}
diff --git a/app/assets/stylesheets/page_bundles/settings.scss b/app/assets/stylesheets/page_bundles/settings.scss
new file mode 100644
index 00000000000..9037eb7ae62
--- /dev/null
+++ b/app/assets/stylesheets/page_bundles/settings.scss
@@ -0,0 +1,209 @@
+@import 'mixins_and_variables_and_functions';
+
+@keyframes expandMaxHeight {
+ 0% {
+ max-height: 0;
+ }
+
+ 99% {
+ max-height: 100vh;
+ }
+
+ 100% {
+ max-height: none;
+ }
+}
+
+@keyframes collapseMaxHeight {
+ 0% {
+ max-height: 100vh;
+ }
+
+ 100% {
+ max-height: 0;
+ }
+}
+
+.settings {
+ // border-top for each item except the top one
+ border-top: 1px solid var(--border-color, $border-color);
+
+ &:first-of-type {
+ margin-top: 10px;
+ padding-top: 0;
+ border: 0;
+ }
+
+ + div .settings:first-of-type {
+ margin-top: 0;
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+
+ &.animating {
+ overflow: hidden;
+ }
+}
+
+.settings-header {
+ position: relative;
+ padding: $gl-padding-24 110px 0 0;
+
+ h4 {
+ margin-top: 0;
+ }
+
+ .settings-title {
+ cursor: pointer;
+ }
+
+ button {
+ position: absolute;
+ top: 20px;
+ right: 6px;
+ min-width: 80px;
+ }
+}
+
+.settings-content {
+ max-height: 1px;
+ overflow-y: hidden;
+ padding-right: 110px;
+ animation: collapseMaxHeight 300ms ease-out;
+ // Keep the section from expanding when we scroll over it
+ pointer-events: none;
+
+ .settings.expanded & {
+ max-height: none;
+ overflow-y: visible;
+ animation: expandMaxHeight 300ms ease-in;
+ // Reset and allow clicks again when expanded
+ pointer-events: auto;
+ }
+
+ .settings.no-animate & {
+ animation: none;
+ }
+
+ @media(max-width: map-get($grid-breakpoints, md)-1) {
+ padding-right: 20px;
+ }
+
+ &::before {
+ content: ' ';
+ display: block;
+ height: 1px;
+ overflow: hidden;
+ margin-bottom: 4px;
+ }
+
+ &::after {
+ content: ' ';
+ display: block;
+ height: 1px;
+ overflow: hidden;
+ margin-top: 20px;
+ }
+
+ .sub-section {
+ margin-bottom: 32px;
+ padding: 16px;
+ border: 1px solid var(--border-color, $border-color);
+ background-color: var(--gray-light, $gray-light);
+ }
+
+ .bs-callout,
+ .form-check:first-child,
+ .form-check .form-text.text-muted,
+ .form-check + .form-text.text-muted {
+ margin-top: 0;
+ }
+
+ .form-check .form-text.text-muted {
+ margin-bottom: $grid-size;
+ }
+}
+
+.settings-list-icon {
+ color: var(--gray-500, $gl-text-color-secondary);
+ font-size: $default-icon-size;
+ line-height: 42px;
+}
+
+.settings-message {
+ padding: 5px;
+ line-height: 1.3;
+ color: var(--gray-900, $gray-900);
+ background-color: var(--orange-50, $orange-50);
+ border: 1px solid var(--orange-200, $orange-200);
+ border-radius: $gl-border-radius-base;
+}
+
+.prometheus-metrics-monitoring {
+ .card {
+ .card-toggle {
+ width: 14px;
+ }
+
+ .badge.badge-pill {
+ font-size: 12px;
+ line-height: 12px;
+ }
+
+ .card-header .label-count {
+ color: var(--white, $white);
+ background: var(--gray-800, $gray-800);
+ }
+
+ .card-body {
+ padding: 0;
+ }
+
+ .flash-container {
+ margin-bottom: 0;
+ cursor: default;
+
+ .flash-notice {
+ border-radius: 0;
+ }
+ }
+ }
+
+ .custom-monitored-metrics {
+ .card-header {
+ display: flex;
+ align-items: center;
+ }
+
+ .custom-metric {
+ display: flex;
+ align-items: center;
+ }
+
+ .custom-metric-link-bold {
+ font-weight: $gl-font-weight-bold;
+ text-decoration: none;
+ }
+ }
+
+ .loading-metrics .metrics-load-spinner {
+ color: var(--gray-700, $gray-700);
+ }
+
+ .metrics-list {
+ margin-bottom: 0;
+
+ li {
+ padding: $gl-padding;
+
+ .badge.badge-pill {
+ margin-left: 5px;
+ background: $badge-bg;
+ }
+
+ /* Ensure we don't add border if there's only single li */
+ + li {
+ border-top: 1px solid var(--border-color, $border-color);
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/todos.scss b/app/assets/stylesheets/page_bundles/todos.scss
index 3eacf98688e..b35f5b38740 100644
--- a/app/assets/stylesheets/page_bundles/todos.scss
+++ b/app/assets/stylesheets/page_bundles/todos.scss
@@ -7,8 +7,16 @@
.todos-list > .todo {
// workaround because we cannot use border-collapse
+ padding: 6px 12px !important;
border-top: 1px solid transparent;
+ span:not(.todo-label),
+ button,
+ a:not(.todo-target-link),
+ time {
+ @include gl-relative;
+ }
+
// overwrite border style of .content-list
&:last-child {
border-bottom: 1px solid transparent;
@@ -38,25 +46,66 @@
.todo-item {
@include transition(opacity);
- .todo-label,
- .todo-project {
- a {
- color: var(--blue-600, $blue-600);
- }
+ .todo-label a::before {
+ // Make area of the todo item clickable by expanding the area around the todo link
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-left-0;
+ @include gl-right-0;
+ @include gl-top-0;
+ @include gl-bottom-0;
+ z-index: 9;
}
+}
- .todo-body {
- p {
- color: var(--gl-text-color, $gl-text-color);
- }
+.todo-title {
+ margin-right: 2.5rem;
- .gl-label-scoped {
- --label-inset-border: inset 0 0 0 1px currentColor;
- }
+ @include media-breakpoint-up(sm) {
+ @include gl-mr-0;
+ @include gl-text-overflow-ellipsis;
+ @include gl-white-space-nowrap;
+ @include gl-overflow-hidden;
+ }
+}
- @include media-breakpoint-down(sm) {
- border-left: 2px solid var(--border-color, $border-color);
- padding-left: 10px;
- }
+.todo-body {
+ p {
+ @include gl-display-inline;
+ color: var(--gl-text-color, $gl-text-color);
+ }
+
+ pre.code.highlight {
+ @include gl-py-0;
+ @include gl-px-1;
+ @include gl-m-0;
+ @include gl-bg-gray-50;
+ @include gl-border-0;
+ @include gl-rounded-base;
+ @include gl-display-inline-flex;
+ @include gl-text-body;
+ }
+
+ .gl-label-scoped {
+ --label-inset-border: inset 0 0 0 1px currentColor;
+ }
+
+ .avatar {
+ @include gl-mb-0;
+ }
+}
+
+.todo-actions,
+.todo-body .todo-avatar,
+.todos-list > .todo a:not(.todo-target-link) {
+ z-index: 11 !important;
+}
+
+.todo-actions {
+ @include gl-absolute;
+ @include gl-right-0;
+
+ @include media-breakpoint-up(sm) {
+ @include gl-relative;
}
}
diff --git a/app/assets/stylesheets/page_bundles/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss
index 58e55e11f7e..50d9684c7d2 100644
--- a/app/assets/stylesheets/page_bundles/tree.scss
+++ b/app/assets/stylesheets/page_bundles/tree.scss
@@ -205,3 +205,18 @@
.blob-content-holder {
margin-top: $gl-padding;
}
+
+
+.web-ide-promo-popover {
+ box-shadow: 0 0 18px -1.9px rgba(119, 89, 194, 0.16),
+ 0 0 12.9px -1.7px rgba(119, 89, 194, 0.16), 0 0 9.2px -1.4px rgba(119, 89, 194, 0.16),
+ 0 0 6.4px -1.1px rgba(119, 89, 194, 0.16), 0 0 4.5px -0.8px rgba(119, 89, 194, 0.16),
+ 0 0 3px -0.6px rgba(119, 89, 194, 0.16), 0 0 1.8px -0.3px rgba(119, 89, 194, 0.16),
+ 0 0 0.6px rgba(119, 89, 194, 0.16);
+ z-index: 999;
+}
+
+.web-ide-promo-popover-illustration {
+ width: calc(100% + 24px);
+ margin: -28px -12px 0;
+}
diff --git a/app/assets/stylesheets/pages/users.scss b/app/assets/stylesheets/page_bundles/users.scss
index 3dcc17df61a..d4cd28504fc 100644
--- a/app/assets/stylesheets/pages/users.scss
+++ b/app/assets/stylesheets/page_bundles/users.scss
@@ -1,3 +1,5 @@
+@import 'mixins_and_variables_and_functions';
+
.user-search-form {
position: relative;
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index 820a1a0b53e..4766f124e5b 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -81,4 +81,8 @@
}
}
}
+
+ > .col {
+ min-width: 0;
+ }
}
diff --git a/app/assets/stylesheets/pages/colors.scss b/app/assets/stylesheets/pages/colors.scss
index 20e072b9903..d1917948c88 100644
--- a/app/assets/stylesheets/pages/colors.scss
+++ b/app/assets/stylesheets/pages/colors.scss
@@ -22,3 +22,11 @@
display: none;
}
}
+
+.warning-title {
+ color: var(--gray-900, $gray-900);
+}
+
+.danger-title {
+ color: var(--red-500, $red-500);
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 19318d87731..dd24e3fcb5d 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -338,11 +338,6 @@
color: $gl-text-color;
}
-.commit .gpg-popover-help-link {
- display: block;
- color: $link-color;
-}
-
.add-review-item {
.gl-tab-nav-item {
height: 100%;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index ce8dd6684f2..f6c79a4eca2 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -4,7 +4,7 @@
*/
.event-item {
font-size: $gl-font-size;
- padding: $gl-padding 0 $gl-padding 56px;
+ padding: $gl-padding 0 $gl-padding $gl-spacing-scale-8;
border-bottom: 1px solid $white-normal;
color: $gl-text-color-secondary;
position: relative;
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
deleted file mode 100644
index 1b6e7954366..00000000000
--- a/app/assets/stylesheets/pages/issuable.scss
+++ /dev/null
@@ -1,912 +0,0 @@
-.status-box {
- padding: 0 $gl-btn-padding;
- border-radius: $border-radius-default;
- display: block;
- float: left;
- margin-right: $gl-padding-8;
- color: $white;
- font-size: $gl-font-size;
- line-height: $gl-line-height-24;
-}
-
-.issuable-warning-icon {
- background-color: $orange-50;
- border-radius: $border-radius-default;
- color: $orange-600;
- 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;
-}
-
-.limit-container-width {
- .flash-container,
- .detail-page-header,
- .page-content-header,
- .commit-box,
- .info-well,
- .commit-ci-menu,
- .files-changed-inner,
- .limited-header-width,
- .limited-width-notes {
- @include fixed-width-container;
- }
-
- .issuable-details {
- .detail-page-description,
- .mr-source-target,
- .mr-state-widget,
- .merge-manually {
- @include fixed-width-container;
- }
- }
-
- .merge-request-details {
- .emoji-list-container {
- @include fixed-width-container;
- }
- }
-}
-
-.issuable-details {
- section {
- .issuable-discussion {
- margin-right: 1px;
- }
- }
-
- .title-container {
- display: flex;
- align-items: flex-start;
- }
-
- .title {
- padding: 0;
- margin-bottom: $gl-padding;
- border-bottom: 0;
- word-wrap: break-word;
- overflow-wrap: break-word;
- min-width: 0;
- width: 100%;
- text-align: initial;
- }
-
- .btn-edit {
- margin-left: auto;
- }
-}
-
-.issuable-show-labels {
- .gl-label {
- margin-bottom: 5px;
- margin-right: 5px;
- }
-
- a {
- display: inline-block;
-
- .color-label {
- padding: 4px $grid-size;
- border-radius: $label-border-radius;
- margin-right: 4px;
- margin-bottom: 4px;
- }
-
- &:hover .color-label {
- text-decoration: underline;
- }
- }
-
- &.has-labels {
- // this font size is a fix to
- // prevent unintended spacing between labels
- // which shows up when rendering markup has white-space
- // characters present.
- // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
- font-size: 0;
- margin-bottom: -5px;
- }
-}
-
-.assignee,
-.reviewer {
- .merge-icon {
- color: $orange-400;
- position: absolute;
- bottom: -3px;
- right: -3px;
- filter: drop-shadow(0 0 0.5px $white) drop-shadow(0 0 1px $white) drop-shadow(0 0 2px $white);
- }
-}
-
-@mixin right-sidebar {
- position: fixed;
- top: $header-height;
- // Default value for CSS var must contain a unit
- // stylelint-disable-next-line length-zero-no-unit
- bottom: var(--review-bar-height, 0px);
- right: 0;
- transition: width $gl-transition-duration-medium;
- background-color: $white;
- z-index: 200;
- overflow: hidden;
-
-}
-
-.right-sidebar {
- &:not(.right-sidebar-merge-requests) {
- @include right-sidebar;
- }
-
- &.right-sidebar-merge-requests {
- @include media-breakpoint-down(md) {
- @include right-sidebar;
- z-index: 251;
- }
- }
-
- @include media-breakpoint-down(sm) {
- z-index: 251;
- }
-
- a:not(.btn) {
- color: inherit;
-
- &:hover {
- color: $blue-800;
- }
- }
-
- .gl-label .gl-label-link:hover {
- color: inherit;
- }
-
- .btn-link {
- color: inherit;
- }
-
- .issuable-header-text {
- margin-top: 7px;
- }
-
- .gutter-toggle {
- display: flex;
- align-items: center;
- margin-left: 20px;
- padding: 4px;
- border-radius: 4px;
- height: 24px;
-
- &:hover {
- color: $gl-text-color;
- background: $gray-50;
- }
-
- &:hover,
- &:focus {
- text-decoration: none;
- }
- }
-
- &.right-sidebar-merge-requests {
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- width: 100%;
- }
-
- .block {
- @include media-breakpoint-up(lg) {
- padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
- }
-
- &.participants {
- border-bottom: 0;
- }
- }
- }
-
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- @include clearfix;
- padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
- border-bottom: 1px solid $border-gray-normal;
- // This prevents the mess when resizing the sidebar
- // of elements repositioning themselves..
- width: $gutter-inner-width;
- // --
-
- &:last-child {
- border: 0;
- }
-
- &.assignee {
- .author-link {
- display: block;
- position: relative;
-
- &:hover {
- .author {
- text-decoration: underline;
- }
- }
- }
- }
-
- &.time-tracking,
- &.participants,
- &.subscriptions,
- &.with-sub-blocks {
- padding-top: $gl-spacing-scale-5;
- }
- }
-
- .block-first {
- padding-top: 0;
- }
-
- .title {
- color: $gl-text-color;
- line-height: $gl-line-height-20;
-
- .avatar {
- margin-left: 0;
- }
- }
-
- .selectbox {
- display: none;
-
- &.show {
- display: block;
- }
- }
-
- .btn-clipboard:hover {
- color: $gl-text-color;
- }
-
- .issuable-sidebar {
- height: 100%;
-
- &:not(.is-merge-request) {
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
-
- &.is-merge-request {
- @include media-breakpoint-down(sm) {
- overflow-y: scroll;
- overflow-x: hidden;
- -webkit-overflow-scrolling: touch;
- }
- }
- }
-
- &.right-sidebar-expanded {
- &:not(.right-sidebar-merge-requests) {
- width: $gutter-width;
- }
-
- .value {
- line-height: 1;
- }
-
- .issuable-sidebar {
- padding: 0 20px;
-
- &.is-merge-request {
- @include media-breakpoint-up(lg) {
- padding: 0;
-
- .issuable-context-form {
- --initial-top: calc(#{$header-height} + 76px);
- --top: var(--initial-top);
-
- @include gl-sticky;
- @include gl-overflow-auto;
-
- top: var(--top);
- height: calc(100vh - var(--top));
- padding: 0 15px;
- margin-bottom: calc(var(--top) * -1);
-
- .with-performance-bar & {
- --top: calc(var(--initial-top) + #{$performance-bar-height});
- }
-
- .with-system-header & {
- --top: calc(var(--initial-top) + #{$system-header-height});
- }
-
- .with-performance-bar.with-system-header & {
- --top: calc(var(--initial-top) + #{$system-header-height} + #{$performance-bar-height});
- }
- }
- }
- }
- }
-
- &:not(.boards-sidebar):not([data-signed-in]):not([data-always-show-toggle]) {
- .issuable-sidebar-header {
- display: none;
- }
- }
-
- .light {
- font-weight: $gl-font-weight-normal;
- }
-
- .no-value {
- color: $gl-text-color-secondary;
- }
-
- .sidebar-collapsed-icon {
- display: none;
- }
-
- .gutter-toggle {
- text-align: center;
- }
-
- .title .gutter-toggle {
- margin-top: 0;
- }
-
- .assignee .user-list .avatar {
- margin: 0;
- }
-
- .hide-expanded {
- display: none;
- }
- }
-
- &.right-sidebar-collapsed {
- /* Extra small devices (phones, less than 768px) */
- display: none;
- /* Small devices (tablets, 768px and up) */
-
- &:not(.right-sidebar-merge-requests) {
- @include media-breakpoint-up(sm) {
- display: block;
- }
- }
-
- &.right-sidebar-merge-requests {
- @include media-breakpoint-up(lg) {
- display: block;
- }
- }
-
- width: $gutter-collapsed-width;
- padding: 0;
-
- .block,
- .sidebar-contained-width,
- .issuable-sidebar-header {
- width: $gutter-collapsed-width - 2px;
- padding: 0;
- border-bottom: 0;
- overflow: hidden;
- }
-
- .block,
- .gutter-toggle,
- .sidebar-collapsed-container {
- &.with-sub-blocks .sub-block:hover,
- &:not(.with-sub-blocks):hover {
- background-color: $gray-100;
- }
- }
-
- .participants {
- border-bottom: 1px solid $border-gray-normal;
- }
-
- .hide-collapsed {
- display: none;
- }
-
- .gutter-toggle {
- width: 100%;
- height: $sidebar-toggle-height;
- margin-left: 0;
- border-bottom: 1px solid $border-white-normal;
- border-radius: 0;
- }
-
- a.gutter-toggle {
- display: flex;
- justify-content: center;
- flex-direction: column;
- text-align: center;
- }
-
- .merge-icon {
- height: 12px;
- width: 12px;
- bottom: -5px;
- right: 4px;
- }
-
- .sidebar-collapsed-icon {
- display: flex;
- flex-direction: column;
- justify-content: center;
- align-items: center;
- width: 100%;
- height: $sidebar-toggle-height;
- text-align: center;
- color: $gl-text-color-secondary;
-
- > svg {
- fill: $gl-text-color-secondary;
- }
-
- &:hover:not(.disabled),
- &:hover .todo-undone {
- color: $gl-text-color;
-
- > svg {
- fill: $gl-text-color;
- }
- }
-
- .todo-undone {
- color: $blue-600;
- fill: $blue-600;
- }
-
- .author {
- display: none;
- }
-
- .avatar-counter:hover {
- color: $gl-text-color-secondary;
- border-color: $gl-text-color-secondary;
- }
-
- .btn-clipboard {
- /*
- This change should be temporary, because the DOM currently gets
- generated from a ruby definition in `app/helpers/button_helper.rb`.
- As soon as the `copy to clipboard` button will be transferred to
- Vue this should be adjusted as well.
- */
- flex: 1;
- align-self: stretch;
- padding: 0;
-
- border: 0;
- background: transparent;
- color: $gl-text-color-secondary;
-
- &:hover {
- color: $gl-text-color;
- }
- }
-
- &.multiple-users {
- display: flex;
- justify-content: center;
- }
- }
-
- .sidebar-avatar-counter {
- width: 24px;
- height: 24px;
- border-radius: 12px;
-
- ~.merge-icon {
- bottom: 0;
- }
- }
-
- .sidebar-collapsed-user {
- padding-bottom: 0;
-
- .author-link {
- padding-left: 0;
-
- .avatar {
- position: static;
- margin: 0;
- }
- }
- }
-
- .issuable-header-btn {
- display: none;
- }
-
- .multiple-users {
- .btn-link {
- padding: 0;
- border: 0;
-
- .avatar {
- margin: 0;
- }
- }
-
- .btn-link:first-child {
- position: absolute;
- left: 10px;
- z-index: 1;
- }
-
- .btn-link:last-child {
- position: absolute;
- right: 10px;
-
- &:hover {
- text-decoration: none;
- }
- }
- }
-
- .milestone-title span,
- .collapse-truncated-title {
- @include str-truncated(100%);
- display: block;
- margin: 0 4px;
- }
- }
-
- .dropdown-menu-toggle {
- width: 100%;
- padding-top: 6px;
- }
-
- .dropdown-menu {
- width: 100%;
-
- /*
- * Overwrite hover style for dropdown items, so that they are not blue
- * This should be removed during dev of https://gitlab.com/gitlab-org/gitlab-foss/issues/44040
- */
- li a {
- &:hover,
- &:active,
- &:focus,
- &.is-focused {
- @include dropdown-item-hover;
- }
- }
-
- }
-}
-
-.with-performance-bar .right-sidebar {
- top: calc(#{$header-height} + #{$performance-bar-height});
-}
-
-.sidebar-move-issue-confirmation-button {
- width: 100%;
-
- &.is-loading {
- .sidebar-move-issue-confirmation-loading-icon {
- display: inline-block;
- }
- }
-}
-
-.sidebar-move-issue-confirmation-loading-icon {
- display: none;
-}
-
-.detail-page-description {
- padding: 16px 0;
-
- small {
- color: $gray-500;
- }
-}
-
-.edited-text {
- color: $gray-500;
- display: block;
- margin: 16px 0 0;
- font-size: 85%;
-
- .author-link {
- color: $gray-500;
- }
-}
-
-.participants-author {
- &:nth-of-type(8n) {
- padding-right: 0;
- }
-
- .avatar.avatar-inline {
- margin: 0;
- }
-}
-
-.user-item {
- padding: 5px;
- flex-basis: 20%;
-
- .user-link {
- display: inline-block;
- }
-}
-
-.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;
- }
-}
-
-.issuable-gutter-toggle {
- @include media-breakpoint-down(sm) {
- margin-left: $btn-side-margin;
- }
-}
-
-.issuable-meta {
- flex: 1;
- display: inline-block;
- font-size: 14px;
- align-self: center;
- overflow: hidden;
- text-overflow: ellipsis;
-
- .user-status-emoji {
- margin-left: $gl-padding-4;
- margin-right: 0;
- }
-}
-
-.js-issuable-selector-wrap {
- .js-issuable-selector {
- width: 100%;
- }
-
- @include media-breakpoint-down(sm) {
- margin-bottom: $gl-padding;
- }
-}
-
-.issuable-list {
- li {
- .issuable-info-container {
- flex: 1;
- display: flex;
- }
-
- .issuable-main-info {
- flex: 1 auto;
- margin-right: 10px;
- min-width: 0;
-
- .issue-weight-icon,
- .issue-estimate-icon {
- vertical-align: sub;
- }
- }
-
- .issuable-meta {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- flex: 1 0 auto;
-
- .controls {
- margin-bottom: 2px;
- line-height: 20px;
- padding: 0;
- }
- }
-
- @include media-breakpoint-down(xs) {
- .issuable-meta {
- .controls li {
- margin-right: 0;
- }
- }
- }
-
- .issue-check {
- min-width: 15px;
- }
-
- .issuable-milestone,
- .issuable-info,
- .task-status,
- .issuable-timestamp {
- font-weight: $gl-font-weight-normal;
- color: $gl-text-color-secondary;
-
- a {
- color: $gl-text-color;
- }
-
- .gl-label-link {
- color: inherit;
-
- &:hover {
- text-decoration: none;
-
- .gl-label-text:last-of-type {
- text-decoration: underline;
- }
- }
- }
-
- .milestone {
- color: $gray-700;
- }
- }
-
- @media(max-width: map-get($grid-breakpoints, lg)-1) {
- .task-status,
- .issuable-due-date,
- .issuable-weight,
- .project-ref-path {
- display: none;
- }
- }
- }
-}
-
-.issuable-list li,
-.issuable-info-container .controls {
- .avatar-counter {
- display: inline-block;
- vertical-align: middle;
- min-width: 16px;
- line-height: 14px;
- height: 16px;
- padding-left: 2px;
- padding-right: 2px;
- }
-}
-
-.add-issuable-form-input-wrapper {
- &.focus {
- border-color: $gray-700;
- @include gl-focus;
-
- input {
- @include gl-shadow-none;
- }
- }
-
- .gl-show-field-errors &.form-control:not(textarea) {
- height: auto;
- }
-}
-
-.sidebar-help-wrap {
- .sidebar-help-state {
- margin: 16px -20px -20px;
- padding: 16px 20px;
- }
-
- .help-state-toggle-enter-active {
- transition: all 0.8s ease;
- }
-
- .help-state-toggle-leave-active {
- transition: all 0.5s ease;
- }
-
- .help-state-toggle-enter,
- .help-state-toggle-leave-active {
- opacity: 0;
- }
-}
-
-.time-tracker {
- .sidebar-collapsed-icon {
- > .stopwatch-svg {
- display: inline-block;
- }
-
- svg {
- width: 16px;
- height: 16px;
- fill: $gl-text-color-secondary;
- }
-
- &:hover svg {
- fill: $gl-text-color;
- }
- }
-
- .compare-meter {
- &.over_estimate {
- .time-remaining,
- .compare-value.spent {
- color: $red-500;
- }
- }
- }
-
- .compare-display-container {
- font-size: 13px;
- }
-}
-
-/*
- * Following overrides are done to prevent
- * legacy dropdown styles from influencing
- * GitLab UI components used within GlDropdown
- */
-.issuable-move-dropdown {
- .b-dropdown-form {
- @include gl-p-0;
- }
-
- .gl-search-box-by-type button.gl-clear-icon-button:hover {
- @include gl-bg-transparent;
- }
-
- .issuable-move-button:not(.disabled):hover {
- @include gl-text-white;
- }
-}
-
-.right-sidebar-collapsed {
- .sidebar-grouped-item {
- .sidebar-collapsed-icon {
- margin-bottom: 0;
- }
-
- .sidebar-collapsed-divider {
- line-height: 5px;
- font-size: 12px;
- color: $gray-500;
-
- + .sidebar-collapsed-icon {
- padding-top: 0;
- }
- }
- }
-}
-
-.suggestion-footer {
- font-size: 12px;
- line-height: 15px;
-
- .avatar {
- margin-top: -3px;
- border: 0;
- }
-}
-
-@include media-breakpoint-down(sm) {
- // Overriding the following rule with the negative margin
- // https://gitlab.com/gitlab-org/gitlab/-/blob/146c43c931c3743a140529307aea616e4aa9ff21/app/assets/stylesheets/framework/sidebar.scss#L1-5
- .container-fluid {
- .issuable-list,
- .issues-filters,
- .epics-filters {
- margin: 0 (-$gl-padding);
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index d4ad6da7f4d..360ea20733d 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -262,7 +262,7 @@
.footer-container,
hr.footer-fixed {
- position: absolute;
+ position: fixed;
bottom: 0;
left: 0;
right: 0;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b016d0a1068..6b662359a67 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -382,3 +382,9 @@ $comparison-empty-state-height: 62px;
.survey-slide-up-enter-active {
@include gl-transition-slow;
}
+
+.mr-compare-dropdown {
+ .gl-button-text {
+ @include gl-w-full;
+ }
+}
diff --git a/app/assets/stylesheets/pages/ml_experiment_tracking.scss b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
index 2dff51cff92..c1582f2090b 100644
--- a/app/assets/stylesheets/pages/ml_experiment_tracking.scss
+++ b/app/assets/stylesheets/pages/ml_experiment_tracking.scss
@@ -14,3 +14,9 @@
color: $gl-text-color;
}
}
+
+table.candidate-details {
+ td {
+ padding: $gl-spacing-scale-3;
+ }
+}
diff --git a/app/assets/stylesheets/pages/monitor.scss b/app/assets/stylesheets/pages/monitor.scss
deleted file mode 100644
index 25ff5abd774..00000000000
--- a/app/assets/stylesheets/pages/monitor.scss
+++ /dev/null
@@ -1,5 +0,0 @@
-.chart-tooltip > .popover {
- min-width: 0;
- width: max-content;
- max-width: $chart-tooltip-max-width;
-}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index cb77c31d59a..adeab227670 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -287,8 +287,7 @@ table {
.discussion-reply-holder {
.reply-placeholder-text-field {
- font-family: $monospace-font;
- font-size: $gl-font-size-monospace;
+ @include gl-font-monospace;
border-radius: $gl-border-radius-base;
width: 100%;
resize: none;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index fa3c87490f1..75d52424fd9 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -4,7 +4,7 @@ $system-note-svg-size: 1rem;
@mixin vertical-line($left) {
&::before {
content: '';
- border-left: 2px solid var(--gray-10, $gray-10);
+ border-left: 2px solid var(--gray-50, $gray-50);
position: absolute;
top: $gl-padding-6;
bottom: 0;
@@ -60,6 +60,10 @@ $system-note-svg-size: 1rem;
padding: $gl-padding-4 $gl-padding-8;
}
+ &.draft-note .timeline-content:not(.flash-container) {
+ border: 0;
+ }
+
.note-header-info {
min-height: 2rem;
display: flex;
@@ -81,7 +85,7 @@ $system-note-svg-size: 1rem;
margin-top: 5px;
}
- .timeline-content {
+ .timeline-content:not(.flash-container) {
margin-left: 2.5rem;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
@@ -93,16 +97,26 @@ $system-note-svg-size: 1rem;
}
}
+ &.draft-note .timeline-content:not(.flash-container) {
+ margin-left: 0;
+ border-top-left-radius: 0;
+ border-top-right-radius: 0;
+ }
+
&:not(:first-of-type) .timeline-entry-inner {
margin-left: 2.5rem;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
background-color: $white;
- .timeline-content {
+ .timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
+ .timeline-discussion-body-footer {
+ padding: 0 $gl-padding-8 0;
+ }
+
.timeline-avatar {
margin: $gl-padding-8 0 0 $gl-padding;
}
@@ -111,6 +125,12 @@ $system-note-svg-size: 1rem;
margin-left: 2rem;
}
}
+
+ &:last-of-type .timeline-entry-inner {
+ border-bottom: 1px solid $border-color;
+ border-bottom-left-radius: $gl-border-radius-base;
+ border-bottom-right-radius: $gl-border-radius-base;
+ }
}
.diff-content {
@@ -416,17 +436,17 @@ $system-note-svg-size: 1rem;
.timeline-icon {
display: flex;
align-items: center;
- background-color: $gray-10;
+ background-color: $gray-50;
width: $system-note-icon-size;
height: $system-note-icon-size;
- border: 1px solid $gray-10;
+ border: 1px solid $gray-50;
border-radius: $system-note-icon-size;
margin: -6px 0 0;
svg {
width: $system-note-svg-size;
height: $system-note-svg-size;
- fill: $gray-400;
+ fill: $gray-600;
display: block;
margin: 0 auto;
}
@@ -1050,7 +1070,7 @@ $system-note-svg-size: 1rem;
padding-left: 0;
ul.notes li.note-wrapper {
- .timeline-content {
+ .timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
@@ -1066,7 +1086,7 @@ $system-note-svg-size: 1rem;
border-right: 0;
}
- div.discussion-reply-holder {
+ .discussion-reply-holder {
margin-left: 0;
}
}
@@ -1097,7 +1117,7 @@ $system-note-svg-size: 1rem;
}
}
- .draft-note-component .draft-note.timeline-entry {
+ .draft-note-component.draft-note.timeline-entry {
.timeline-content:not(.flash-container) {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index bf20204cfd9..15a32ea8ad3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -247,7 +247,7 @@
.repository-languages-bar {
height: 8px;
- margin-bottom: $gl-padding-8;
+ margin-bottom: $gl-padding;
background-color: $white;
border-radius: $border-radius-default;
@@ -562,7 +562,7 @@
// Remove once gitlab/ui solution is implemented
// https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1158
// https://gitlab.com/gitlab-org/gitlab/-/issues/300405
- .gl-new-dropdown-button-text {
+ .gl-dropdown-button-text {
@include str-truncated;
}
}
@@ -654,86 +654,3 @@
}
}
}
-
-.project-filters {
- .btn svg {
- color: $gray-700;
- }
-
- .button-filter-group {
- .btn {
- width: 96px;
- }
-
- a {
- color: $black;
- }
-
- .active {
- background: $btn-active-gray;
- }
- }
-
- .filtered-search-dropdown-label {
- min-width: 68px;
-
- @include media-breakpoint-down(xs) {
- min-width: 60px;
- }
- }
-
- .filtered-search {
- min-width: 30%;
- flex-basis: 0;
-
- .project-filter-form .project-filter-form-field {
- padding-right: $gl-padding-8;
- }
-
- .filtered-search,
- .filtered-search-nav,
- .filtered-search-dropdown {
- flex-basis: 0;
- }
-
- @include media-breakpoint-down(lg) {
- min-width: 15%;
-
- .project-filter-form-field {
- min-width: 150px;
- }
- }
-
- @include media-breakpoint-down(md) {
- min-width: 30%;
- }
- }
-
- .filtered-search-box {
- border-radius: 3px 0 0 3px;
- }
-
- .dropdown-menu-toggle {
- margin-left: $gl-padding-8;
- }
-
- @include media-breakpoint-down(md) {
- .extended-filtered-search-box {
- min-width: 55%;
- }
-
- .filtered-search-dropdown {
- width: 50%;
-
- .dropdown-menu-toggle {
- width: 100%;
- }
- }
- }
-
- @include media-breakpoint-down(xs) {
- .filtered-search-dropdown {
- width: 100%;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index c364b233803..2d78ab82b7d 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -1,149 +1,3 @@
-@keyframes expandMaxHeight {
- 0% {
- max-height: 0;
- }
-
- 99% {
- max-height: 100vh;
- }
-
- 100% {
- max-height: none;
- }
-}
-
-@keyframes collapseMaxHeight {
- 0% {
- max-height: 100vh;
- }
-
- 100% {
- max-height: 0;
- }
-}
-
-.settings {
- // border-top for each item except the top one
- border-top: 1px solid $border-color;
-
- &:first-of-type {
- margin-top: 10px;
- padding-top: 0;
- border: 0;
- }
-
- + div .settings:first-of-type {
- margin-top: 0;
- border-top: 1px solid $border-color;
- }
-
- &.animating {
- overflow: hidden;
- }
-}
-
-.settings-header {
- position: relative;
- padding: 24px 110px 0 0;
-
- h4 {
- margin-top: 0;
- }
-
- .settings-title {
- cursor: pointer;
- }
-
- button {
- position: absolute;
- top: 20px;
- right: 6px;
- min-width: 80px;
- }
-}
-
-.settings-content {
- max-height: 1px;
- overflow-y: hidden;
- padding-right: 110px;
- animation: collapseMaxHeight 300ms ease-out;
- // Keep the section from expanding when we scroll over it
- pointer-events: none;
-
- .settings.expanded & {
- max-height: none;
- overflow-y: visible;
- animation: expandMaxHeight 300ms ease-in;
- // Reset and allow clicks again when expanded
- pointer-events: auto;
- }
-
- .settings.no-animate & {
- animation: none;
- }
-
- @media(max-width: map-get($grid-breakpoints, md)-1) {
- padding-right: 20px;
- }
-
- &::before {
- content: ' ';
- display: block;
- height: 1px;
- overflow: hidden;
- margin-bottom: 4px;
- }
-
- &::after {
- content: ' ';
- display: block;
- height: 1px;
- overflow: hidden;
- margin-top: 20px;
- }
-
- .sub-section {
- margin-bottom: 32px;
- padding: 16px;
- border: 1px solid $border-color;
- background-color: $gray-light;
- }
-
- .bs-callout,
- .form-check:first-child,
- .form-check .form-text.text-muted,
- .form-check + .form-text.text-muted {
- margin-top: 0;
- }
-
- .form-check .form-text.text-muted {
- margin-bottom: $grid-size;
- }
-}
-
-.settings-list-icon {
- color: $gl-text-color-secondary;
- font-size: $default-icon-size;
- line-height: 42px;
-}
-
-.settings-message {
- padding: 5px;
- line-height: 1.3;
- color: $gray-900;
- background-color: $orange-50;
- border: 1px solid $orange-200;
- border-radius: $border-radius-base;
-}
-
-.warning-title {
- color: $gray-900;
-}
-
-.danger-title {
- color: $red-500;
-}
-
.integration-settings-form {
.card.card-body,
.info-well {
@@ -160,13 +14,13 @@
.option-title {
font-weight: $gl-font-weight-normal;
display: inline-block;
- color: $gl-text-color;
+ color: var(--gl-text-color, $gl-text-color);
vertical-align: top;
}
.option-description,
.option-disabled-reason {
- color: $project-option-descr-color;
+ color: var(--gray-700, $gray-700);
}
.option-disabled-reason {
@@ -188,79 +42,9 @@
}
}
-.prometheus-metrics-monitoring {
- .card {
- .card-toggle {
- width: 14px;
- }
-
- .badge.badge-pill {
- font-size: 12px;
- line-height: 12px;
- }
-
- .card-header .label-count {
- color: $white;
- background: $common-gray-dark;
- }
-
- .card-body {
- padding: 0;
- }
-
- .flash-container {
- margin-bottom: 0;
- cursor: default;
-
- .flash-notice {
- border-radius: 0;
- }
- }
- }
-
- .custom-monitored-metrics {
- .card-header {
- display: flex;
- align-items: center;
- }
-
- .custom-metric {
- display: flex;
- align-items: center;
- }
-
- .custom-metric-link-bold {
- font-weight: $gl-font-weight-bold;
- text-decoration: none;
- }
- }
-
- .loading-metrics .metrics-load-spinner {
- color: $gray-700;
- }
-
- .metrics-list {
- margin-bottom: 0;
-
- li {
- padding: $gl-padding;
-
- .badge.badge-pill {
- margin-left: 5px;
- background: $badge-bg;
- }
-
- /* Ensure we don't add border if there's only single li */
- + li {
- border-top: 1px solid $border-color;
- }
- }
- }
-}
-
.saml-settings.info-well {
.form-control[readonly] {
- background: $white;
+ background: var(--white, $white);
}
}
@@ -275,8 +59,8 @@
}
.btn-clipboard {
- background-color: $white;
- border: 1px solid $gray-100;
+ background-color: var(--white, $white);
+ border: 1px solid var(--gray-100, $gray-100);
}
.deploy-token-help-block {
@@ -294,7 +78,7 @@
.ci-secure-files-table {
table {
thead {
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid var(--gray-50, $gray-50);
}
tr {
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index 11131cc1a4b..c7e55289b11 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -2,25 +2,6 @@
// Please see the feedback issue for more details and help:
// https://gitlab.com/gitlab-org/gitlab/-/issues/331812
@charset "UTF-8";
-:root {
- color-scheme: dark;
-}
-body.gl-dark {
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-700: #bfbfc3;
- --gray-900: #ececef;
- --green-100: #0d532a;
- --green-700: #91d4a8;
- --gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --black: #fff;
-}
-:root {
- --white: #333;
-}
*,
*::before,
*::after {
@@ -36,9 +17,10 @@ header {
}
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -66,8 +48,9 @@ a:not([href]):not([class]) {
text-decoration: none;
}
kbd {
- font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
- "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-family: var(--default-mono-font, "Menlo"), "DejaVu Sans Mono",
+ "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono",
+ "lucida console", monospace;
font-size: 1em;
}
img {
@@ -117,7 +100,7 @@ button::-moz-focus-inner,
kbd {
padding: 0.2rem 0.4rem;
font-size: 90%;
- color: #333;
+ color: #333238;
background-color: #ececef;
border-radius: 0.2rem;
}
@@ -142,7 +125,7 @@ kbd kbd {
font-weight: 400;
line-height: 1.5;
color: #ececef;
- background-color: #333;
+ background-color: #333238;
background-clip: padding-box;
border: 1px solid #737278;
border-radius: 0.25rem;
@@ -158,7 +141,7 @@ kbd kbd {
opacity: 1;
}
.form-control:disabled {
- background-color: #333238;
+ background-color: #24232a;
opacity: 1;
}
.form-inline {
@@ -215,7 +198,7 @@ kbd kbd {
color: #ececef;
text-align: left;
list-style: none;
- background-color: #333;
+ background-color: #333238;
background-clip: padding-box;
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 0.25rem;
@@ -410,7 +393,7 @@ a.gl-badge.badge-info:active {
background-color: #0b5cad;
}
a.gl-badge.badge-info:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-success {
@@ -423,7 +406,7 @@ a.gl-badge.badge-success:active {
background-color: #24663b;
}
a.gl-badge.badge-success:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-badge.badge-warning {
@@ -436,7 +419,7 @@ a.gl-badge.badge-warning:active {
background-color: #8f4700;
}
a.gl-badge.badge-warning:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.gl-button .gl-badge {
@@ -444,10 +427,11 @@ a.gl-badge.badge-warning:active {
}
.gl-form-input,
.gl-form-input.form-control {
- background-color: #333;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ background-color: #333238;
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 0.875rem;
line-height: 1rem;
padding-top: 0.5rem;
@@ -526,12 +510,21 @@ a.gl-badge.badge-warning:active {
font-size: 0.875rem;
border-radius: 0.25rem;
}
+.gl-button.gl-button .gl-button-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
.gl-button.gl-button.btn-default {
- background-color: #333;
+ background-color: #333238;
}
.gl-button.gl-button.btn-default:active,
.gl-button.gl-button.btn-default.active {
- box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: inset 0 0 0 1px #a4a3a8, 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
background-color: #434248;
}
@@ -613,7 +606,7 @@ html {
font-size: 0.875rem;
font-weight: 400;
padding: 6px 10px;
- background-color: #333;
+ background-color: #333238;
border-color: #434248;
color: #ececef;
color: #ececef;
@@ -625,7 +618,7 @@ html {
}
.btn:active,
.btn.active {
- background-color: #444;
+ background-color: #434248;
border-color: #4f4f4f;
color: #ececef;
}
@@ -649,7 +642,7 @@ html {
position: relative;
}
.dropdown-menu-toggle:active {
- box-shadow: 0 0 0 1px #333, 0 0 0 3px #1f75cb;
+ box-shadow: 0 0 0 1px #333238, 0 0 0 3px #1f75cb;
outline: none;
}
.search-input-container .dropdown-menu {
@@ -657,7 +650,7 @@ html {
}
.dropdown-menu-toggle {
padding: 6px 8px 6px 10px;
- background-color: #333;
+ background-color: #333238;
color: #ececef;
font-size: 14px;
text-align: left;
@@ -689,7 +682,7 @@ html {
font-size: 0.875rem;
font-weight: 400;
padding: 8px 0;
- background-color: #333;
+ background-color: #333238;
border: 1px solid #434248;
border-radius: 0.25rem;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
@@ -704,7 +697,7 @@ html {
list-style: none;
}
.dropdown-menu li > a,
-.dropdown-menu li button {
+.dropdown-menu li > button {
background: transparent;
border: 0;
border-radius: 0;
@@ -721,16 +714,16 @@ html {
width: 100%;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
- background-color: #4f4f4f;
+.dropdown-menu li > button:active {
+ background-color: #4e4c53;
color: #ececef;
outline: 0;
text-decoration: none;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
- box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333,
- inset 0 0 0 1px #333;
+.dropdown-menu li > button:active {
+ box-shadow: inset 0 0 0 2px #1f75cb, inset 0 0 0 3px #333238,
+ inset 0 0 0 1px #333238;
outline: none;
}
.dropdown-menu .divider {
@@ -765,7 +758,7 @@ html {
input {
border-radius: 0.25rem;
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
.form-control {
border-radius: 4px;
@@ -777,10 +770,10 @@ input {
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: 0.6875rem;
+ font-size: 0.75rem;
line-height: 10px;
color: var(--gray-700, #bfbfc3);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, #1f1e24);
border-width: 1px;
border-style: solid;
@@ -840,6 +833,22 @@ kbd {
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+.navbar-gitlab .header-content .header-search-new {
+ max-width: 640px;
+}
+.navbar-gitlab .header-search {
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .navbar-gitlab .header-search {
+ min-width: 200px;
+ }
+}
+.navbar-gitlab .header-search .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+}
.navbar-gitlab .navbar-collapse {
flex: 0 0 auto;
border-top: 0;
@@ -1010,7 +1019,7 @@ kbd {
float: left;
margin-right: 5px;
border-radius: 50%;
- border: 1px solid #333;
+ border: 1px solid #333238;
}
.notification-dot {
background-color: #9e5400;
@@ -1049,7 +1058,7 @@ kbd {
}
.context-header .avatar-container {
flex: 0 0 32px;
- background-color: #333;
+ background-color: #333238;
}
.context-header .sidebar-context-title {
overflow: hidden;
@@ -1142,7 +1151,7 @@ kbd {
font-weight: 600;
}
.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(255, 255, 255, 0.08);
+ background-color: rgba(251, 250, 253, 0.08);
}
.nav-sidebar ul {
padding-left: 0;
@@ -1189,7 +1198,7 @@ kbd {
margin-bottom: -0.25rem;
margin-top: 0;
position: relative;
- color: #333;
+ color: #333238;
background: var(--black, #fff);
}
.nav-sidebar
@@ -1417,7 +1426,7 @@ kbd {
.close-nav-button {
height: 48px;
padding: 0 16px;
- background-color: #333238;
+ background-color: #24232a;
border: 0;
color: #89888d;
display: flex;
@@ -1523,87 +1532,6 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
-.header-content .header-search-new {
- max-width: 640px;
-}
-.header-search {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .header-search {
- min-width: 200px;
- }
-}
-.header-search .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.search {
- margin: 0 8px;
-}
-.search form {
- display: block;
- margin: 0;
- padding: 4px;
- width: 200px;
- line-height: 24px;
- height: 32px;
- border: 0;
- border-radius: 4px;
-}
-@media (min-width: 1200px) {
- .search form {
- width: 320px;
- }
-}
-.search .search-input {
- border: 0;
- font-size: 14px;
- padding: 0 20px 0 0;
- margin-left: 5px;
- line-height: 25px;
- width: 98%;
- color: #333;
- background: none;
-}
-.search .search-input-container {
- display: flex;
- position: relative;
-}
-.search .search-input-wrap {
- width: 100%;
-}
-.search .search-input-wrap .search-icon,
-.search .search-input-wrap .clear-icon {
- position: absolute;
- right: 5px;
- top: 4px;
-}
-.search .search-input-wrap .search-icon {
- user-select: none;
-}
-.search .search-input-wrap .clear-icon {
- display: none;
-}
-.search .search-input-wrap .dropdown {
- position: static;
-}
-.search .search-input-wrap .dropdown-menu {
- left: -5px;
- max-height: 400px;
- overflow: auto;
-}
-@media (min-width: 1200px) {
- .search .search-input-wrap .dropdown-menu {
- width: 320px;
- }
-}
-.search .identicon {
- flex-basis: 16px;
- flex-shrink: 0;
- margin-right: 4px;
-}
.avatar,
.avatar-container {
float: left;
@@ -1627,7 +1555,7 @@ svg.s16 {
width: 40px;
height: 40px;
padding: 0;
- background: #222;
+ background: #212027;
overflow: hidden;
box-shadow: inset 0 0 0 1px rgba(251, 250, 253, 0.1);
}
@@ -1705,97 +1633,25 @@ svg.s16 {
}
:root {
color-scheme: dark;
-}
-body.gl-dark {
--gray-10: #1f1e24;
--gray-50: #333238;
--gray-100: #434248;
--gray-200: #535158;
- --gray-300: #626168;
- --gray-400: #737278;
- --gray-500: #89888d;
- --gray-600: #a4a3a8;
--gray-700: #bfbfc3;
- --gray-800: #dcdcde;
--gray-900: #ececef;
- --gray-950: #fbfafd;
- --green-50: #0a4020;
--green-100: #0d532a;
- --green-200: #24663b;
- --green-300: #217645;
- --green-400: #108548;
- --green-500: #2da160;
- --green-600: #52b87a;
--green-700: #91d4a8;
- --green-800: #c3e6cd;
- --green-900: #ecf4ee;
- --green-950: #f1fdf6;
- --blue-50: #033464;
- --blue-100: #064787;
- --blue-200: #0b5cad;
- --blue-300: #1068bf;
- --blue-400: #1f75cb;
- --blue-500: #428fdc;
- --blue-600: #63a6e9;
- --blue-700: #9dc7f1;
- --blue-800: #cbe2f9;
- --blue-900: #e9f3fc;
- --blue-950: #f2f9ff;
- --orange-50: #5c2900;
- --orange-100: #703800;
- --orange-200: #8f4700;
- --orange-300: #9e5400;
- --orange-400: #ab6100;
- --orange-500: #c17d10;
- --orange-600: #d99530;
- --orange-700: #e9be74;
- --orange-800: #f5d9a8;
- --orange-900: #fdf1dd;
- --orange-950: #fff4e1;
- --red-50: #660e00;
- --red-100: #8d1300;
- --red-200: #ae1800;
- --red-300: #c91c00;
- --red-400: #dd2b0e;
- --red-500: #ec5941;
- --red-600: #f57f6c;
- --red-700: #fcb5aa;
- --red-800: #fdd4cd;
- --red-900: #fcf1ef;
- --red-950: #fff4f3;
- --indigo-50: #1a1a40;
- --indigo-100: #292961;
- --indigo-200: #393982;
- --indigo-300: #4b4ba3;
- --indigo-400: #5b5bbd;
- --indigo-500: #6666c4;
- --indigo-600: #7c7ccc;
- --indigo-700: #a6a6de;
- --indigo-800: #d1d1f0;
- --indigo-900: #ebebfa;
- --indigo-950: #f7f7ff;
- --purple-50: #232150;
- --purple-100: #2f2a6b;
- --purple-200: #453894;
- --purple-300: #5943b6;
- --purple-400: #694cc0;
- --purple-500: #7b58cf;
- --purple-600: #9475db;
- --purple-700: #ac93e6;
- --purple-800: #cbbbf2;
- --purple-900: #e1d8f9;
- --purple-950: #f4f0ff;
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
--gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --white: #333;
+ --border-color: #434248;
+ --white: #333238;
+ --black: #fff;
+}
+body.gl-dark {
+ color-scheme: dark;
+ --gray-10: #1f1e24;
+ --border-color: #434248;
+ --white: #333238;
--black: #fff;
- --gray-light: #333238;
- --svg-status-bg: #333;
}
.nav-sidebar,
.toggle-sidebar-button,
@@ -1830,7 +1686,7 @@ body.gl-dark .navbar-gitlab .navbar-sub-nav > li.active > button,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > a,
body.gl-dark .navbar-gitlab .navbar-nav > li.active > button {
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
body.gl-dark .navbar-gitlab .navbar-sub-nav {
color: #ececef;
@@ -1862,10 +1718,10 @@ body.gl-dark
}
body.gl-dark .navbar-gitlab .nav > li.active > a {
color: #ececef;
- background-color: #333;
+ background-color: #333238;
}
body.gl-dark .navbar-gitlab .nav > li.active > a .notification-dot {
- border-color: #333;
+ border-color: #333238;
}
body.gl-dark
.navbar-gitlab
@@ -1947,100 +1803,6 @@ body.gl-dark .navbar-gitlab .search form .search-input {
color: var(--gl-text-color);
}
-:root {
- color-scheme: dark;
-}
-body.gl-dark {
- --gray-10: #1f1e24;
- --gray-50: #333238;
- --gray-100: #434248;
- --gray-200: #535158;
- --gray-300: #626168;
- --gray-400: #737278;
- --gray-500: #89888d;
- --gray-600: #a4a3a8;
- --gray-700: #bfbfc3;
- --gray-800: #dcdcde;
- --gray-900: #ececef;
- --gray-950: #fbfafd;
- --green-50: #0a4020;
- --green-100: #0d532a;
- --green-200: #24663b;
- --green-300: #217645;
- --green-400: #108548;
- --green-500: #2da160;
- --green-600: #52b87a;
- --green-700: #91d4a8;
- --green-800: #c3e6cd;
- --green-900: #ecf4ee;
- --green-950: #f1fdf6;
- --blue-50: #033464;
- --blue-100: #064787;
- --blue-200: #0b5cad;
- --blue-300: #1068bf;
- --blue-400: #1f75cb;
- --blue-500: #428fdc;
- --blue-600: #63a6e9;
- --blue-700: #9dc7f1;
- --blue-800: #cbe2f9;
- --blue-900: #e9f3fc;
- --blue-950: #f2f9ff;
- --orange-50: #5c2900;
- --orange-100: #703800;
- --orange-200: #8f4700;
- --orange-300: #9e5400;
- --orange-400: #ab6100;
- --orange-500: #c17d10;
- --orange-600: #d99530;
- --orange-700: #e9be74;
- --orange-800: #f5d9a8;
- --orange-900: #fdf1dd;
- --orange-950: #fff4e1;
- --red-50: #660e00;
- --red-100: #8d1300;
- --red-200: #ae1800;
- --red-300: #c91c00;
- --red-400: #dd2b0e;
- --red-500: #ec5941;
- --red-600: #f57f6c;
- --red-700: #fcb5aa;
- --red-800: #fdd4cd;
- --red-900: #fcf1ef;
- --red-950: #fff4f3;
- --indigo-50: #1a1a40;
- --indigo-100: #292961;
- --indigo-200: #393982;
- --indigo-300: #4b4ba3;
- --indigo-400: #5b5bbd;
- --indigo-500: #6666c4;
- --indigo-600: #7c7ccc;
- --indigo-700: #a6a6de;
- --indigo-800: #d1d1f0;
- --indigo-900: #ebebfa;
- --indigo-950: #f7f7ff;
- --purple-50: #232150;
- --purple-100: #2f2a6b;
- --purple-200: #453894;
- --purple-300: #5943b6;
- --purple-400: #694cc0;
- --purple-500: #7b58cf;
- --purple-600: #9475db;
- --purple-700: #ac93e6;
- --purple-800: #cbbbf2;
- --purple-900: #e1d8f9;
- --purple-950: #f4f0ff;
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
- --gl-text-color: #ececef;
- --border-color: #4f4f4f;
- --white: #333;
- --black: #fff;
- --gray-light: #333238;
- --svg-status-bg: #333;
-}
.tab-width-8 {
tab-size: 8;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 7fb373bb6f4..f24b6fb9e81 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -17,9 +17,10 @@ header {
}
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -47,8 +48,9 @@ a:not([href]):not([class]) {
text-decoration: none;
}
kbd {
- font-family: "Menlo", "DejaVu Sans Mono", "Liberation Mono", "Consolas",
- "Ubuntu Mono", "Courier New", "andale mono", "lucida console", monospace;
+ font-family: var(--default-mono-font, "Menlo"), "DejaVu Sans Mono",
+ "Liberation Mono", "Consolas", "Ubuntu Mono", "Courier New", "andale mono",
+ "lucida console", monospace;
font-size: 1em;
}
img {
@@ -426,9 +428,10 @@ a.gl-badge.badge-warning:active {
.gl-form-input,
.gl-form-input.form-control {
background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 0.875rem;
line-height: 1rem;
padding-top: 0.5rem;
@@ -507,6 +510,15 @@ a.gl-badge.badge-warning:active {
font-size: 0.875rem;
border-radius: 0.25rem;
}
+.gl-button.gl-button .gl-button-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
.gl-button.gl-button.btn-default {
background-color: #fff;
}
@@ -606,8 +618,8 @@ html {
}
.btn:active,
.btn.active {
- background-color: #eaeaea;
- border-color: #e3e3e3;
+ background-color: #e6e6ea;
+ border-color: #dedee3;
color: #333238;
}
.btn svg {
@@ -685,7 +697,7 @@ html {
list-style: none;
}
.dropdown-menu li > a,
-.dropdown-menu li button {
+.dropdown-menu li > button {
background: transparent;
border: 0;
border-radius: 0;
@@ -702,14 +714,14 @@ html {
width: 100%;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
+.dropdown-menu li > button:active {
background-color: #ececef;
color: #333238;
outline: 0;
text-decoration: none;
}
.dropdown-menu li > a:active,
-.dropdown-menu li button:active {
+.dropdown-menu li > button:active {
box-shadow: inset 0 0 0 2px #428fdc, inset 0 0 0 3px #fff,
inset 0 0 0 1px #fff;
outline: none;
@@ -758,10 +770,10 @@ input {
kbd {
display: inline-block;
padding: 3px 5px;
- font-size: 0.6875rem;
+ font-size: 0.75rem;
line-height: 10px;
color: var(--gray-700, #535158);
- vertical-align: middle;
+ vertical-align: unset;
background-color: var(--gray-10, #fbfafd);
border-width: 1px;
border-style: solid;
@@ -821,6 +833,22 @@ kbd {
.navbar-gitlab .header-content .navbar-collapse > ul.nav > li:not(.d-none) {
margin: 0 2px;
}
+.navbar-gitlab .header-content .header-search-new {
+ max-width: 640px;
+}
+.navbar-gitlab .header-search {
+ min-width: 320px;
+}
+@media (min-width: 768px) and (max-width: 1199.98px) {
+ .navbar-gitlab .header-search {
+ min-width: 200px;
+ }
+}
+.navbar-gitlab .header-search .keyboard-shortcut-helper {
+ transform: translateY(calc(50% - 2px));
+ box-shadow: none;
+ border-color: transparent;
+}
.navbar-gitlab .navbar-collapse {
flex: 0 0 auto;
border-top: 0;
@@ -1123,7 +1151,7 @@ kbd {
font-weight: 600;
}
.nav-sidebar li.active:not(.fly-out-top-item) > a:not(.has-sub-items) {
- background-color: rgba(0, 0, 0, 0.08);
+ background-color: rgba(31, 30, 36, 0.08);
}
.nav-sidebar ul {
padding-left: 0;
@@ -1504,87 +1532,6 @@ svg.s12 {
svg.s16 {
vertical-align: -3px;
}
-.header-content .header-search-new {
- max-width: 640px;
-}
-.header-search {
- min-width: 320px;
-}
-@media (min-width: 768px) and (max-width: 1199.98px) {
- .header-search {
- min-width: 200px;
- }
-}
-.header-search .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
-}
-.search {
- margin: 0 8px;
-}
-.search form {
- display: block;
- margin: 0;
- padding: 4px;
- width: 200px;
- line-height: 24px;
- height: 32px;
- border: 0;
- border-radius: 4px;
-}
-@media (min-width: 1200px) {
- .search form {
- width: 320px;
- }
-}
-.search .search-input {
- border: 0;
- font-size: 14px;
- padding: 0 20px 0 0;
- margin-left: 5px;
- line-height: 25px;
- width: 98%;
- color: #fff;
- background: none;
-}
-.search .search-input-container {
- display: flex;
- position: relative;
-}
-.search .search-input-wrap {
- width: 100%;
-}
-.search .search-input-wrap .search-icon,
-.search .search-input-wrap .clear-icon {
- position: absolute;
- right: 5px;
- top: 4px;
-}
-.search .search-input-wrap .search-icon {
- user-select: none;
-}
-.search .search-input-wrap .clear-icon {
- display: none;
-}
-.search .search-input-wrap .dropdown {
- position: static;
-}
-.search .search-input-wrap .dropdown-menu {
- left: -5px;
- max-height: 400px;
- overflow: auto;
-}
-@media (min-width: 1200px) {
- .search .search-input-wrap .dropdown-menu {
- width: 320px;
- }
-}
-.search .identicon {
- flex-basis: 16px;
- flex-shrink: 0;
- margin-right: 4px;
-}
.avatar,
.avatar-container {
float: left;
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 7ae158b3930..d8afff1a200 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -16,9 +16,10 @@ header {
}
body {
margin: 0;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
@@ -390,9 +391,10 @@ input.btn-block[type="button"] {
.gl-form-input,
.gl-form-input.form-control {
background-color: #fff;
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
- "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue", sans-serif,
- "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
+ font-family: var(--default-regular-font, -apple-system), BlinkMacSystemFont,
+ "Segoe UI", Roboto, "Noto Sans", Ubuntu, Cantarell, "Helvetica Neue",
+ sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol",
+ "Noto Color Emoji";
font-size: 0.875rem;
line-height: 1rem;
padding-top: 0.5rem;
@@ -647,8 +649,8 @@ body.navless {
}
.btn:active,
.btn.active {
- background-color: #eaeaea;
- border-color: #e3e3e3;
+ background-color: #e6e6ea;
+ border-color: #dedee3;
color: #333238;
}
.btn svg {
@@ -954,7 +956,7 @@ svg {
}
.devise-layout-html body .footer-container,
.devise-layout-html body hr.footer-fixed {
- position: absolute;
+ position: fixed;
bottom: 0;
left: 0;
right: 0;
@@ -988,6 +990,9 @@ svg {
.gl-justify-content-center {
justify-content: center;
}
+.gl-justify-content-space-between {
+ justify-content: space-between;
+}
.gl-float-right {
float: right;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index a3474d2ed50..8db91fd9908 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -11,6 +11,20 @@ $gray-800: #dcdcde;
$gray-900: #ececef;
$gray-950: #fbfafd;
+$gray-lightest: lighten($gray-10, 1);
+$gray-light: lighten($gray-10, 2);
+$gray-lighter: darken($gray-50, 4);
+$gray-normal: $gray-50;
+$gray-dark: darken($gray-100, 2);
+$gray-darker: darken($gray-200, 2);
+$gray-darkest: $gray-700;
+
+$black: #fff;
+$black-normal: $gray-900;
+$white: $gray-50;
+$white-normal: $gray-50;
+$white-dark: $gray-100;
+
$green-50: #0a4020;
$green-100: #0d532a;
$green-200: #24663b;
@@ -83,159 +97,21 @@ $purple-800: #cbbbf2;
$purple-900: #e1d8f9;
$purple-950: #f4f0ff;
-$gray-lightest: #222;
-$gray-light: $gray-50;
-$gray-lighter: #303030;
-$gray-normal: #333;
-$gray-dark: $gray-100;
-$gray-darker: #4f4f4f;
-$gray-darkest: #c4c4c4;
-
-$black: #fff;
-$black-normal: $gray-900;
-$white: #333;
-$white-light: #2b2b2b;
-$white-normal: #333;
-$white-dark: #444;
-
$theme-indigo-50: #1a1a40;
$border-color: #4f4f4f;
-:root {
- color-scheme: dark;
-}
-
-body.gl-dark {
- --gray-10: #{$gray-10};
- --gray-50: #{$gray-50};
- --gray-100: #{$gray-100};
- --gray-200: #{$gray-200};
- --gray-300: #{$gray-300};
- --gray-400: #{$gray-400};
- --gray-500: #{$gray-500};
- --gray-600: #{$gray-600};
- --gray-700: #{$gray-700};
- --gray-800: #{$gray-800};
- --gray-900: #{$gray-900};
- --gray-950: #{$gray-950};
-
- --green-50: #{$green-50};
- --green-100: #{$green-100};
- --green-200: #{$green-200};
- --green-300: #{$green-300};
- --green-400: #{$green-400};
- --green-500: #{$green-500};
- --green-600: #{$green-600};
- --green-700: #{$green-700};
- --green-800: #{$green-800};
- --green-900: #{$green-900};
- --green-950: #{$green-950};
-
- --blue-50: #{$blue-50};
- --blue-100: #{$blue-100};
- --blue-200: #{$blue-200};
- --blue-300: #{$blue-300};
- --blue-400: #{$blue-400};
- --blue-500: #{$blue-500};
- --blue-600: #{$blue-600};
- --blue-700: #{$blue-700};
- --blue-800: #{$blue-800};
- --blue-900: #{$blue-900};
- --blue-950: #{$blue-950};
-
- --orange-50: #{$orange-50};
- --orange-100: #{$orange-100};
- --orange-200: #{$orange-200};
- --orange-300: #{$orange-300};
- --orange-400: #{$orange-400};
- --orange-500: #{$orange-500};
- --orange-600: #{$orange-600};
- --orange-700: #{$orange-700};
- --orange-800: #{$orange-800};
- --orange-900: #{$orange-900};
- --orange-950: #{$orange-950};
-
- --red-50: #{$red-50};
- --red-100: #{$red-100};
- --red-200: #{$red-200};
- --red-300: #{$red-300};
- --red-400: #{$red-400};
- --red-500: #{$red-500};
- --red-600: #{$red-600};
- --red-700: #{$red-700};
- --red-800: #{$red-800};
- --red-900: #{$red-900};
- --red-950: #{$red-950};
-
- --indigo-50: #{$indigo-50};
- --indigo-100: #{$indigo-100};
- --indigo-200: #{$indigo-200};
- --indigo-300: #{$indigo-300};
- --indigo-400: #{$indigo-400};
- --indigo-500: #{$indigo-500};
- --indigo-600: #{$indigo-600};
- --indigo-700: #{$indigo-700};
- --indigo-800: #{$indigo-800};
- --indigo-900: #{$indigo-900};
- --indigo-950: #{$indigo-950};
-
- --purple-50: #{$purple-50};
- --purple-100: #{$purple-100};
- --purple-200: #{$purple-200};
- --purple-300: #{$purple-300};
- --purple-400: #{$purple-400};
- --purple-500: #{$purple-500};
- --purple-600: #{$purple-600};
- --purple-700: #{$purple-700};
- --purple-800: #{$purple-800};
- --purple-900: #{$purple-900};
- --purple-950: #{$purple-950};
-
- --dark-icon-color-purple-1: #524a68;
- --dark-icon-color-purple-2: #715bae;
- --dark-icon-color-purple-3: #9a79f7;
- --dark-icon-color-orange-1: #665349;
- --dark-icon-color-orange-2: #b37a5d;
-
- --gl-text-color: #{$gray-900};
- --border-color: #{$border-color};
-
- --white: #{$white};
- --black: #{$black};
- --gray-light: #{$gray-50};
-
- --svg-status-bg: #{$white};
-
- .gl-button.gl-button,
- .gl-button.gl-button.btn-block {
- &.btn-default,
- &.btn-dashed,
- &.btn-info,
- &.btn-success,
- &.btn-danger,
- &.btn-confirm {
- &-tertiary {
- mix-blend-mode: screen;
- }
- }
- }
-
- .gl-datepicker-theme {
- .pika-prev,
- .pika-next {
- filter: invert(0.9);
- }
-
- .is-selected > .pika-button {
- color: $gray-900;
- }
-
- :not(.is-selected) > .pika-button:hover {
- background-color: $gray-200;
- }
- }
-}
+$data-viz-blue-50: #2a2b59;
+$data-viz-blue-100: #303470;
+$data-viz-blue-200: #374291;
+$data-viz-blue-300: #3f51ae;
+$data-viz-blue-400: #4e65cd;
+$data-viz-blue-500: #617ae2;
+$data-viz-blue-600: #7992f5;
+$data-viz-blue-700: #97acff;
+$data-viz-blue-800: #b7c6ff;
+$data-viz-blue-900: #d2dcff;
+$data-viz-blue-950: #e9ebff;
$border-white-normal: $border-color;
@@ -265,11 +141,3 @@ $line-removed-dark: $red-200;
$well-expand-item: $gray-200;
$well-inner-border: $gray-200;
-
-$calendar-activity-colors: (
- #404040,
- #1e23a8,
- #445cf2,
- #97acff,
- #e9ebff
-);
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index a0d19c3de2a..bb97261a1ca 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -2,6 +2,149 @@
@import 'page_bundles/mixins_and_variables_and_functions';
@import './themes/theme_helper';
+:root {
+ color-scheme: dark;
+ --gray-10: #{$gray-10};
+ --gray-50: #{$gray-50};
+ --gray-100: #{$gray-100};
+ --gray-200: #{$gray-200};
+ --gray-300: #{$gray-300};
+ --gray-400: #{$gray-400};
+ --gray-500: #{$gray-500};
+ --gray-600: #{$gray-600};
+ --gray-700: #{$gray-700};
+ --gray-800: #{$gray-800};
+ --gray-900: #{$gray-900};
+ --gray-950: #{$gray-950};
+
+ --green-50: #{$green-50};
+ --green-100: #{$green-100};
+ --green-200: #{$green-200};
+ --green-300: #{$green-300};
+ --green-400: #{$green-400};
+ --green-500: #{$green-500};
+ --green-600: #{$green-600};
+ --green-700: #{$green-700};
+ --green-800: #{$green-800};
+ --green-900: #{$green-900};
+ --green-950: #{$green-950};
+
+ --blue-50: #{$blue-50};
+ --blue-100: #{$blue-100};
+ --blue-200: #{$blue-200};
+ --blue-300: #{$blue-300};
+ --blue-400: #{$blue-400};
+ --blue-500: #{$blue-500};
+ --blue-600: #{$blue-600};
+ --blue-700: #{$blue-700};
+ --blue-800: #{$blue-800};
+ --blue-900: #{$blue-900};
+ --blue-950: #{$blue-950};
+
+ --orange-50: #{$orange-50};
+ --orange-100: #{$orange-100};
+ --orange-200: #{$orange-200};
+ --orange-300: #{$orange-300};
+ --orange-400: #{$orange-400};
+ --orange-500: #{$orange-500};
+ --orange-600: #{$orange-600};
+ --orange-700: #{$orange-700};
+ --orange-800: #{$orange-800};
+ --orange-900: #{$orange-900};
+ --orange-950: #{$orange-950};
+
+ --red-50: #{$red-50};
+ --red-100: #{$red-100};
+ --red-200: #{$red-200};
+ --red-300: #{$red-300};
+ --red-400: #{$red-400};
+ --red-500: #{$red-500};
+ --red-600: #{$red-600};
+ --red-700: #{$red-700};
+ --red-800: #{$red-800};
+ --red-900: #{$red-900};
+ --red-950: #{$red-950};
+
+ --indigo-50: #{$indigo-50};
+ --indigo-100: #{$indigo-100};
+ --indigo-200: #{$indigo-200};
+ --indigo-300: #{$indigo-300};
+ --indigo-400: #{$indigo-400};
+ --indigo-500: #{$indigo-500};
+ --indigo-600: #{$indigo-600};
+ --indigo-700: #{$indigo-700};
+ --indigo-800: #{$indigo-800};
+ --indigo-900: #{$indigo-900};
+ --indigo-950: #{$indigo-950};
+
+ --purple-50: #{$purple-50};
+ --purple-100: #{$purple-100};
+ --purple-200: #{$purple-200};
+ --purple-300: #{$purple-300};
+ --purple-400: #{$purple-400};
+ --purple-500: #{$purple-500};
+ --purple-600: #{$purple-600};
+ --purple-700: #{$purple-700};
+ --purple-800: #{$purple-800};
+ --purple-900: #{$purple-900};
+ --purple-950: #{$purple-950};
+
+ --dark-icon-color-purple-1: #524a68;
+ --dark-icon-color-purple-2: #715bae;
+ --dark-icon-color-purple-3: #9a79f7;
+ --dark-icon-color-orange-1: #665349;
+ --dark-icon-color-orange-2: #b37a5d;
+
+ --gl-text-color: #{$gray-900};
+ --border-color: #{$border-color};
+
+ --white: #{$white};
+ --black: #{$black};
+ --gray-light: #{$gray-50};
+
+ --svg-status-bg: #{$white};
+}
+
+body.gl-dark {
+ // redefine some colors and values to prevent sourcegraph conflicts
+ color-scheme: dark;
+ --gray-10: #{$gray-10};
+ --border-color: #{$border-color};
+ --white: #{$white};
+ --black: #{$black};
+}
+
+.gl-dark {
+ .gl-button.gl-button,
+ .gl-button.gl-button.btn-block {
+ &.btn-default,
+ &.btn-dashed,
+ &.btn-info,
+ &.btn-success,
+ &.btn-danger,
+ &.btn-confirm {
+ &-tertiary {
+ mix-blend-mode: screen;
+ }
+ }
+ }
+
+ .gl-datepicker-theme {
+ .pika-prev,
+ .pika-next {
+ filter: invert(0.9);
+ }
+
+ .is-selected > .pika-button {
+ color: $gray-900;
+ }
+
+ :not(.is-selected) > .pika-button:hover {
+ background-color: $gray-200;
+ }
+ }
+}
+
// Some hacks and overrides for things that don't properly support dark mode
.gl-label {
filter: brightness(0.9) contrast(1.1);
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 4be4fc82d04..714dd932147 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -236,6 +236,13 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
}
}
+// TODO: Remove once https: //gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3198 is merged
+.gl-sm-ml-5 {
+ @include gl-media-breakpoint-up(sm) {
+ @include gl-ml-5;
+ }
+}
+
/* End gitlab-ui#1709 */
/*
@@ -251,3 +258,8 @@ to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1709
.gl-flex-flow-row-wrap {
flex-flow: row wrap;
}
+
+// Will be moved to @gitlab/ui by https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2098
+.gl-max-w-0 {
+ max-width: 0;
+}
diff --git a/app/channels/graphql_channel.rb b/app/channels/graphql_channel.rb
index d364cc2b64b..ae37be85da8 100644
--- a/app/channels/graphql_channel.rb
+++ b/app/channels/graphql_channel.rb
@@ -25,9 +25,7 @@ class GraphqlChannel < ApplicationCable::Channel # rubocop:disable Gitlab/Namesp
# Track the subscription here so we can remove it
# on unsubscribe.
- if result.context[:subscription_id]
- @subscription_ids << result.context[:subscription_id]
- end
+ @subscription_ids << result.context[:subscription_id] if result.context[:subscription_id]
transmit(payload)
end
diff --git a/app/components/diffs/stats_component.rb b/app/components/diffs/stats_component.rb
index 74788133aa2..407c3ca4e58 100644
--- a/app/components/diffs/stats_component.rb
+++ b/app/components/diffs/stats_component.rb
@@ -14,14 +14,14 @@ module Diffs
def diff_files_data
diffs_map = @diff_files.map do |f|
{
- href: "##{helpers.hexdigest(f.file_path)}",
- title: f.new_path,
- name: f.file_path,
- path: diff_file_path_text(f),
- icon: diff_file_changed_icon(f),
- iconColor: "#{diff_file_changed_icon_color(f)}",
- added: f.added_lines,
- removed: f.removed_lines
+ href: "##{helpers.hexdigest(f.file_path)}",
+ title: f.new_path,
+ name: f.file_path,
+ path: diff_file_path_text(f),
+ icon: diff_file_changed_icon(f),
+ iconColor: diff_file_changed_icon_color(f).to_s,
+ added: f.added_lines,
+ removed: f.removed_lines
}
end
diff --git a/app/components/pajamas/button_component.rb b/app/components/pajamas/button_component.rb
index b2dd798b718..cdfd201bfb8 100644
--- a/app/components/pajamas/button_component.rb
+++ b/app/components/pajamas/button_component.rb
@@ -65,7 +65,7 @@ module Pajamas
classes.push(VARIANT_CLASSES[@variant])
unless NON_CATEGORY_VARIANTS.include?(@variant) || @category == :primary
- classes.push(VARIANT_CLASSES[@variant] + '-' + CATEGORY_CLASSES[@category])
+ classes.push("#{VARIANT_CLASSES[@variant]}-#{CATEGORY_CLASSES[@category]}")
end
classes.push(@button_options[:class])
diff --git a/app/controllers/abuse_reports_controller.rb b/app/controllers/abuse_reports_controller.rb
index 0de2115d4d6..80aca7e21ce 100644
--- a/app/controllers/abuse_reports_controller.rb
+++ b/app/controllers/abuse_reports_controller.rb
@@ -3,7 +3,7 @@
class AbuseReportsController < ApplicationController
before_action :set_user, only: [:new]
- feature_category :users
+ feature_category :insider_threat
def new
@abuse_report = AbuseReport.new
diff --git a/app/controllers/admin/abuse_reports_controller.rb b/app/controllers/admin/abuse_reports_controller.rb
index 6f80ed3c172..5357558434e 100644
--- a/app/controllers/admin/abuse_reports_controller.rb
+++ b/app/controllers/admin/abuse_reports_controller.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::AbuseReportsController < Admin::ApplicationController
- feature_category :users
+ feature_category :insider_threat
def index
@abuse_reports = AbuseReportsFinder.new(params).execute
diff --git a/app/controllers/admin/application_settings/appearances_controller.rb b/app/controllers/admin/application_settings/appearances_controller.rb
index cf765c96a8f..1a8447185a7 100644
--- a/app/controllers/admin/application_settings/appearances_controller.rb
+++ b/app/controllers/admin/application_settings/appearances_controller.rb
@@ -68,6 +68,7 @@ class Admin::ApplicationSettings::AppearancesController < Admin::ApplicationCont
def allowed_appearance_params
%i[
title
+ short_title
description
logo
logo_cache
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index ec9441c2b9b..b8c1bc266f7 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -40,9 +40,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
feature_category :pages, [:lets_encrypt_terms_of_service]
feature_category :error_tracking, [:reset_error_tracking_access_token]
- VALID_SETTING_PANELS = %w(general repository
+ VALID_SETTING_PANELS = %w[general repository
ci_cd reporting metrics_and_profiling
- network preferences).freeze
+ network preferences].freeze
# The current size of a sidekiq job's jid is 24 characters. The size of the
# jid is an internal detail of Sidekiq, and they do not guarantee that it'll
@@ -150,9 +150,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
}
end
- if @application_setting.self_monitoring_project_id.present?
- return render status: :ok, json: self_monitoring_data
- end
+ return render status: :ok, json: self_monitoring_data if @application_setting.self_monitoring_project_id.present?
render status: :bad_request, json: {
message: _('Self-monitoring project does not exist. Please check logs ' \
@@ -236,7 +234,9 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params[:application_setting][:restricted_visibility_levels]&.delete("")
if params[:application_setting].key?(:required_instance_ci_template)
- params[:application_setting][:required_instance_ci_template] = nil if params[:application_setting][:required_instance_ci_template].empty?
+ if params[:application_setting][:required_instance_ci_template].empty?
+ params[:application_setting][:required_instance_ci_template] = nil
+ end
end
remove_blank_params_for!(:elasticsearch_aws_secret_access_key, :eks_secret_access_key)
@@ -290,9 +290,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
.new(@application_setting, current_user, application_setting_params)
.execute
- if recheck_user_consent?
- session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
- end
+ session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent? if recheck_user_consent?
redirect_path = referer_path(request) || general_admin_application_settings_path
diff --git a/app/controllers/admin/background_jobs_controller.rb b/app/controllers/admin/background_jobs_controller.rb
index 4eda35d66f6..43d2c983823 100644
--- a/app/controllers/admin/background_jobs_controller.rb
+++ b/app/controllers/admin/background_jobs_controller.rb
@@ -1,5 +1,7 @@
# frozen_string_literal: true
-class Admin::BackgroundJobsController < Admin::ApplicationController
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
+module Admin
+ class BackgroundJobsController < ApplicationController
+ feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
+ end
end
diff --git a/app/controllers/admin/background_migrations_controller.rb b/app/controllers/admin/background_migrations_controller.rb
index c6c9e0ced22..b904196c5ab 100644
--- a/app/controllers/admin/background_migrations_controller.rb
+++ b/app/controllers/admin/background_migrations_controller.rb
@@ -1,66 +1,68 @@
# frozen_string_literal: true
-class Admin::BackgroundMigrationsController < Admin::ApplicationController
- feature_category :database
- urgency :low
-
- around_action :support_multiple_databases
-
- def index
- @relations_by_tab = {
- 'queued' => batched_migration_class.queued.queue_order,
- 'failed' => batched_migration_class.with_status(:failed).queue_order,
- 'finished' => batched_migration_class.with_status(:finished).queue_order.reverse_order
- }
-
- @current_tab = @relations_by_tab.key?(params[:tab]) ? params[:tab] : 'queued'
- @migrations = @relations_by_tab[@current_tab].page(params[:page])
- @successful_rows_counts = batched_migration_class.successful_rows_counts(@migrations.map(&:id))
- @databases = Gitlab::Database.db_config_names
- end
+module Admin
+ class BackgroundMigrationsController < ApplicationController
+ feature_category :database
+ urgency :low
+
+ around_action :support_multiple_databases
+
+ def index
+ @relations_by_tab = {
+ 'queued' => batched_migration_class.queued.queue_order,
+ 'failed' => batched_migration_class.with_status(:failed).queue_order,
+ 'finished' => batched_migration_class.with_status(:finished).queue_order.reverse_order
+ }
+
+ @current_tab = @relations_by_tab.key?(params[:tab]) ? params[:tab] : 'queued'
+ @migrations = @relations_by_tab[@current_tab].page(params[:page])
+ @successful_rows_counts = batched_migration_class.successful_rows_counts(@migrations.map(&:id))
+ @databases = Gitlab::Database.db_config_names
+ end
- def show
- @migration = batched_migration_class.find(params[:id])
+ def show
+ @migration = batched_migration_class.find(params[:id])
- @failed_jobs = @migration.batched_jobs.with_status(:failed).page(params[:page])
- end
+ @failed_jobs = @migration.batched_jobs.with_status(:failed).page(params[:page])
+ end
- def pause
- migration = batched_migration_class.find(params[:id])
- migration.pause!
+ def pause
+ migration = batched_migration_class.find(params[:id])
+ migration.pause!
- redirect_back fallback_location: { action: 'index' }
- end
+ redirect_back fallback_location: { action: 'index' }
+ end
- def resume
- migration = batched_migration_class.find(params[:id])
- migration.execute!
+ def resume
+ migration = batched_migration_class.find(params[:id])
+ migration.execute!
- redirect_back fallback_location: { action: 'index' }
- end
+ redirect_back fallback_location: { action: 'index' }
+ end
- def retry
- migration = batched_migration_class.find(params[:id])
- migration.retry_failed_jobs! if migration.failed?
+ def retry
+ migration = batched_migration_class.find(params[:id])
+ migration.retry_failed_jobs! if migration.failed?
- redirect_back fallback_location: { action: 'index' }
- end
+ redirect_back fallback_location: { action: 'index' }
+ end
- private
+ private
- def support_multiple_databases
- Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- yield
+ def support_multiple_databases
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ yield
+ end
end
- end
- def base_model
- @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+ def base_model
+ @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
- Gitlab::Database.database_base_models[@selected_database]
- end
+ Gitlab::Database.database_base_models[@selected_database]
+ end
- def batched_migration_class
- @batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration
+ def batched_migration_class
+ @batched_migration_class ||= Gitlab::Database::BackgroundMigration::BatchedMigration
+ end
end
end
diff --git a/app/controllers/admin/batched_jobs_controller.rb b/app/controllers/admin/batched_jobs_controller.rb
index 0a00ba13dc8..10b5f68d630 100644
--- a/app/controllers/admin/batched_jobs_controller.rb
+++ b/app/controllers/admin/batched_jobs_controller.rb
@@ -1,28 +1,30 @@
# frozen_string_literal: true
-class Admin::BatchedJobsController < Admin::ApplicationController
- feature_category :database
- urgency :low
+module Admin
+ class BatchedJobsController < ApplicationController
+ feature_category :database
+ urgency :low
- around_action :support_multiple_databases
+ around_action :support_multiple_databases
- def show
- @job = Gitlab::Database::BackgroundMigration::BatchedJob.find(params[:id])
+ def show
+ @job = Gitlab::Database::BackgroundMigration::BatchedJob.find(params[:id])
- @transition_logs = @job.batched_job_transition_logs
- end
+ @transition_logs = @job.batched_job_transition_logs
+ end
- private
+ private
- def support_multiple_databases
- Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- yield
+ def support_multiple_databases
+ Gitlab::Database::SharedModel.using_connection(base_model.connection) do
+ yield
+ end
end
- end
- def base_model
- @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
+ def base_model
+ @selected_database = params[:database] || Gitlab::Database::MAIN_DATABASE_NAME
- Gitlab::Database.database_base_models[@selected_database]
+ Gitlab::Database.database_base_models[@selected_database]
+ end
end
end
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index bdf0c6aedb9..093c5667a24 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -1,104 +1,106 @@
# frozen_string_literal: true
-class Admin::BroadcastMessagesController < Admin::ApplicationController
- include BroadcastMessagesHelper
+module Admin
+ class BroadcastMessagesController < ApplicationController
+ include BroadcastMessagesHelper
- before_action :find_broadcast_message, only: [:edit, :update, :destroy]
- before_action :find_broadcast_messages, only: [:index, :create]
- before_action :push_features, only: [:index, :edit]
+ before_action :find_broadcast_message, only: [:edit, :update, :destroy]
+ before_action :find_broadcast_messages, only: [:index, :create]
+ before_action :push_features, only: [:index, :edit]
- feature_category :onboarding
- urgency :low
+ feature_category :onboarding
+ urgency :low
- def index
- @broadcast_message = BroadcastMessage.new
- end
-
- def edit
- end
+ def index
+ @broadcast_message = BroadcastMessage.new
+ end
- def create
- @broadcast_message = BroadcastMessage.new(broadcast_message_params)
- success = @broadcast_message.save
+ def edit
+ end
- respond_to do |format|
- format.json do
- if success
- render json: @broadcast_message, status: :ok
- else
- render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
+ def create
+ @broadcast_message = BroadcastMessage.new(broadcast_message_params)
+ success = @broadcast_message.save
+
+ respond_to do |format|
+ format.json do
+ if success
+ render json: @broadcast_message, status: :ok
+ else
+ render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
+ end
end
- end
- format.html do
- if success
- redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully created.')
- else
- render :index
+ format.html do
+ if success
+ redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully created.')
+ else
+ render :index
+ end
end
end
end
- end
- def update
- success = @broadcast_message.update(broadcast_message_params)
+ def update
+ success = @broadcast_message.update(broadcast_message_params)
- respond_to do |format|
- format.json do
- if success
- render json: @broadcast_message, status: :ok
- else
- render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
+ respond_to do |format|
+ format.json do
+ if success
+ render json: @broadcast_message, status: :ok
+ else
+ render json: { errors: @broadcast_message.errors.full_messages }, status: :bad_request
+ end
end
- end
- format.html do
- if success
- redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully updated.')
- else
- render :edit
+ format.html do
+ if success
+ redirect_to admin_broadcast_messages_path, notice: _('Broadcast Message was successfully updated.')
+ else
+ render :edit
+ end
end
end
end
- end
- def destroy
- @broadcast_message.destroy
+ def destroy
+ @broadcast_message.destroy
- respond_to do |format|
- format.html { redirect_back_or_default(default: { action: 'index' }) }
- format.js { head :ok }
+ respond_to do |format|
+ format.html { redirect_back_or_default(default: { action: 'index' }) }
+ format.js { head :ok }
+ end
end
- end
- def preview
- @broadcast_message = BroadcastMessage.new(broadcast_message_params)
- render partial: 'admin/broadcast_messages/preview'
- end
+ def preview
+ @broadcast_message = BroadcastMessage.new(broadcast_message_params)
+ render partial: 'admin/broadcast_messages/preview'
+ end
- protected
+ protected
- def find_broadcast_message
- @broadcast_message = BroadcastMessage.find(params[:id])
- end
+ def find_broadcast_message
+ @broadcast_message = BroadcastMessage.find(params[:id])
+ end
- def find_broadcast_messages
- @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord
- end
+ def find_broadcast_messages
+ @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page]) # rubocop: disable CodeReuse/ActiveRecord
+ end
- def broadcast_message_params
- params.require(:broadcast_message)
- .permit(%i(
- theme
- ends_at
- message
- starts_at
- target_path
- broadcast_type
- dismissable
- ), target_access_levels: []).reverse_merge!(target_access_levels: [])
- end
+ def broadcast_message_params
+ params.require(:broadcast_message)
+ .permit(%i[
+ theme
+ ends_at
+ message
+ starts_at
+ target_path
+ broadcast_type
+ dismissable
+ ], target_access_levels: []).reverse_merge!(target_access_levels: [])
+ end
- def push_features
- push_frontend_feature_flag(:vue_broadcast_messages, current_user)
- push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
+ def push_features
+ push_frontend_feature_flag(:vue_broadcast_messages, current_user)
+ push_frontend_feature_flag(:role_targeted_broadcast_messages, current_user)
+ end
end
end
diff --git a/app/controllers/admin/ci/variables_controller.rb b/app/controllers/admin/ci/variables_controller.rb
index 7d643435ddb..ef50d7362c4 100644
--- a/app/controllers/admin/ci/variables_controller.rb
+++ b/app/controllers/admin/ci/variables_controller.rb
@@ -1,50 +1,54 @@
# frozen_string_literal: true
-class Admin::Ci::VariablesController < Admin::ApplicationController
- feature_category :pipeline_authoring
-
- def show
- respond_to do |format|
- format.json { render_instance_variables }
- end
- end
-
- def update
- service = Ci::UpdateInstanceVariablesService.new(variables_params)
-
- if service.execute
- respond_to do |format|
- format.json { render_instance_variables }
+module Admin
+ module Ci
+ class VariablesController < ApplicationController
+ feature_category :pipeline_authoring
+
+ def show
+ respond_to do |format|
+ format.json { render_instance_variables }
+ end
end
- else
- respond_to do |format|
- format.json { render_error(service.errors) }
+
+ def update
+ service = ::Ci::UpdateInstanceVariablesService.new(variables_params)
+
+ if service.execute
+ respond_to do |format|
+ format.json { render_instance_variables }
+ end
+ else
+ respond_to do |format|
+ format.json { render_error(service.errors) }
+ end
+ end
end
- end
- end
- private
+ private
- def variables
- @variables ||= Ci::InstanceVariable.all
- end
+ def variables
+ @variables ||= ::Ci::InstanceVariable.all
+ end
- def render_instance_variables
- render status: :ok,
- json: {
- variables: Ci::InstanceVariableSerializer.new.represent(variables)
- }
- end
+ def render_instance_variables
+ render status: :ok,
+ json: {
+ variables: ::Ci::InstanceVariableSerializer.new.represent(variables)
+ }
+ end
- def render_error(errors)
- render status: :bad_request, json: errors
- end
+ def render_error(errors)
+ render status: :bad_request, json: errors
+ end
- def variables_params
- params.permit(variables_attributes: Array(variable_params_attributes))
- end
+ def variables_params
+ params.permit(variables_attributes: Array(variable_params_attributes))
+ end
- def variable_params_attributes
- %i[id variable_type key secret_value protected masked _destroy]
+ def variable_params_attributes
+ %i[id variable_type key secret_value protected masked raw _destroy]
+ end
+ end
end
end
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 1395d4bb3b7..8005babe19e 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -51,6 +51,10 @@ class Admin::GroupsController < Admin::ApplicationController
@group.build_admin_note unless @group.admin_note
if @group.update(group_params)
+ unless Gitlab::Utils.to_boolean(group_params['runner_registration_enabled'])
+ Ci::Runners::ResetRegistrationTokenService.new(@group, current_user).execute
+ end
+
redirect_to [:admin, @group], notice: _('Group was successfully updated.')
else
render "edit"
@@ -91,6 +95,7 @@ class Admin::GroupsController < Admin::ApplicationController
:name,
:path,
:request_access_enabled,
+ :runner_registration_enabled,
:visibility_level,
:require_two_factor_authentication,
:two_factor_grace_period,
diff --git a/app/controllers/admin/plan_limits_controller.rb b/app/controllers/admin/plan_limits_controller.rb
index 2cebc059830..ea52198432c 100644
--- a/app/controllers/admin/plan_limits_controller.rb
+++ b/app/controllers/admin/plan_limits_controller.rb
@@ -47,6 +47,7 @@ class Admin::PlanLimitsController < Admin::ApplicationController
ci_needs_size_limit
ci_registered_group_runners
ci_registered_project_runners
+ pipeline_hierarchy_size
])
end
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 3f3c3581555..9e841487508 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -13,9 +13,7 @@ class Admin::ProjectsController < Admin::ApplicationController
params[:sort] ||= 'latest_activity_desc'
@sort = params[:sort]
- if params[:last_repository_check_failed].present? && params[:archived].nil?
- params[:archived] = true
- end
+ params[:archived] = true if params[:last_repository_check_failed].present? && params[:archived].nil?
@projects = Admin::ProjectsFinder.new(params: params, current_user: current_user).execute
@@ -57,9 +55,7 @@ class Admin::ProjectsController < Admin::ApplicationController
namespace = Namespace.find_by(id: params[:new_namespace_id])
::Projects::TransferService.new(@project, current_user, params.dup).execute(namespace)
- if @project.errors[:new_namespace].present?
- flash[:alert] = @project.errors[:new_namespace].first
- end
+ flash[:alert] = @project.errors[:new_namespace].first if @project.errors[:new_namespace].present?
@project.reset
redirect_to admin_project_path(@project)
diff --git a/app/controllers/admin/spam_logs_controller.rb b/app/controllers/admin/spam_logs_controller.rb
index 3a55fc4b951..180f4634136 100644
--- a/app/controllers/admin/spam_logs_controller.rb
+++ b/app/controllers/admin/spam_logs_controller.rb
@@ -5,7 +5,7 @@ class Admin::SpamLogsController < Admin::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def index
- @spam_logs = SpamLog.order(id: :desc).page(params[:page])
+ @spam_logs = SpamLog.includes(:user).order(id: :desc).page(params[:page])
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index 41f95addc66..96fb73cedfe 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -59,11 +59,11 @@ class Admin::SystemInfoController < Admin::ApplicationController
begin
disk = Sys::Filesystem.stat(mount.mount_point)
@disks.push({
- bytes_total: disk.bytes_total,
- bytes_used: disk.bytes_used,
- disk_name: mount.name,
- mount_path: disk.path
- })
+ bytes_total: disk.bytes_total,
+ bytes_used: disk.bytes_used,
+ disk_name: mount.name,
+ mount_path: disk.path
+ })
rescue Sys::Filesystem::Error
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 2c8b4888d5d..5f6e3f0062f 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -88,17 +88,25 @@ class Admin::UsersController < Admin::ApplicationController
end
def activate
- return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated")) if user.blocked?
+ if user.blocked?
+ return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user must be unblocked to be activated"))
+ end
user.activate
redirect_back_or_admin_user(notice: _("Successfully activated"))
end
def deactivate
- return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated")) if user.blocked?
+ if user.blocked?
+ return redirect_back_or_admin_user(notice: _("Error occurred. A blocked user cannot be deactivated"))
+ end
+
return redirect_back_or_admin_user(notice: _("Successfully deactivated")) if user.deactivated?
return redirect_back_or_admin_user(notice: _("Internal users cannot be deactivated")) if user.internal?
- return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: Gitlab::CurrentSettings.deactivate_dormant_users_period }) unless user.can_be_deactivated?
+
+ unless user.can_be_deactivated?
+ return redirect_back_or_admin_user(notice: _("The user you are trying to deactivate has been active in the past %{minimum_inactive_days} days and cannot be deactivated") % { minimum_inactive_days: Gitlab::CurrentSettings.deactivate_dormant_users_period })
+ end
user.deactivate
redirect_back_or_admin_user(notice: _("Successfully deactivated"))
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 4de6b5de42a..e64d3110c3a 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -158,7 +158,7 @@ class ApplicationController < ActionController::Base
protected
def workhorse_excluded_content_types
- @workhorse_excluded_content_types ||= %w(text/html application/json)
+ @workhorse_excluded_content_types ||= %w[text/html application/json]
end
def append_info_to_payload(payload)
@@ -179,9 +179,7 @@ class ApplicationController < ActionController::Base
payload[:queue_duration_s] = request.env[::Gitlab::Middleware::RailsQueueDuration::GITLAB_RAILS_QUEUE_DURATION_KEY]
- if Feature.enabled?(:log_response_length)
- payload[:response_bytes] = response.body_parts.sum(&:bytesize)
- end
+ payload[:response_bytes] = response.body_parts.sum(&:bytesize) if Feature.enabled?(:log_response_length)
store_cloudflare_headers!(payload, request)
end
@@ -349,9 +347,7 @@ class ApplicationController < ActionController::Base
def check_password_expiration
return if session[:impersonator_id] || !current_user&.allow_password_authentication?
- if current_user&.password_expired?
- redirect_to new_profile_password_path
- end
+ redirect_to new_profile_password_path if current_user&.password_expired?
end
def active_user_check
@@ -426,8 +422,8 @@ class ApplicationController < ActionController::Base
# accepting the terms.
redirect_path = if request.get?
request.fullpath
- else
- URI(request.referer).path if request.referer
+ elsif request.referer
+ URI(request.referer).path
end
flash[:notice] = message
@@ -529,7 +525,7 @@ class ApplicationController < ActionController::Base
end
def set_page_title_header
- # Per https://tools.ietf.org/html/rfc5987, headers need to be ISO-8859-1, not UTF-8
+ # Per https://www.rfc-editor.org/rfc/rfc5987, headers need to be ISO-8859-1, not UTF-8
response.headers['Page-Title'] = Addressable::URI.encode_component(page_title('GitLab'))
end
@@ -565,9 +561,7 @@ class ApplicationController < ActionController::Base
session[:ask_for_usage_stats_consent] = current_user.requires_usage_stats_consent?
- if session[:ask_for_usage_stats_consent]
- disable_usage_stats
- end
+ disable_usage_stats if session[:ask_for_usage_stats_consent]
end
def disable_usage_stats
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index 817f82085e6..b4a36b7db22 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,6 +23,8 @@ module AuthenticatesWithTwoFactor
session[:otp_user_id] = user.id
session[:user_password_hash] = Digest::SHA256.hexdigest(user.encrypted_password)
+
+ add_gon_variables
push_frontend_feature_flag(:webauthn)
if Feature.enabled?(:webauthn)
diff --git a/app/controllers/concerns/controller_with_cross_project_access_check.rb b/app/controllers/concerns/controller_with_cross_project_access_check.rb
index 3f72f092683..eace8e9464b 100644
--- a/app/controllers/concerns/controller_with_cross_project_access_check.rb
+++ b/app/controllers/concerns/controller_with_cross_project_access_check.rb
@@ -9,9 +9,7 @@ module ControllerWithCrossProjectAccessCheck
end
def cross_project_check
- if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
- authorize_cross_project_page!
- end
+ authorize_cross_project_page! if Gitlab::CrossProjectAccess.find_check(self)&.should_run?(self)
end
def authorize_cross_project_page!
diff --git a/app/controllers/concerns/creates_commit.rb b/app/controllers/concerns/creates_commit.rb
index b6ba1b13cc3..53bb11090c8 100644
--- a/app/controllers/concerns/creates_commit.rb
+++ b/app/controllers/concerns/creates_commit.rb
@@ -78,7 +78,7 @@ module CreatesCommit
_("You can now submit a merge request to get this change into the original branch.")
end
- flash[:notice] += " " + mr_message
+ flash[:notice] += " #{mr_message}"
end
end
end
diff --git a/app/controllers/concerns/cycle_analytics_params.rb b/app/controllers/concerns/cycle_analytics_params.rb
index 70bcefe339c..5199d879595 100644
--- a/app/controllers/concerns/cycle_analytics_params.rb
+++ b/app/controllers/concerns/cycle_analytics_params.rb
@@ -23,7 +23,10 @@ module CycleAnalyticsParams
opts[:from] = params[:from] || start_date(params)
opts[:to] = params[:to] if params[:to]
opts[:end_event_filter] = params[:end_event_filter] if params[:end_event_filter]
- opts[:use_aggregated_data_collector] = params[:use_aggregated_data_collector] if params[:use_aggregated_data_collector]
+ if params[:use_aggregated_data_collector]
+ opts[:use_aggregated_data_collector] = params[:use_aggregated_data_collector]
+ end
+
opts.merge!(params.slice(*::Gitlab::Analytics::CycleAnalytics::RequestParams::FINDER_PARAM_NAMES))
opts.merge!(date_range(params))
end
diff --git a/app/controllers/concerns/enforces_two_factor_authentication.rb b/app/controllers/concerns/enforces_two_factor_authentication.rb
index b1b6e21644e..c8de041d5bd 100644
--- a/app/controllers/concerns/enforces_two_factor_authentication.rb
+++ b/app/controllers/concerns/enforces_two_factor_authentication.rb
@@ -10,19 +10,12 @@
module EnforcesTwoFactorAuthentication
extend ActiveSupport::Concern
- MFA_HELP_PAGE = Rails.application.routes.url_helpers.help_page_url(
- 'user/profile/account/two_factor_authentication.html',
- anchor: 'enable-two-factor-authentication'
- )
-
included do
before_action :check_two_factor_requirement, except: [:route_not_found]
# to include this in controllers inheriting from `ActionController::Metal`
# we need to add this block
- if respond_to?(:helper_method)
- helper_method :two_factor_grace_period_expired?, :two_factor_skippable?
- end
+ helper_method :two_factor_grace_period_expired?, :two_factor_skippable? if respond_to?(:helper_method)
end
def check_two_factor_requirement
@@ -33,7 +26,7 @@ module EnforcesTwoFactorAuthentication
when GraphqlController
render_error(
_("Authentication error: enable 2FA in your profile settings to continue using GitLab: %{mfa_help_page}") %
- { mfa_help_page: MFA_HELP_PAGE },
+ { mfa_help_page: mfa_help_page_url },
status: :unauthorized
)
else
@@ -84,6 +77,13 @@ module EnforcesTwoFactorAuthentication
def two_factor_verifier
@two_factor_verifier ||= Gitlab::Auth::TwoFactorAuthVerifier.new(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+
+ def mfa_help_page_url
+ Rails.application.routes.url_helpers.help_page_url(
+ 'user/profile/account/two_factor_authentication.html',
+ anchor: 'enable-two-factor-authentication'
+ )
+ end
end
EnforcesTwoFactorAuthentication.prepend_mod_with('EnforcesTwoFactorAuthentication')
diff --git a/app/controllers/concerns/impersonation.rb b/app/controllers/concerns/impersonation.rb
index 539dd9ad69d..e562cf5dbe4 100644
--- a/app/controllers/concerns/impersonation.rb
+++ b/app/controllers/concerns/impersonation.rb
@@ -3,11 +3,11 @@
module Impersonation
include Gitlab::Utils::StrongMemoize
- SESSION_KEYS_TO_DELETE = %w(
+ SESSION_KEYS_TO_DELETE = %w[
github_access_token gitea_access_token gitlab_access_token
bitbucket_token bitbucket_refresh_token bitbucket_server_personal_access_token
bulk_import_gitlab_access_token fogbugz_token
- ).freeze
+ ].freeze
def current_user
user = super
diff --git a/app/controllers/concerns/import/github_oauth.rb b/app/controllers/concerns/import/github_oauth.rb
index d53022aabf2..c233f5d09fa 100644
--- a/app/controllers/concerns/import/github_oauth.rb
+++ b/app/controllers/concerns/import/github_oauth.rb
@@ -53,6 +53,7 @@ module Import
def authorize_url
state = SecureRandom.base64(64)
session[auth_state_key] = state
+ session[:auth_on_failure_path] = "#{new_project_path}#import_project"
if Feature.enabled?(:remove_legacy_github_client)
oauth_client.auth_code.authorize_url(
redirect_uri: callback_import_url,
diff --git a/app/controllers/concerns/integrations/params.rb b/app/controllers/concerns/integrations/params.rb
index 30de4a86bec..74d998503b7 100644
--- a/app/controllers/concerns/integrations/params.rb
+++ b/app/controllers/concerns/integrations/params.rb
@@ -88,7 +88,9 @@ module Integrations
param_values = return_value[:integration]
if param_values.is_a?(ActionController::Parameters)
- if action_name == 'update' && integration.chat? && param_values['webhook'] == BaseChatNotification::SECRET_MASK
+ if %w[update test].include?(action_name) && integration.chat? &&
+ param_values['webhook'] == BaseChatNotification::SECRET_MASK
+
param_values.delete('webhook')
end
diff --git a/app/controllers/concerns/invisible_captcha_on_signup.rb b/app/controllers/concerns/invisible_captcha_on_signup.rb
index c7fd6d08744..b78869e02d0 100644
--- a/app/controllers/concerns/invisible_captcha_on_signup.rb
+++ b/app/controllers/concerns/invisible_captcha_on_signup.rb
@@ -13,7 +13,7 @@ module InvisibleCaptchaOnSignup
invisible_captcha_honeypot_counter.increment
log_request('Invisible_Captcha_Honeypot_Request')
- head(200)
+ head(:ok)
end
def on_timestamp_spam_callback
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index bea184e44b9..0669f051457 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -146,13 +146,17 @@ module IssuableActions
finder = Issuable::DiscussionsListService.new(current_user, issuable, finder_params_for_issuable)
discussion_notes = finder.execute
- response.headers['X-Next-Page-Cursor'] = finder.paginator.cursor_for_next_page if finder.paginator.present? && finder.paginator.has_next_page?
+ if finder.paginator.present? && finder.paginator.has_next_page?
+ response.headers['X-Next-Page-Cursor'] = finder.paginator.cursor_for_next_page
+ end
case issuable
when MergeRequest
render_mr_discussions(discussion_notes, discussion_serializer, discussion_cache_context)
when Issue
- render json: discussion_serializer.represent(discussion_notes, context: self) if stale?(etag: [discussion_cache_context, discussion_notes])
+ if stale?(etag: [discussion_cache_context, discussion_notes])
+ render json: discussion_serializer.represent(discussion_notes, context: self)
+ end
else
render json: discussion_serializer.represent(discussion_notes, context: self)
end
@@ -173,7 +177,7 @@ module IssuableActions
def render_cached_discussions(discussions, serializer, cache_context)
render_cached(discussions,
with: serializer,
- cache_context: -> (_) { cache_context },
+ cache_context: ->(_) { cache_context },
context: self)
end
@@ -230,15 +234,11 @@ module IssuableActions
end
def authorize_destroy_issuable!
- unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
- access_denied!
- end
+ access_denied! unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
end
def authorize_admin_issuable!
- unless can?(current_user, :"admin_#{resource_name}", parent)
- access_denied!
- end
+ access_denied! unless can?(current_user, :"admin_#{resource_name}", parent)
end
def authorize_update_issuable!
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index de38d26e3fe..7b0d8cf8dcb 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -14,7 +14,9 @@ module IssuableCollections
private
def show_alert_if_search_is_disabled
- return if current_user || params[:search].blank? || !html_request? || Feature.disabled?(:disable_anonymous_search, type: :ops)
+ if current_user || params[:search].blank? || !html_request? || Feature.disabled?(:disable_anonymous_search, type: :ops)
+ return
+ end
flash.now[:notice] = _('You must sign in to search for specific terms.')
end
diff --git a/app/controllers/concerns/issues_calendar.rb b/app/controllers/concerns/issues_calendar.rb
index 51d6d3cf05a..692ac5e700b 100644
--- a/app/controllers/concerns/issues_calendar.rb
+++ b/app/controllers/concerns/issues_calendar.rb
@@ -16,9 +16,7 @@ module IssuesCalendar
# the content as a file (even ignoring the Content-Disposition
# header). We want to display the content inline when accessed
# from GitLab, similarly to the RSS feed.
- if request.referer&.start_with?(::Settings.gitlab.base_url)
- response.headers['Content-Type'] = 'text/plain'
- end
+ response.headers['Content-Type'] = 'text/plain' if request.referer&.start_with?(::Settings.gitlab.base_url)
end
end
end
diff --git a/app/controllers/concerns/labels_as_hash.rb b/app/controllers/concerns/labels_as_hash.rb
index e428520f709..601d3bf50eb 100644
--- a/app/controllers/concerns/labels_as_hash.rb
+++ b/app/controllers/concerns/labels_as_hash.rb
@@ -16,9 +16,7 @@ module LabelsAsHash
if already_set_labels.present?
titles = already_set_labels.map(&:title)
label_hashes.each do |hash|
- if titles.include?(hash['title'])
- hash[:set] = true
- end
+ hash[:set] = true if titles.include?(hash['title'])
end
end
end
diff --git a/app/controllers/concerns/lfs_request.rb b/app/controllers/concerns/lfs_request.rb
index 97df3c7caea..1653b40bad5 100644
--- a/app/controllers/concerns/lfs_request.rb
+++ b/app/controllers/concerns/lfs_request.rb
@@ -78,25 +78,27 @@ module LfsRequest
end
def lfs_download_access?
- strong_memoize(:lfs_download_access) do
- ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code?
- end
+ ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code?
end
+ strong_memoize_attr :lfs_download_access?, :lfs_download_access
def deploy_token_can_download_code?
deploy_token.present? &&
- deploy_token.project == project &&
- deploy_token.active? &&
+ deploy_token.has_access_to?(project) &&
deploy_token.read_repository?
end
def lfs_upload_access?
- strong_memoize(:lfs_upload_access) do
- next false unless has_authentication_ability?(:push_code)
- next false if limit_exceeded?
+ return false unless has_authentication_ability?(:push_code)
+ return false if limit_exceeded?
- lfs_deploy_token? || can?(user, :push_code, project) || can?(deploy_token, :push_code, project)
- end
+ lfs_deploy_token? || can?(user, :push_code,
+project) || can?(deploy_token, :push_code, project) || any_branch_allows_collaboration?
+ end
+ strong_memoize_attr :lfs_upload_access?, :lfs_upload_access
+
+ def any_branch_allows_collaboration?
+ project.merge_requests_allowing_push_to_user(user).any?
end
def lfs_deploy_token?
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 8a67b62f28b..28d0af7a118 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -40,17 +40,15 @@ module MembershipActions
respond_to do |format|
format.html do
message =
- begin
- case membershipable
- when Namespace
- if skip_subresources
- _("User was successfully removed from group.")
- else
- _("User was successfully removed from group and any subgroups and projects.")
- end
+ case membershipable
+ when Namespace
+ if skip_subresources
+ _("User was successfully removed from group.")
else
- _("User was successfully removed from project.")
+ _("User was successfully removed from group and any subgroups and projects.")
end
+ else
+ _("User was successfully removed from project.")
end
redirect_to members_page_url, notice: message
diff --git a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
index 65237b552ca..ea9fd2de961 100644
--- a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
+++ b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb
@@ -12,9 +12,7 @@ module Metrics::Dashboard::PrometheusApiProxy
variable_substitution_result =
proxy_variable_substitution_service.new(proxyable, permit_params).execute
- if variable_substitution_result[:status] == :error
- return error_response(variable_substitution_result)
- end
+ return error_response(variable_substitution_result) if variable_substitution_result[:status] == :error
prometheus_result = ::Prometheus::ProxyService.new(
proxyable,
diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb
index 28d0692d748..d4e8e95e016 100644
--- a/app/controllers/concerns/metrics_dashboard.rb
+++ b/app/controllers/concerns/metrics_dashboard.rb
@@ -118,9 +118,7 @@ module MetricsDashboard
def decoded_params
params = metrics_dashboard_params
- if params[:dashboard_path]
- params[:dashboard_path] = CGI.unescape(params[:dashboard_path])
- end
+ params[:dashboard_path] = CGI.unescape(params[:dashboard_path]) if params[:dashboard_path]
params
end
diff --git a/app/controllers/concerns/milestone_actions.rb b/app/controllers/concerns/milestone_actions.rb
index 0a859bd3af9..e1967c50d70 100644
--- a/app/controllers/concerns/milestone_actions.rb
+++ b/app/controllers/concerns/milestone_actions.rb
@@ -8,9 +8,9 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_issues_tab", {
- issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
- show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
- })
+ issues: @milestone.sorted_issues(current_user), # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
+ })
end
end
end
@@ -20,9 +20,9 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_merge_requests_tab", {
- merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables
- show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
- })
+ merge_requests: @milestone.sorted_merge_requests(current_user).preload_milestoneish_associations, # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ show_project_name: Gitlab::Utils.to_boolean(params[:show_project_name])
+ })
end
end
end
@@ -32,8 +32,8 @@ module MilestoneActions
format.html { redirect_to milestone_redirect_path }
format.json do
render json: tabs_json("shared/milestones/_participants_tab", {
- users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
- })
+ users: @milestone.issue_participants_visible_by_user(current_user) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ })
end
end
end
@@ -46,10 +46,10 @@ module MilestoneActions
milestone_labels = @milestone.issue_labels_visible_by_user(current_user)
render json: tabs_json("shared/milestones/_labels_tab", {
- labels: milestone_labels.map do |label|
- label.present(issuable_subject: @milestone.resource_parent)
- end
- })
+ labels: milestone_labels.map do |label|
+ label.present(issuable_subject: @milestone.resource_parent)
+ end
+ })
end
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index b595c3c6790..a41e2d840ac 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -89,9 +89,7 @@ module NotesActions
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def destroy
- if note.editable?
- Notes::DestroyService.new(project, current_user).execute(note)
- end
+ Notes::DestroyService.new(project, current_user).execute(note) if note.editable?
respond_to do |format|
format.js { head :ok }
@@ -258,15 +256,14 @@ module NotesActions
end
def last_fetched_at
- strong_memoize(:last_fetched_at) do
- microseconds = request.headers['X-Last-Fetched-At'].to_i
+ microseconds = request.headers['X-Last-Fetched-At'].to_i
- seconds = microseconds / MICROSECOND
- frac = microseconds % MICROSECOND
+ seconds = microseconds / MICROSECOND
+ frac = microseconds % MICROSECOND
- Time.zone.at(seconds, frac)
- end
+ Time.zone.at(seconds, frac)
end
+ strong_memoize_attr :last_fetched_at
def notes_filter
current_user&.notes_filter_for(params[:target_type])
@@ -285,23 +282,22 @@ module NotesActions
end
def note_project
- strong_memoize(:note_project) do
- next nil unless project
+ return unless project
- note_project_id = params[:note_project_id]
+ note_project_id = params[:note_project_id]
- the_project =
- if note_project_id.present?
- Project.find(note_project_id)
- else
- project
- end
+ the_project =
+ if note_project_id.present?
+ Project.find(note_project_id)
+ else
+ project
+ end
- next access_denied! unless can?(current_user, :create_note, the_project)
+ return access_denied! unless can?(current_user, :create_note, the_project)
- the_project
- end
+ the_project
end
+ strong_memoize_attr :note_project
def return_discussion?
Gitlab::Utils.to_boolean(params[:return_discussion])
diff --git a/app/controllers/concerns/oauth_applications.rb b/app/controllers/concerns/oauth_applications.rb
index 8e63cc391ff..5b6fe933fda 100644
--- a/app/controllers/concerns/oauth_applications.rb
+++ b/app/controllers/concerns/oauth_applications.rb
@@ -12,9 +12,7 @@ module OauthApplications
def prepare_scopes
scopes = params.fetch(:doorkeeper_application, {}).fetch(:scopes, nil)
- if scopes
- params[:doorkeeper_application][:scopes] = scopes.join(' ')
- end
+ params[:doorkeeper_application][:scopes] = scopes.join(' ') if scopes
end
def set_created_session
@@ -30,7 +28,7 @@ module OauthApplications
end
def permitted_params
- %i{name redirect_uri scopes confidential}
+ %i[name redirect_uri scopes confidential]
end
def application_params
diff --git a/app/controllers/concerns/observability/content_security_policy.rb b/app/controllers/concerns/observability/content_security_policy.rb
new file mode 100644
index 00000000000..eccd1e1e3ef
--- /dev/null
+++ b/app/controllers/concerns/observability/content_security_policy.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Observability
+ module ContentSecurityPolicy
+ extend ActiveSupport::Concern
+
+ included do
+ content_security_policy do |p|
+ next if p.directives.blank? || Gitlab::Observability.observability_url.blank?
+
+ default_frame_src = p.directives['frame-src'] || p.directives['default-src']
+
+ # When ObservabilityUI is not authenticated, it needs to be able
+ # to redirect to the GL sign-in page, hence '/users/sign_in' and '/oauth/authorize'
+ frame_src_values = Array.wrap(default_frame_src) | [Gitlab::Observability.observability_url,
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
+'/users/sign_in'),
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.url,
+'/oauth/authorize')]
+
+ p.frame_src(*frame_src_values)
+ end
+ end
+ end
+end
diff --git a/app/controllers/concerns/page_limiter.rb b/app/controllers/concerns/page_limiter.rb
index 362b02e5856..1d044a41899 100644
--- a/app/controllers/concerns/page_limiter.rb
+++ b/app/controllers/concerns/page_limiter.rb
@@ -44,10 +44,11 @@ module PageLimiter
raise PageLimitNotANumberError unless max_page_number.is_a?(Integer)
raise PageLimitNotSensibleError unless max_page_number > 0
- if params[:page].present? && params[:page].to_i > max_page_number
- record_page_limit_interception
- raise PageOutOfBoundsError, max_page_number
- end
+ return if params[:page].blank?
+ return if params[:page].to_i <= max_page_number
+
+ record_page_limit_interception
+ raise PageOutOfBoundsError, max_page_number
end
# By default just return a HTTP status code and an empty response
diff --git a/app/controllers/concerns/paginated_collection.rb b/app/controllers/concerns/paginated_collection.rb
index fcee4493314..94a52dd0f89 100644
--- a/app/controllers/concerns/paginated_collection.rb
+++ b/app/controllers/concerns/paginated_collection.rb
@@ -10,9 +10,7 @@ module PaginatedCollection
out_of_range = collection.current_page > total_pages
- if out_of_range
- redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true)))
- end
+ redirect_to(url_for(safe_params.merge(page: total_pages, only_path: true))) if out_of_range
out_of_range
end
diff --git a/app/controllers/concerns/preferred_language_switcher.rb b/app/controllers/concerns/preferred_language_switcher.rb
index 9711e57cf7a..00cd0f9d1d5 100644
--- a/app/controllers/concerns/preferred_language_switcher.rb
+++ b/app/controllers/concerns/preferred_language_switcher.rb
@@ -16,3 +16,5 @@ module PreferredLanguageSwitcher
Gitlab::CurrentSettings.default_preferred_language
end
end
+
+PreferredLanguageSwitcher.prepend_mod
diff --git a/app/controllers/concerns/preview_markdown.rb b/app/controllers/concerns/preview_markdown.rb
index 7af114313a1..a7655efe7a9 100644
--- a/app/controllers/concerns/preview_markdown.rb
+++ b/app/controllers/concerns/preview_markdown.rb
@@ -45,7 +45,13 @@ module PreviewMarkdown
when 'projects' then projects_filter_params
when 'timeline_events' then timeline_events_filter_params
else {}
- end.merge(requested_path: params[:path], ref: params[:ref])
+ end.merge(
+ requested_path: params[:path],
+ ref: params[:ref],
+ # Disable comments in markdown for IE browsers because comments in IE
+ # could allow script execution.
+ allow_comments: !browser.ie?
+ )
end
# rubocop:enable Gitlab/ModuleWithInstanceVariables
diff --git a/app/controllers/concerns/product_analytics_tracking.rb b/app/controllers/concerns/product_analytics_tracking.rb
index dfa159ccfd7..b01320ce3ec 100644
--- a/app/controllers/concerns/product_analytics_tracking.rb
+++ b/app/controllers/concerns/product_analytics_tracking.rb
@@ -16,7 +16,7 @@ module ProductAnalyticsTracking
end
end
- def track_custom_event(*controller_actions, name:, conditions: nil, action:, label:, destinations: [:redis_hll], &block)
+ def track_custom_event(*controller_actions, name:, action:, label:, conditions: nil, destinations: [:redis_hll], &block)
custom_conditions = [:trackable_html_request?, *conditions]
after_action only: controller_actions, if: custom_conditions do
@@ -30,15 +30,15 @@ module ProductAnalyticsTracking
def route_events_to(destinations, name, &block)
track_unique_redis_hll_event(name, &block) if destinations.include?(:redis_hll)
- if destinations.include?(:snowplow) && event_enabled?(name)
- Gitlab::Tracking.event(
- self.class.to_s,
- name,
- namespace: tracking_namespace_source,
- user: current_user,
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: name).to_context]
- )
- end
+ return unless destinations.include?(:snowplow) && event_enabled?(name)
+
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ name,
+ namespace: tracking_namespace_source,
+ user: current_user,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: name).to_context]
+ )
end
def route_custom_events_to(destinations, name, action, label, &block)
@@ -64,30 +64,32 @@ module ProductAnalyticsTracking
def event_enabled?(event)
events_to_ff = {
- g_analytics_valuestream: :route_hll_to_snowplow,
-
- i_search_paid: :route_hll_to_snowplow_phase2,
- i_search_total: :route_hll_to_snowplow_phase2,
- i_search_advanced: :route_hll_to_snowplow_phase2,
- i_ecosystem_jira_service_list_issues: :route_hll_to_snowplow_phase2,
- users_viewing_analytics_group_devops_adoption: :route_hll_to_snowplow_phase2,
- i_analytics_dev_ops_adoption: :route_hll_to_snowplow_phase2,
- i_analytics_dev_ops_score: :route_hll_to_snowplow_phase2,
- p_analytics_merge_request: :route_hll_to_snowplow_phase2,
- i_analytics_instance_statistics: :route_hll_to_snowplow_phase2,
- g_analytics_contribution: :route_hll_to_snowplow_phase2,
- p_analytics_pipelines: :route_hll_to_snowplow_phase2,
- p_analytics_code_reviews: :route_hll_to_snowplow_phase2,
- p_analytics_valuestream: :route_hll_to_snowplow_phase2,
- p_analytics_insights: :route_hll_to_snowplow_phase2,
- p_analytics_issues: :route_hll_to_snowplow_phase2,
- p_analytics_repo: :route_hll_to_snowplow_phase2,
- g_analytics_insights: :route_hll_to_snowplow_phase2,
- g_analytics_issues: :route_hll_to_snowplow_phase2,
- g_analytics_productivity: :route_hll_to_snowplow_phase2,
- i_analytics_cohorts: :route_hll_to_snowplow_phase2
+ g_analytics_valuestream: '',
+
+ i_search_paid: :_phase2,
+ i_search_total: :_phase2,
+ i_search_advanced: :_phase2,
+ i_ecosystem_jira_service_list_issues: :_phase2,
+ users_viewing_analytics_group_devops_adoption: :_phase2,
+ i_analytics_dev_ops_adoption: :_phase2,
+ i_analytics_dev_ops_score: :_phase2,
+ p_analytics_merge_request: :_phase2,
+ i_analytics_instance_statistics: :_phase2,
+ g_analytics_contribution: :_phase2,
+ p_analytics_pipelines: :_phase2,
+ p_analytics_code_reviews: :_phase2,
+ p_analytics_valuestream: :_phase2,
+ p_analytics_insights: :_phase2,
+ p_analytics_issues: :_phase2,
+ p_analytics_repo: :_phase2,
+ g_analytics_insights: :_phase2,
+ g_analytics_issues: :_phase2,
+ g_analytics_productivity: :_phase2,
+ i_analytics_cohorts: :_phase2,
+
+ g_compliance_dashboard: :_phase4
}
- Feature.enabled?(events_to_ff[event.to_sym], tracking_namespace_source)
+ Feature.enabled?("route_hll_to_snowplow#{events_to_ff[event.to_sym]}", tracking_namespace_source)
end
end
diff --git a/app/controllers/concerns/record_user_last_activity.rb b/app/controllers/concerns/record_user_last_activity.rb
index 29164df4516..6ac87d8f27b 100644
--- a/app/controllers/concerns/record_user_last_activity.rb
+++ b/app/controllers/concerns/record_user_last_activity.rb
@@ -18,9 +18,8 @@ module RecordUserLastActivity
def set_user_last_activity
return unless request.get?
return if Gitlab::Database.read_only?
+ return unless current_user && current_user.last_activity_on != Date.today
- if current_user && current_user.last_activity_on != Date.today
- Users::ActivityService.new(current_user).execute
- end
+ Users::ActivityService.new(current_user).execute
end
end
diff --git a/app/controllers/concerns/render_service_results.rb b/app/controllers/concerns/render_service_results.rb
index 0149a71d9f5..83b880096be 100644
--- a/app/controllers/concerns/render_service_results.rb
+++ b/app/controllers/concerns/render_service_results.rb
@@ -5,25 +5,25 @@ module RenderServiceResults
def success_response(result)
render({
- status: result[:http_status],
- json: result[:body]
- })
+ status: result[:http_status],
+ json: result[:body]
+ })
end
def continue_polling_response
render({
- status: :no_content,
- json: {
- status: _('processing'),
- message: _('Not ready yet. Try again later.')
- }
- })
+ status: :no_content,
+ json: {
+ status: _('processing'),
+ message: _('Not ready yet. Try again later.')
+ }
+ })
end
def error_response(result)
render({
- status: result[:http_status] || :bad_request,
- json: { status: result[:status], message: result[:message] }
- })
+ status: result[:http_status] || :bad_request,
+ json: { status: result[:status], message: result[:message] }
+ })
end
end
diff --git a/app/controllers/concerns/renders_ldap_servers.rb b/app/controllers/concerns/renders_ldap_servers.rb
index cc83ff47048..8c3d9fd4d5c 100644
--- a/app/controllers/concerns/renders_ldap_servers.rb
+++ b/app/controllers/concerns/renders_ldap_servers.rb
@@ -8,12 +8,10 @@ module RendersLdapServers
end
def ldap_servers
- @ldap_servers ||= begin
- if Gitlab::Auth::Ldap::Config.sign_in_enabled?
- Gitlab::Auth::Ldap::Config.available_servers
- else
- []
- end
- end
+ @ldap_servers ||= if Gitlab::Auth::Ldap::Config.sign_in_enabled?
+ Gitlab::Auth::Ldap::Config.available_servers
+ else
+ []
+ end
end
end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index e34d6b09c24..28e1fa473b3 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -46,13 +46,13 @@ module RoutableActions
return unless request.get?
canonical_path = routable.full_path
- if canonical_path != routable_full_path
- if !request.xhr? && request.format.html? && canonical_path.casecmp(routable_full_path) != 0
- flash[:notice] = "#{routable.class.to_s.titleize} '#{routable_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
- end
+ return unless canonical_path != routable_full_path
- redirect_to build_canonical_path(routable), status: :moved_permanently
+ if !request.xhr? && request.format.html? && canonical_path.casecmp(routable_full_path) != 0
+ flash[:notice] = "#{routable.class.to_s.titleize} '#{routable_full_path}' was moved to '#{canonical_path}'. Please update any links and bookmarks that may still have the old path."
end
+
+ redirect_to build_canonical_path(routable), status: :moved_permanently
end
end
diff --git a/app/controllers/concerns/snippets/blobs_actions.rb b/app/controllers/concerns/snippets/blobs_actions.rb
index b510594ad63..2a0491b4df8 100644
--- a/app/controllers/concerns/snippets/blobs_actions.rb
+++ b/app/controllers/concerns/snippets/blobs_actions.rb
@@ -25,14 +25,13 @@ module Snippets::BlobsActions
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def blob
- strong_memoize(:blob) do
- assign_ref_vars
+ assign_ref_vars
- next unless @commit
+ return unless @commit
- @repo.blob_at(@commit.id, @path)
- end
+ @repo.blob_at(@commit.id, @path)
end
+ strong_memoize_attr :blob
# rubocop:enable Gitlab/ModuleWithInstanceVariables
def ensure_blob
@@ -40,11 +39,11 @@ module Snippets::BlobsActions
end
def ensure_repository
- unless snippet.repo_exists?
- Gitlab::AppLogger.error(message: "Snippet raw blob attempt with no repo", snippet: snippet.id)
+ return if snippet.repo_exists?
- respond_422
- end
+ Gitlab::AppLogger.error(message: "Snippet raw blob attempt with no repo", snippet: snippet.id)
+
+ respond_422
end
def snippet_id
diff --git a/app/controllers/concerns/sorting_preference.rb b/app/controllers/concerns/sorting_preference.rb
index 6278b489028..300c1d6d779 100644
--- a/app/controllers/concerns/sorting_preference.rb
+++ b/app/controllers/concerns/sorting_preference.rb
@@ -45,9 +45,7 @@ module SortingPreference
return sort_param if Gitlab::Database.read_only?
- if user_preference[field] != sort_param
- user_preference.update(field => sort_param)
- end
+ user_preference.update(field => sort_param) if user_preference[field] != sort_param
sort_param
end
diff --git a/app/controllers/concerns/sourcegraph_decorator.rb b/app/controllers/concerns/sourcegraph_decorator.rb
index 061990a4361..4aeace1ca67 100644
--- a/app/controllers/concerns/sourcegraph_decorator.rb
+++ b/app/controllers/concerns/sourcegraph_decorator.rb
@@ -22,8 +22,8 @@ module SourcegraphDecorator
return unless sourcegraph_enabled?
gon.push({
- sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url }
- })
+ sourcegraph: { url: Gitlab::CurrentSettings.sourcegraph_url }
+ })
end
def sourcegraph_enabled?
diff --git a/app/controllers/concerns/uploads_actions.rb b/app/controllers/concerns/uploads_actions.rb
index e98d36854f1..0ba13896631 100644
--- a/app/controllers/concerns/uploads_actions.rb
+++ b/app/controllers/concerns/uploads_actions.rb
@@ -5,7 +5,7 @@ module UploadsActions
include Gitlab::Utils::StrongMemoize
include SendFileUpload
- UPLOAD_MOUNTS = %w(avatar attachment file logo header_logo favicon).freeze
+ UPLOAD_MOUNTS = %w[avatar attachment file logo header_logo favicon].freeze
included do
prepend_before_action :set_request_format_from_path_extension
@@ -73,11 +73,11 @@ module UploadsActions
def set_request_format_from_path_extension
path = request.headers['action_dispatch.original_path'] || request.headers['PATH_INFO']
- if match = path&.match(/\.(\w+)\z/)
- format = Mime[match.captures.first]
+ return unless match = path&.match(/\.(\w+)\z/)
- request.format = format.symbol if format
- end
+ format = Mime[match.captures.first]
+
+ request.format = format.symbol if format
end
def content_disposition
@@ -102,14 +102,13 @@ module UploadsActions
end
def uploader
- strong_memoize(:uploader) do
- if uploader_mounted?
- model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
- else
- build_uploader_from_upload || build_uploader_from_params
- end
+ if uploader_mounted?
+ model.public_send(upload_mount) # rubocop:disable GitlabSecurity/PublicSend
+ else
+ build_uploader_from_upload || build_uploader_from_params
end
end
+ strong_memoize_attr :uploader
# rubocop: disable CodeReuse/ActiveRecord
def build_uploader_from_upload
@@ -163,8 +162,9 @@ module UploadsActions
end
def model
- strong_memoize(:model) { find_model }
+ find_model
end
+ strong_memoize_attr :model
def workhorse_authorize_request?
action_name == 'authorize'
diff --git a/app/controllers/concerns/verifies_with_email.rb b/app/controllers/concerns/verifies_with_email.rb
index ac1475597ff..3cada24a81a 100644
--- a/app/controllers/concerns/verifies_with_email.rb
+++ b/app/controllers/concerns/verifies_with_email.rb
@@ -28,7 +28,7 @@ module VerifiesWithEmail
if user.unlock_token
# Prompt for the token if it already has been set
prompt_for_email_verification(user)
- elsif user.access_locked? || !AuthenticationEvent.initial_login_or_known_ip_address?(user, request.ip)
+ elsif user.access_locked? || !trusted_ip_address?(user)
# require email verification if:
# - their account has been locked because of too many failed login attempts, or
# - they have logged in before, but never from the current ip address
@@ -68,7 +68,7 @@ module VerifiesWithEmail
# After successful verification and calling sign_in, devise redirects the
# user to this path. Override it to show the successful verified page.
def after_sign_in_path_for(resource)
- if action_name == 'create' && session[:verification_user_id]
+ if action_name == 'create' && session[:verification_user_id] == resource.id
return users_successful_verification_path
end
@@ -133,6 +133,12 @@ module VerifiesWithEmail
sign_in(user)
end
+ def trusted_ip_address?(user)
+ return true if Feature.disabled?(:check_ip_address_for_email_verification)
+
+ AuthenticationEvent.initial_login_or_known_ip_address?(user, request.ip)
+ end
+
def prompt_for_email_verification(user)
session[:verification_user_id] = user.id
self.resource = user
diff --git a/app/controllers/concerns/vscode_cdn_csp.rb b/app/controllers/concerns/vscode_cdn_csp.rb
new file mode 100644
index 00000000000..dc8cea966e5
--- /dev/null
+++ b/app/controllers/concerns/vscode_cdn_csp.rb
@@ -0,0 +1,17 @@
+# rubocop:disable Naming/FileName
+# frozen_string_literal: true
+
+module VSCodeCDNCSP
+ extend ActiveSupport::Concern
+
+ included do
+ content_security_policy do |policy|
+ next if policy.directives.blank?
+
+ default_src = Array(policy.directives['default-src'] || [])
+ policy.directives['frame-src'] ||= default_src
+ policy.directives['frame-src'].concat(['https://*.vscode-cdn.net/'])
+ end
+ end
+end
+# rubocop:enable Naming/FileName
diff --git a/app/controllers/concerns/web_hooks/hook_actions.rb b/app/controllers/concerns/web_hooks/hook_actions.rb
index 75065ef9d24..f61600af951 100644
--- a/app/controllers/concerns/web_hooks/hook_actions.rb
+++ b/app/controllers/concerns/web_hooks/hook_actions.rb
@@ -18,7 +18,9 @@ module WebHooks
self.hook = relation.new(hook_params)
hook.save
- unless hook.valid?
+ if hook.valid?
+ flash[:notice] = _('Webhook was created')
+ else
self.hooks = relation.select(&:persisted?)
flash[:alert] = hook.errors.full_messages.to_sentence.html_safe
end
@@ -28,8 +30,8 @@ module WebHooks
def update
if hook.update(hook_params)
- flash[:notice] = _('Hook was successfully updated.')
- redirect_to action: :index
+ flash[:notice] = _('Webhook was updated')
+ redirect_to action: :edit
else
render 'edit'
end
@@ -66,21 +68,14 @@ module WebHooks
end
def hook_param_names
- param_names = %i[enable_ssl_verification token url push_events_branch_filter]
- param_names.push(:branch_filter_strategy) if Feature.enabled?(:enhanced_webhook_support_regex)
- param_names
+ %i[enable_ssl_verification token url push_events_branch_filter branch_filter_strategy]
end
def destroy_hook(hook)
result = WebHooks::DestroyService.new(current_user).execute(hook)
if result[:status] == :success
- flash[:notice] =
- if result[:async]
- format(_("%{hook_type} was scheduled for deletion"), hook_type: hook.model_name.human)
- else
- format(_("%{hook_type} was deleted"), hook_type: hook.model_name.human)
- end
+ flash[:notice] = result[:async] ? _('Webhook was scheduled for deletion') : _('Webhook was deleted')
else
flash[:alert] = result[:message]
end
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
index 5a885349467..d72a5e44b9f 100644
--- a/app/controllers/dashboard/snippets_controller.rb
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -7,7 +7,7 @@ class Dashboard::SnippetsController < Dashboard::ApplicationController
skip_cross_project_access_check :index
- feature_category :snippets
+ feature_category :source_code_management
def index
@snippet_counts = Snippets::CountService
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index d2434d4b0ba..3005d19f8ed 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -3,6 +3,7 @@
class Dashboard::TodosController < Dashboard::ApplicationController
include ActionView::Helpers::NumberHelper
include PaginatedCollection
+ include Gitlab::Utils::StrongMemoize
before_action :authorize_read_project!, only: :index
before_action :authorize_read_group!, only: :index
@@ -64,19 +65,19 @@ class Dashboard::TodosController < Dashboard::ApplicationController
def authorize_read_project!
project_id = params[:project_id]
- if project_id.present?
- project = Project.find(project_id)
- render_404 unless can?(current_user, :read_project, project)
- end
+ return unless project_id.present?
+
+ project = Project.find(project_id)
+ render_404 unless can?(current_user, :read_project, project)
end
def authorize_read_group!
group_id = params[:group_id]
- if group_id.present?
- group = Group.find(group_id)
- render_404 unless can?(current_user, :read_group, group)
- end
+ return unless group_id.present?
+
+ group = Group.find(group_id)
+ render_404 unless can?(current_user, :read_group, group)
end
def find_todos
@@ -99,14 +100,28 @@ class Dashboard::TodosController < Dashboard::ApplicationController
end
def todo_params
- aliased_action_id(
+ aliased_params(
params.permit(:action_id, :author_id, :project_id, :type, :sort, :state, :group_id)
)
end
+ strong_memoize_attr :todo_params
+
+ def aliased_params(original_params)
+ alias_issue_type(original_params)
+ alias_action_id(original_params)
+
+ original_params
+ end
+
+ def alias_issue_type(original_params)
+ return unless original_params[:type] == Issue.name
+
+ original_params[:type] = [Issue.name, WorkItem.name]
+ end
- def aliased_action_id(original_params)
- return original_params unless original_params[:action_id].to_i == ::Todo::MENTIONED
+ def alias_action_id(original_params)
+ return unless original_params[:action_id].to_i == ::Todo::MENTIONED
- original_params.merge(action_id: [::Todo::MENTIONED, ::Todo::DIRECTLY_ADDRESSED])
+ original_params[:action_id] = [::Todo::MENTIONED, ::Todo::DIRECTLY_ADDRESSED]
end
end
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
index 617cc2e7f3d..dee94b53cc1 100644
--- a/app/controllers/explore/snippets_controller.rb
+++ b/app/controllers/explore/snippets_controller.rb
@@ -3,7 +3,7 @@
class Explore::SnippetsController < Explore::ApplicationController
include Gitlab::NoteableMetadata
- feature_category :snippets
+ feature_category :source_code_management
def index
@snippets = SnippetsFinder.new(current_user, explore: true)
diff --git a/app/controllers/google_api/authorizations_controller.rb b/app/controllers/google_api/authorizations_controller.rb
index 5080ee5fbbe..536c5e347e7 100644
--- a/app/controllers/google_api/authorizations_controller.rb
+++ b/app/controllers/google_api/authorizations_controller.rb
@@ -48,14 +48,13 @@ module GoogleApi
end
def redirect_uri_from_session
- strong_memoize(:redirect_uri_from_session) do
- if params[:state].present?
- session[session_key_for_redirect_uri(params[:state])]
- else
- nil
- end
+ if params[:state].present?
+ session[session_key_for_redirect_uri(params[:state])]
+ else
+ nil
end
end
+ strong_memoize_attr :redirect_uri_from_session
def session_key_for_redirect_uri(state)
GoogleApi::CloudPlatform::Client.session_key_for_redirect_uri(state)
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 5ffd525c170..942cb9beed4 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -70,6 +70,12 @@ class GraphqlController < ApplicationController
end
end
+ rescue_from Gitlab::Auth::TooManyIps do |exception|
+ log_exception(exception)
+
+ render_error(exception.message, status: :forbidden)
+ end
+
rescue_from Gitlab::Graphql::Variables::Invalid do |exception|
render_error(exception.message, status: :unprocessable_entity)
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index f8cfa996447..5440908aee7 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -96,6 +96,28 @@ class Groups::ApplicationController < ApplicationController
def validate_root_group!
render_404 unless group.root?
end
+
+ def authorize_action!(action)
+ access_denied! unless can?(current_user, action, group)
+ end
+
+ def respond_to_missing?(method, *args)
+ case method.to_s
+ when /\Aauthorize_(.*)!\z/
+ true
+ else
+ super
+ end
+ end
+
+ def method_missing(method_sym, *arguments, &block)
+ case method_sym.to_s
+ when /\Aauthorize_(.*)!\z/
+ authorize_action!(Regexp.last_match(1).to_sym)
+ else
+ super
+ end
+ end
end
Groups::ApplicationController.prepend_mod_with('Groups::ApplicationController')
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index e1ba86220c7..6bb807be1c4 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -20,16 +20,14 @@ class Groups::BoardsController < Groups::ApplicationController
private
def board_finder
- strong_memoize :board_finder do
- Boards::BoardsFinder.new(parent, current_user, board_id: params[:id])
- end
+ Boards::BoardsFinder.new(parent, current_user, board_id: params[:id])
end
+ strong_memoize_attr :board_finder
def board_create_service
- strong_memoize :board_create_service do
- Boards::CreateService.new(parent, current_user)
- end
+ Boards::CreateService.new(parent, current_user)
end
+ strong_memoize_attr :board_create_service
def authorize_read_board!
access_denied! unless can?(current_user, :read_issue_board, group)
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index 2e9e0b12d2f..427df9a7129 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -117,7 +117,7 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def blob_file_name
- @blob_file_name ||= params[:sha].sub('sha256:', '') + '.gz'
+ @blob_file_name ||= "#{params[:sha].sub('sha256:', '')}.gz"
end
def manifest_file_name
diff --git a/app/controllers/groups/observability_controller.rb b/app/controllers/groups/observability_controller.rb
index 4b1f2b582ce..3baa5e830ff 100644
--- a/app/controllers/groups/observability_controller.rb
+++ b/app/controllers/groups/observability_controller.rb
@@ -1,18 +1,9 @@
# frozen_string_literal: true
module Groups
class ObservabilityController < Groups::ApplicationController
- feature_category :tracing
-
- content_security_policy do |p|
- next if p.directives.blank?
-
- default_frame_src = p.directives['frame-src'] || p.directives['default-src']
+ include ::Observability::ContentSecurityPolicy
- # When ObservabilityUI is not authenticated, it needs to be able to redirect to the GL sign-in page, hence 'self'
- frame_src_values = Array.wrap(default_frame_src) | [observability_url, "'self'"]
-
- p.frame_src(*frame_src_values)
- end
+ feature_category :tracing
before_action :check_observability_allowed
@@ -34,16 +25,8 @@ module Groups
render 'observability', layout: 'group', locals: { base_layout: 'layouts/fullscreen' }
end
- def self.observability_url
- Gitlab::Observability.observability_url
- end
-
- def observability_url
- self.class.observability_url
- end
-
def check_observability_allowed
- return render_404 unless observability_url.present?
+ return render_404 unless Gitlab::Observability.observability_url.present?
render_404 unless can?(current_user, :read_observability, @group)
end
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index b1afac1f1c7..1dfa8cdf133 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -15,6 +15,8 @@ module Groups
urgency :low
def show
+ @entity = :group
+ @variable_limit = ::Plan.default.actual_limits.group_ci_variables
end
def update
diff --git a/app/controllers/groups/usage_quotas_controller.rb b/app/controllers/groups/usage_quotas_controller.rb
new file mode 100644
index 00000000000..29878f0001d
--- /dev/null
+++ b/app/controllers/groups/usage_quotas_controller.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Groups
+ class UsageQuotasController < Groups::ApplicationController
+ before_action :authorize_read_usage_quotas!
+ before_action :verify_usage_quotas_enabled!
+
+ feature_category :subscription_cost_management
+ urgency :low
+
+ def index
+ # To be used in ee/app/controllers/ee/groups/usage_quotas_controller.rb
+ @seat_count_data = seat_count_data
+ end
+
+ private
+
+ def verify_usage_quotas_enabled!
+ render_404 unless Feature.enabled?(:usage_quotas_for_all_editions, group)
+ render_404 if group.has_parent?
+ end
+
+ # To be overriden in ee/app/controllers/ee/groups/usage_quotas_controller.rb
+ def seat_count_data; end
+ end
+end
+
+Groups::UsageQuotasController.prepend_mod
diff --git a/app/controllers/groups/variables_controller.rb b/app/controllers/groups/variables_controller.rb
index 220b0b4509c..9ddf6c80c70 100644
--- a/app/controllers/groups/variables_controller.rb
+++ b/app/controllers/groups/variables_controller.rb
@@ -50,7 +50,7 @@ module Groups
end
def variable_params_attributes
- %i[id variable_type key secret_value protected masked _destroy]
+ %i[id variable_type key secret_value protected masked raw _destroy]
end
def authorize_admin_build!
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 3f516c24a69..0a487bb2508 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -386,7 +386,7 @@ class GroupsController < Groups::ApplicationController
override :has_project_list?
def has_project_list?
- %w(details show index).include?(action_name)
+ %w[details show index].include?(action_name)
end
def captcha_enabled?
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
index fcf6871d137..8a8c41e65b9 100644
--- a/app/controllers/ide_controller.rb
+++ b/app/controllers/ide_controller.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class IdeController < ApplicationController
+ include VSCodeCDNCSP
include ClientsidePreviewCSP
include StaticObjectExternalStorageCSP
include Gitlab::Utils::StrongMemoize
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 75193309a4e..1d05cee02d4 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -49,6 +49,14 @@ class Import::BitbucketController < Import::BaseController
namespace_path = params[:new_namespace].presence || repo_owner
target_namespace = find_or_create_namespace(namespace_path, current_user)
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create',
+ label: 'import_access_level',
+ user: current_user,
+ extra: { user_role: user_role(current_user, target_namespace), import_type: 'bitbucket' }
+ )
+
if current_user.can?(:create_projects, target_namespace)
# The token in a session can be expired, we need to get most recent one because
# Bitbucket::Connection class refreshes it.
@@ -89,6 +97,21 @@ class Import::BitbucketController < Import::BaseController
private
+ def user_role(user, namespace)
+ if current_user.id == namespace&.owner_id
+ Gitlab::Access.options_with_owner.key(Gitlab::Access::OWNER)
+ else
+ access_level = current_user&.group_members&.find_by(source_id: namespace&.id)&.access_level
+
+ case access_level
+ when nil
+ 'Not a member'
+ else
+ Gitlab::Access.human_access(access_level)
+ end
+ end
+ end
+
def oauth_client
@oauth_client ||= OAuth2::Client.new(provider.app_id, provider.app_secret, options)
end
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index 655fc7854fe..9a7118ce498 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -135,7 +135,7 @@ class Import::BulkImportsController < ApplicationController
session[url_key],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
clear_session_data
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
index 7b580234227..77043e174b4 100644
--- a/app/controllers/import/fogbugz_controller.rb
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -114,7 +114,7 @@ class Import::FogbugzController < Import::BaseController
end
def user_map_params
- params.permit(users: %w(name email gitlab_user))
+ params.permit(users: %w[name email gitlab_user])
end
def verify_fogbugz_import_enabled
@@ -126,7 +126,7 @@ class Import::FogbugzController < Import::BaseController
params[:uri],
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
redirect_to new_import_fogbugz_url, alert: _('Specified URL cannot be used: "%{reason}"') % { reason: e.message }
diff --git a/app/controllers/import/gitea_controller.rb b/app/controllers/import/gitea_controller.rb
index 4b4ac07b389..61e32650db3 100644
--- a/app/controllers/import/gitea_controller.rb
+++ b/app/controllers/import/gitea_controller.rb
@@ -16,12 +16,27 @@ class Import::GiteaController < Import::GithubController
super
end
- # We need to re-expose controller's internal method 'status' as action.
- # rubocop:disable Lint/UselessMethodDefinition
def status
- super
+ # Request repos to display error page if provider token is invalid
+ # Improving in https://gitlab.com/gitlab-org/gitlab/-/issues/25859
+ client_repos
+
+ respond_to do |format|
+ format.json do
+ render json: { imported_projects: serialized_imported_projects,
+ provider_repos: serialized_provider_repos,
+ incompatible_repos: serialized_incompatible_repos }
+ end
+
+ format.html do
+ if params[:namespace_id].present?
+ @namespace = Namespace.find_by_id(params[:namespace_id])
+
+ render_404 unless current_user.can?(:create_projects, @namespace)
+ end
+ end
+ end
end
- # rubocop:enable Lint/UselessMethodDefinition
protected
@@ -61,7 +76,6 @@ class Import::GiteaController < Import::GithubController
@client_repos ||= filtered(client.repos)
end
- override :client
def client
@client ||= Gitlab::LegacyGithubImport::Client.new(session[access_token_key], **client_options)
end
@@ -78,7 +92,7 @@ class Import::GiteaController < Import::GithubController
provider_url,
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
- schemes: %w(http https)
+ schemes: %w[http https]
)
rescue Gitlab::UrlBlocker::BlockedUrlError => e
session[access_token_key] = nil
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 92763e09ba3..cb58b5974ca 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -15,6 +15,8 @@ class Import::GithubController < Import::BaseController
rescue_from Octokit::TooManyRequests, with: :provider_rate_limit
rescue_from Gitlab::GithubImport::RateLimitError, with: :rate_limit_threshold_exceeded
+ delegate :client, to: :client_proxy, private: true
+
PAGE_LENGTH = 25
def new
@@ -46,7 +48,22 @@ class Import::GithubController < Import::BaseController
# Improving in https://gitlab.com/gitlab-org/gitlab-foss/issues/55585
client_repos
- super
+ respond_to do |format|
+ format.json do
+ render json: { imported_projects: serialized_imported_projects,
+ provider_repos: serialized_provider_repos,
+ incompatible_repos: serialized_incompatible_repos,
+ page_info: client_repos_response[:page_info] }
+ end
+
+ format.html do
+ if params[:namespace_id].present?
+ @namespace = Namespace.find_by_id(params[:namespace_id])
+
+ render_404 unless current_user.can?(:create_projects, @namespace)
+ end
+ end
+ end
end
def create
@@ -126,24 +143,18 @@ class Import::GithubController < Import::BaseController
end
end
- def client
- @client ||= if Feature.enabled?(:remove_legacy_github_client)
- Gitlab::GithubImport::Client.new(session[access_token_key])
- else
- Gitlab::LegacyGithubImport::Client.new(session[access_token_key], **client_options)
- end
+ def client_proxy
+ @client_proxy ||= Gitlab::GithubImport::Clients::Proxy.new(
+ session[access_token_key], client_options
+ )
+ end
+
+ def client_repos_response
+ @client_repos_response ||= client_proxy.repos(sanitized_filter_param, pagination_options)
end
def client_repos
- @client_repos ||= if Feature.enabled?(:remove_legacy_github_client)
- if sanitized_filter_param
- client.search_repos_by_name(sanitized_filter_param, pagination_options)[:items]
- else
- client.repos(pagination_options)
- end
- else
- filtered(client.repos)
- end
+ client_repos_response[:repos]
end
def sanitized_filter_param
@@ -213,6 +224,11 @@ class Import::GithubController < Import::BaseController
def pagination_options
{
+ before: params[:before].presence,
+ after: params[:after].presence,
+ first: PAGE_LENGTH,
+ # TODO: remove after rollout FF github_client_fetch_repos_via_graphql
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/385649
page: [1, params[:page].to_i].max,
per_page: PAGE_LENGTH
}
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index 16bd73f5ab6..3c50d54fa10 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -28,7 +28,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
type: 'jwt'
},
modules: modules,
- scopes: %w(READ WRITE DELETE),
+ scopes: %w[READ WRITE DELETE],
apiVersion: 1,
apiMigrations: {
'context-qsh': true,
@@ -76,7 +76,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
jiraDevelopmentTool: {
actions: {
createBranch: {
- templateUrl: new_jira_connect_branch_url + '?issue_key={issue.key}&issue_summary={issue.summary}'
+ templateUrl: "#{new_jira_connect_branch_url}?issue_key={issue.key}&issue_summary={issue.summary}"
}
},
key: 'gitlab-development-tool',
@@ -84,7 +84,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
name: { value: 'GitLab' },
url: HOME_URL,
logoUrl: logo_url,
- capabilities: %w(branch commit pull_request)
+ capabilities: %w[branch commit pull_request]
}
}
end
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index b9f0ea795e1..e26d69314cd 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -3,11 +3,6 @@
class JiraConnect::ApplicationController < ApplicationController
include Gitlab::Utils::StrongMemoize
- CORS_ALLOWED_METHODS = {
- '/-/jira_connect/oauth_application_id' => %i[GET OPTIONS],
- '/-/jira_connect/subscriptions/*' => %i[DELETE OPTIONS]
- }.freeze
-
skip_before_action :authenticate_user!
skip_before_action :verify_authenticity_token
before_action :verify_atlassian_jwt!
@@ -65,25 +60,4 @@ class JiraConnect::ApplicationController < ApplicationController
def auth_token
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
end
-
- def cors_allowed_methods
- CORS_ALLOWED_METHODS[resource]
- end
-
- def resource
- request.path.gsub(%r{/\d+$}, '/*')
- end
-
- def set_cors_headers
- return unless allow_cors_request?
-
- response.set_header('Access-Control-Allow-Origin', Gitlab::CurrentSettings.jira_connect_proxy_url)
- response.set_header('Access-Control-Allow-Methods', cors_allowed_methods.join(', '))
- end
-
- def allow_cors_request?
- return false if cors_allowed_methods.nil?
-
- !Gitlab.com? && Gitlab::CurrentSettings.jira_connect_proxy_url.present?
- end
end
diff --git a/app/controllers/jira_connect/cors_preflight_checks_controller.rb b/app/controllers/jira_connect/cors_preflight_checks_controller.rb
deleted file mode 100644
index 3f30c1e04df..00000000000
--- a/app/controllers/jira_connect/cors_preflight_checks_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-module JiraConnect
- class CorsPreflightChecksController < ApplicationController
- feature_category :integrations
-
- skip_before_action :verify_atlassian_jwt!
- before_action :set_cors_headers
-
- def index
- return render_404 unless allow_cors_request?
-
- render plain: '', content_type: 'text/plain'
- end
- end
-end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index 394fdc9b2f6..fa1e1f505eb 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -31,7 +31,10 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
end
def update_installation
- current_jira_installation.update(update_params)
+ JiraConnectInstallations::UpdateService.execute(
+ current_jira_installation,
+ update_params
+ ).success?
end
def create_params
@@ -56,7 +59,7 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
def jwt_verification_claims
{
- aud: jira_connect_base_url(protocol: 'https'),
+ aud: Gitlab.config.jira_connect.enforce_jira_base_url_https ? jira_connect_base_url(protocol: 'https') : jira_connect_base_url,
iss: transformed_params[:client_key],
qsh: Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
}
diff --git a/app/controllers/jira_connect/installations_controller.rb b/app/controllers/jira_connect/installations_controller.rb
index 401bc4f9c87..44dbf90f5fb 100644
--- a/app/controllers/jira_connect/installations_controller.rb
+++ b/app/controllers/jira_connect/installations_controller.rb
@@ -6,11 +6,12 @@ class JiraConnect::InstallationsController < JiraConnect::ApplicationController
end
def update
- if current_jira_installation.update(installation_params)
+ result = update_installation
+ if result.success?
render json: installation_json(current_jira_installation)
else
render(
- json: { errors: current_jira_installation.errors },
+ json: { errors: result.message },
status: :unprocessable_entity
)
end
@@ -18,6 +19,13 @@ class JiraConnect::InstallationsController < JiraConnect::ApplicationController
private
+ def update_installation
+ JiraConnectInstallations::UpdateService.execute(
+ current_jira_installation,
+ installation_params
+ )
+ end
+
def installation_json(installation)
{
gitlab_com: installation.instance_url.blank?,
diff --git a/app/controllers/jira_connect/oauth_application_ids_controller.rb b/app/controllers/jira_connect/oauth_application_ids_controller.rb
index 3e788e2282e..de520337af3 100644
--- a/app/controllers/jira_connect/oauth_application_ids_controller.rb
+++ b/app/controllers/jira_connect/oauth_application_ids_controller.rb
@@ -5,7 +5,6 @@ module JiraConnect
feature_category :integrations
skip_before_action :verify_atlassian_jwt!
- before_action :set_cors_headers
def show
if show_application_id?
@@ -20,7 +19,7 @@ module JiraConnect
def show_application_id?
return if Gitlab.com?
- Feature.enabled?(:jira_connect_oauth_self_managed) && jira_connect_application_key.present?
+ jira_connect_application_key.present?
end
def jira_connect_application_key
diff --git a/app/controllers/jira_connect/public_keys_controller.rb b/app/controllers/jira_connect/public_keys_controller.rb
index b3144993edb..09003f8478f 100644
--- a/app/controllers/jira_connect/public_keys_controller.rb
+++ b/app/controllers/jira_connect/public_keys_controller.rb
@@ -10,7 +10,9 @@ module JiraConnect
skip_before_action :authenticate_user!
def show
- return render_404 if Feature.disabled?(:jira_connect_oauth_self_managed) || !Gitlab.com?
+ if Feature.disabled?(:jira_connect_oauth_self_managed) || !Gitlab.config.jira_connect.enable_public_keys_storage
+ return render_404
+ end
render plain: public_key.key
end
diff --git a/app/controllers/jira_connect/subscriptions_controller.rb b/app/controllers/jira_connect/subscriptions_controller.rb
index 9a732cadd94..ff7477a94d6 100644
--- a/app/controllers/jira_connect/subscriptions_controller.rb
+++ b/app/controllers/jira_connect/subscriptions_controller.rb
@@ -1,19 +1,20 @@
# frozen_string_literal: true
class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
+ ALLOWED_IFRAME_ANCESTORS = [:self, 'https://*.atlassian.net', 'https://*.jira.com'].freeze
layout 'jira_connect'
content_security_policy do |p|
next if p.directives.blank?
# rubocop: disable Lint/PercentStringArray
- script_src_values = Array.wrap(p.directives['script-src']) | %w('self' https://connect-cdn.atl-paas.net)
- style_src_values = Array.wrap(p.directives['style-src']) | %w('self' 'unsafe-inline')
+ script_src_values = Array.wrap(p.directives['script-src']) | %w['self' https://connect-cdn.atl-paas.net]
+ style_src_values = Array.wrap(p.directives['style-src']) | %w['self' 'unsafe-inline']
# rubocop: enable Lint/PercentStringArray
# *.jira.com is needed for some legacy Jira Cloud instances, new ones will use *.atlassian.net
# https://support.atlassian.com/organization-administration/docs/ip-addresses-and-domains-for-atlassian-cloud-products/
- p.frame_ancestors :self, 'https://*.atlassian.net', 'https://*.jira.com'
+ p.frame_ancestors(*(ALLOWED_IFRAME_ANCESTORS + Gitlab.config.jira_connect.additional_iframe_ancestors))
p.script_src(*script_src_values)
p.style_src(*style_src_values)
end
@@ -27,7 +28,6 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
before_action :verify_qsh_claim!, only: :index
before_action :allow_self_managed_content_security_policy, only: :index
before_action :authenticate_user!, only: :create
- before_action :set_cors_headers
def index
@subscriptions = current_jira_installation.subscriptions.preload_namespace_route
@@ -65,8 +65,6 @@ class JiraConnect::SubscriptionsController < JiraConnect::ApplicationController
private
def allow_self_managed_content_security_policy
- return unless Feature.enabled?(:jira_connect_oauth_self_managed_setting)
-
return unless current_jira_installation.instance_url?
request.content_security_policy.directives['connect-src'] ||= []
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index f3f0ddd968a..8650b6cbc6f 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -119,6 +119,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth, session)
link_identity(identity_linker)
+ set_remember_me(current_user)
if identity_linker.changed?
redirect_identity_linked
@@ -169,6 +170,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# available in the logs for this request.
Gitlab::ApplicationContext.push(user: user)
log_audit_event(user, with: oauth['provider'])
+ Gitlab::Tracking.event(self.class.name, "#{oauth['provider']}_sso", user: user) if new_user
set_remember_me(user)
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 1216353be36..38cdb16c350 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -58,8 +58,8 @@ class PasswordsController < Devise::PasswordsController
def check_password_authentication_available
if resource
return if resource.allow_password_authentication?
- else
- return if Gitlab::CurrentSettings.password_authentication_enabled?
+ elsif Gitlab::CurrentSettings.password_authentication_enabled?
+ return
end
redirect_to after_sending_reset_password_instructions_path_for(resource_name),
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 90d5f945d78..39e8f6c500d 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -37,6 +37,6 @@ class Profiles::KeysController < Profiles::ApplicationController
private
def key_params
- params.require(:key).permit(:title, :key, :expires_at)
+ params.require(:key).permit(:title, :key, :usage_type, :expires_at)
end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index a57c87bf691..974e7104c07 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -57,7 +57,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:render_whitespace_in_code,
:markdown_surround_selection,
:markdown_automatic_lists,
- :use_legacy_web_ide
+ :use_legacy_web_ide,
+ :use_new_navigation
]
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index 0933f2bb7ea..03b7cc9f892 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -97,7 +97,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def skip
if two_factor_grace_period_expired?
- redirect_to new_profile_two_factor_auth_path, alert: s_('Cannot skip two factor authentication setup')
+ redirect_to new_profile_two_factor_auth_path, alert: _('Cannot skip two factor authentication setup')
else
session[:skip_two_factor] = current_user.otp_grace_period_started_at + two_factor_grace_period.hours
redirect_to root_path
@@ -186,9 +186,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def u2f_registrations
current_user.u2f_registrations.map do |u2f_registration|
{
- name: u2f_registration.name,
- created_at: u2f_registration.created_at,
- delete_path: profile_u2f_registration_path(u2f_registration)
+ name: u2f_registration.name,
+ created_at: u2f_registration.created_at,
+ delete_path: profile_u2f_registration_path(u2f_registration)
}
end
end
@@ -196,9 +196,9 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
def webauthn_registrations
current_user.webauthn_registrations.map do |webauthn_registration|
{
- name: webauthn_registration.name,
- created_at: webauthn_registration.created_at,
- delete_path: profile_webauthn_registration_path(webauthn_registration)
+ name: webauthn_registration.name,
+ created_at: webauthn_registration.created_at,
+ delete_path: profile_webauthn_registration_path(webauthn_registration)
}
end
end
@@ -216,7 +216,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
group_links = groups.map { |group| view_context.link_to group.full_name, group_path(group) }.to_sentence
leave_group_links = groups.map { |group| view_context.link_to (s_("leave %{group_name}") % { group_name: group.full_name }), leave_group_members_path(group), remote: false, method: :delete }.to_sentence
- s_(%{The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.})
+ s_(%(The group settings for %{group_links} require you to enable Two-Factor Authentication for your account. You can %{leave_group_links}.))
.html_safe % { group_links: group_links.html_safe, leave_group_links: leave_group_links.html_safe }
end
diff --git a/app/controllers/projects/autocomplete_sources_controller.rb b/app/controllers/projects/autocomplete_sources_controller.rb
index 7755effe1da..ef20c71cd77 100644
--- a/app/controllers/projects/autocomplete_sources_controller.rb
+++ b/app/controllers/projects/autocomplete_sources_controller.rb
@@ -7,7 +7,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
feature_category :team_planning, [:issues, :labels, :milestones, :commands, :contacts]
feature_category :code_review, [:merge_requests]
feature_category :users, [:members]
- feature_category :snippets, [:snippets]
+ feature_category :source_code_management, [:snippets]
urgency :low, [:merge_requests, :members]
urgency :low, [:issues, :labels, :milestones, :commands, :contacts]
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index 42bd87e1c01..dbbffc4c283 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -13,10 +13,10 @@ class Projects::BadgesController < Projects::ApplicationController
def pipeline
pipeline_status = Gitlab::Ci::Badge::Pipeline::Status
.new(project, params[:ref], opts: {
- ignore_skipped: params[:ignore_skipped],
- key_text: params[:key_text],
- key_width: params[:key_width]
- })
+ ignore_skipped: params[:ignore_skipped],
+ key_text: params[:key_text],
+ key_width: params[:key_width]
+ })
render_badge pipeline_status
end
@@ -24,13 +24,13 @@ class Projects::BadgesController < Projects::ApplicationController
def coverage
coverage_report = Gitlab::Ci::Badge::Coverage::Report
.new(project, params[:ref], opts: {
- job: params[:job],
- key_text: params[:key_text],
- key_width: params[:key_width],
- min_good: params[:min_good],
- min_acceptable: params[:min_acceptable],
- min_medium: params[:min_medium]
- })
+ job: params[:job],
+ key_text: params[:key_text],
+ key_width: params[:key_width],
+ min_good: params[:min_good],
+ min_acceptable: params[:min_acceptable],
+ min_medium: params[:min_medium]
+ })
render_badge coverage_report
end
@@ -38,10 +38,10 @@ class Projects::BadgesController < Projects::ApplicationController
def release
latest_release = Gitlab::Ci::Badge::Release::LatestRelease
.new(project, current_user, opts: {
- key_text: params[:key_text],
- key_width: params[:key_width],
- order_by: params[:order_by]
- })
+ key_text: params[:key_text],
+ key_width: params[:key_width],
+ order_by: params[:order_by]
+ })
render_badge latest_release
end
diff --git a/app/controllers/projects/blame_controller.rb b/app/controllers/projects/blame_controller.rb
index 01ed5473b41..cfff281604e 100644
--- a/app/controllers/projects/blame_controller.rb
+++ b/app/controllers/projects/blame_controller.rb
@@ -7,7 +7,7 @@ class Projects::BlameController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
feature_category :source_code_management
urgency :low, [:show]
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index f5188e28b81..4eda76f4f21 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -18,7 +18,8 @@ class Projects::BlobController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching, only: [:show]
before_action :require_non_empty_project, except: [:new, :create]
- before_action :authorize_download_code!
+ before_action :authorize_download_code!, except: [:show]
+ before_action :authorize_read_code!, only: [:show]
# We need to assign the blob vars before `authorize_edit_tree!` so we can
# validate access to a specific ref.
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 27969cb1a75..7b01e4db42a 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -6,7 +6,7 @@ class Projects::BranchesController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project, except: :create
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
# Support legacy URLs
diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
index b2b5e096105..37138afc719 100644
--- a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
+++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
@@ -25,7 +25,7 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati
{
date: 'date',
group_name: 'group_name',
- param_type => -> (record) { record.data[param_type] }
+ param_type => ->(record) { record.data[param_type] }
}
).render
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 30d001d0ac5..b781365b3c3 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -5,7 +5,6 @@ class Projects::ClustersController < Clusters::ClustersController
before_action :repository
before_action do
- push_frontend_feature_flag(:prometheus_computed_alerts)
push_frontend_feature_flag(:show_gitlab_agent_feedback, type: :ops)
end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 870320a79d9..583b572d4b1 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :commit
before_action :define_commit_vars, only: [:show, :diff_for_path, :diff_files, :pipelines, :merge_requests]
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index f4125fd0a15..c006d56ae81 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitsController < Projects::ApplicationController
around_action :allow_gitaly_ref_name_caching
before_action :require_non_empty_project
before_action :assign_ref_vars, except: :commits_root
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :validate_ref!, except: :commits_root
before_action :set_commits, except: :commits_root
@@ -28,6 +28,8 @@ class Projects::CommitsController < Projects::ApplicationController
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
+ @ref_type = ref_type
+
respond_to do |format|
format.html
format.atom { render layout: 'xml' }
@@ -73,18 +75,20 @@ class Projects::CommitsController < Projects::ApplicationController
search = permitted_params[:search]
author = permitted_params[:author]
+ # fully_qualified_ref is available in some situations when the use_ref_type_parameter FF is enabled
+ ref = @fully_qualified_ref || @ref
@commits =
if search.present?
- @repository.find_commits_by_message(search, @ref, @path, @limit, @offset)
+ @repository.find_commits_by_message(search, ref, @path, @limit, @offset)
elsif author.present?
- @repository.commits(@ref, author: author, path: @path, limit: @limit, offset: @offset)
+ @repository.commits(ref, author: author, path: @path, limit: @limit, offset: @offset)
else
- @repository.commits(@ref, path: @path, limit: @limit, offset: @offset)
+ @repository.commits(ref, path: @path, limit: @limit, offset: @offset)
end
@commits.each(&:lazy_author) # preload authors
- @commits = @commits.with_markdown_cache.with_latest_pipeline(@ref)
+ @commits = @commits.with_markdown_cache.with_latest_pipeline(ref)
@commits = set_commits_for_rendering(@commits)
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 61308f24412..266edd506d5 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -10,7 +10,7 @@ class Projects::CompareController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
# Defining ivars
before_action :define_diffs, only: [:show, :diff_for_path]
before_action :define_environment, only: [:show]
@@ -95,7 +95,7 @@ class Projects::CompareController < Projects::ApplicationController
target_project = target_projects(source_project).find_by_id(compare_params[:from_project_id])
# Just ignore the field if it points at a non-existent or hidden project
- next source_project unless target_project && can?(current_user, :download_code, target_project)
+ next source_project unless target_project && can?(current_user, :read_code, target_project)
target_project
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index 67f2f85ce65..537fd3854c4 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -14,11 +14,11 @@ class Projects::EnvironmentsController < Projects::ApplicationController
before_action only: [:metrics, :additional_metrics, :metrics_dashboard] do
authorize_metrics_dashboard!
-
- push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
end
+ before_action only: [:show] do
+ push_frontend_feature_flag(:environment_details_vue, @project)
+ end
before_action :authorize_read_environment!, except: [:metrics, :additional_metrics, :metrics_dashboard, :metrics_redirect]
before_action :authorize_create_environment!, only: [:new, :create]
before_action :authorize_stop_environment!, only: [:stop]
diff --git a/app/controllers/projects/find_file_controller.rb b/app/controllers/projects/find_file_controller.rb
index c6bc115e737..b5099d555ae 100644
--- a/app/controllers/projects/find_file_controller.rb
+++ b/app/controllers/projects/find_file_controller.rb
@@ -8,7 +8,7 @@ class Projects::FindFileController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
feature_category :source_code_management
urgency :low, [:show, :list]
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 3208a5076e7..ff3dc71b6cc 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -9,9 +9,9 @@ class Projects::ForksController < Projects::ApplicationController
# Authorize
before_action :disable_query_limiting, only: [:create]
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :authenticate_user!, only: [:new, :create]
- before_action :authorize_fork_project!, only: [:new, :create]
+ before_action :authorize_fork_project!, except: [:index]
before_action :authorize_fork_namespace!, only: [:create]
feature_category :source_code_management
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 6da70b5e157..d072381933a 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -21,11 +21,24 @@ class Projects::GraphsController < Projects::ApplicationController
feature_category :continuous_integration, [:ci]
urgency :low, [:ci]
+ MAX_COMMITS = 6000
+
def show
+ @ref_type = ref_type
+
respond_to do |format|
format.html
format.json do
- fetch_graph
+ commits = @project.repository.commits(ref, limit: MAX_COMMITS, skip_merges: true)
+ log = commits.map do |commit|
+ {
+ author_name: commit.author_name,
+ author_email: commit.author_email,
+ date: commit.committed_date.strftime("%Y-%m-%d")
+ }
+ end
+
+ render json: Gitlab::Json.dump(log)
end
end
end
@@ -50,9 +63,13 @@ class Projects::GraphsController < Projects::ApplicationController
private
+ def ref
+ @fully_qualified_ref || @ref
+ end
+
def get_commits
@commits_limit = 2000
- @commits = @project.repository.commits(@ref, limit: @commits_limit, skip_merges: true)
+ @commits = @project.repository.commits(ref, limit: @commits_limit, skip_merges: true)
@commits_graph = Gitlab::Graphs::Commits.new(@commits)
@commits_per_week_days = @commits_graph.commits_per_week_days
@commits_per_time = @commits_graph.commits_per_time
@@ -76,7 +93,7 @@ class Projects::GraphsController < Projects::ApplicationController
base_params: {
start_date: date_today - report_window,
end_date: date_today,
- ref_path: @project.repository.expand_ref(@ref),
+ ref_path: @project.repository.expand_ref(ref),
param_type: 'coverage'
},
download_path: namespace_project_ci_daily_build_group_report_results_path(
@@ -92,21 +109,6 @@ class Projects::GraphsController < Projects::ApplicationController
}
end
- def fetch_graph
- @commits = @project.repository.commits(@ref, limit: 6000, skip_merges: true)
- @log = []
-
- @commits.each do |commit|
- @log << {
- author_name: commit.author_name,
- author_email: commit.author_email,
- date: commit.committed_date.strftime("%Y-%m-%d")
- }
- end
-
- render json: Gitlab::Json.dump(@log)
- end
-
def tracking_namespace_source
project.namespace
end
diff --git a/app/controllers/projects/incidents_controller.rb b/app/controllers/projects/incidents_controller.rb
index 599505dcb6d..3842a88d15b 100644
--- a/app/controllers/projects/incidents_controller.rb
+++ b/app/controllers/projects/incidents_controller.rb
@@ -8,6 +8,7 @@ class Projects::IncidentsController < Projects::ApplicationController
before_action :load_incident, only: [:show]
before_action do
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?)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index ee845cd001e..631e697dd2f 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -7,6 +7,7 @@ class Projects::IssuesController < Projects::ApplicationController
include IssuableCollections
include IssuesCalendar
include RecordUserLastActivity
+ include ::Observability::ContentSecurityPolicy
ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze
SET_ISSUABLES_INDEX_ONLY_ACTIONS = %i[index calendar service_desk].freeze
@@ -19,7 +20,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :disable_query_limiting, only: [:create_merge_request, :move, :bulk_update]
before_action :check_issues_available!
before_action :issue, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
- before_action :redirect_if_task, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
+ before_action :redirect_if_work_item, unless: ->(c) { ISSUES_EXCEPT_ACTIONS.include?(c.action_name.to_sym) }
after_action :log_issue_show, only: :show
@@ -37,7 +38,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_create_merge_request_from!, only: [:create_merge_request]
before_action :authorize_import_issues!, only: [:import_csv]
- before_action :authorize_download_code!, only: [:related_branches]
+ before_action :authorize_read_code!, only: [:related_branches]
before_action do
push_frontend_feature_flag(:preserve_unchanged_markdown, project)
@@ -55,8 +56,10 @@ class Projects::IssuesController < Projects::ApplicationController
before_action only: :show do
push_frontend_feature_flag(:issue_assignees_widget, project)
push_frontend_feature_flag(:work_items_mvc, project&.group)
+ push_force_frontend_feature_flag(:work_items_mvc, project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:epic_widget_edit_confirmation, project)
+ push_frontend_feature_flag(:use_iid_in_work_items_path, project)
push_force_frontend_feature_flag(:work_items_create_from_markdown, project&.work_items_create_from_markdown_feature_flag_enabled?)
end
@@ -432,8 +435,8 @@ class Projects::IssuesController < Projects::ApplicationController
# Overridden in EE
def create_vulnerability_issue_feedback(issue); end
- def redirect_if_task
- return unless issue.task?
+ def redirect_if_work_item
+ return unless allowed_work_item?
if Feature.enabled?(:use_iid_in_work_items_path, project.group)
redirect_to project_work_items_path(project, issue.iid, params: request.query_parameters.merge(iid_path: true))
@@ -441,6 +444,10 @@ class Projects::IssuesController < Projects::ApplicationController
redirect_to project_work_items_path(project, issue.id, params: request.query_parameters)
end
end
+
+ def allowed_work_item?
+ issue.task?
+ end
end
Projects::IssuesController.prepend_mod_with('Projects::IssuesController')
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 557ac566733..c6d442a6f27 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -20,9 +20,6 @@ class Projects::JobsController < Projects::ApplicationController
before_action :verify_proxy_request!, only: :proxy_websocket_authorize
before_action :push_job_log_jump_to_failures, only: [:show]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:erase]
- before_action do
- push_frontend_feature_flag(:graphql_job_app, project, type: :development)
- end
layout 'project'
diff --git a/app/controllers/projects/merge_requests/creations_controller.rb b/app/controllers/projects/merge_requests/creations_controller.rb
index 93e2298ca98..cba0056ccd5 100644
--- a/app/controllers/projects/merge_requests/creations_controller.rb
+++ b/app/controllers/projects/merge_requests/creations_controller.rb
@@ -4,6 +4,7 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
include DiffForPath
include DiffHelper
include RendersCommits
+ include ::Observability::ContentSecurityPolicy
skip_before_action :merge_request
before_action :authorize_create_merge_request_from!
@@ -19,6 +20,10 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
:branch_to
]
+ before_action do
+ push_frontend_feature_flag(:mr_compare_dropdowns, project)
+ end
+
def new
define_new_vars
end
@@ -89,6 +94,14 @@ class Projects::MergeRequests::CreationsController < Projects::MergeRequests::Ap
render layout: false
end
+ def target_projects
+ projects = MergeRequestTargetProjectFinder
+ .new(current_user: current_user, source_project: @project, project_feature: :repository)
+ .execute(include_routes: true).limit(20).search(params[:search])
+
+ render json: ProjectSerializer.new.represent(projects)
+ end
+
private
def build_merge_request
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index c88dbc70ed5..83377f67723 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -60,17 +60,11 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
options[:merge_conflicts_in_diff]
]
- if Feature.enabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
- return
- end
+ return unless stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
diffs.unfold_diff_files(unfoldable_positions)
diffs.write_cache
- if Feature.disabled?(:check_etags_diffs_batch_before_write_cache, merge_request.project) && !stale?(etag: [cache_context + diff_options_hash.fetch(:paths, []), diffs])
- return
- end
-
render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options)
end
# rubocop: enable Metrics/AbcSize
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 4ba79d43f27..3ab1f7d1d32 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -11,10 +11,11 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
include SourcegraphDecorator
include DiffHelper
include Gitlab::Cache::Helpers
+ include ::Observability::ContentSecurityPolicy
prepend_before_action(only: [:index]) { authenticate_sessionless_user!(:rss) }
skip_before_action :merge_request, only: [:index, :bulk_update, :export_csv]
- before_action :apply_diff_view_cookie!, only: [:show]
+ before_action :apply_diff_view_cookie!, only: [:show, :diffs]
before_action :disable_query_limiting, only: [:assign_related_issues, :update]
before_action :authorize_update_issuable!, only: [:close, :edit, :update, :remove_wip, :sort]
before_action :authorize_read_actual_head_pipeline!, only: [
@@ -30,7 +31,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
before_action :authenticate_user!, only: [:assign_related_issues]
before_action :check_user_can_push_to_source_branch!, only: [:rebase]
- before_action only: [:show] do
+ before_action only: [:show, :diffs] do
push_frontend_feature_flag(:core_security_mr_widget_counts, project)
push_frontend_feature_flag(:issue_assignees_widget, @project)
push_frontend_feature_flag(:refactor_security_extension, @project)
@@ -40,21 +41,22 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:mr_review_submit_comment, project)
push_frontend_feature_flag(:mr_experience_survey, project)
push_frontend_feature_flag(:realtime_reviewers, project)
+ push_frontend_feature_flag(:realtime_mr_status_change, project)
end
before_action do
push_frontend_feature_flag(:permit_all_shared_groups_for_approval, @project)
end
- around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :discussions]
+ around_action :allow_gitaly_ref_name_caching, only: [:index, :show, :diffs, :discussions]
- after_action :log_merge_request_show, only: [:show]
+ after_action :log_merge_request_show, only: [:show, :diffs]
feature_category :code_review, [
:assign_related_issues, :bulk_update, :cancel_auto_merge,
:commit_change_content, :commits, :context_commits, :destroy,
:discussions, :edit, :index, :merge, :rebase, :remove_wip,
- :show, :toggle_award_emoji, :toggle_subscription, :update
+ :show, :diffs, :toggle_award_emoji, :toggle_subscription, :update
]
feature_category :code_testing, [:test_reports, :coverage_reports]
@@ -67,6 +69,7 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
urgency :low, [
:index,
:show,
+ :diffs,
:commits,
:bulk_update,
:edit,
@@ -100,74 +103,13 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
end
end
- # rubocop:disable Metrics/AbcSize
def show
- close_merge_request_if_no_source_project
- @merge_request.check_mergeability(async: true)
-
- respond_to do |format|
- format.html do
- # use next to appease Rubocop
- next render('invalid') if target_branch_missing?
-
- preload_assignees_for_render(@merge_request)
-
- # Build a note object for comment form
- @note = @project.notes.new(noteable: @merge_request)
-
- @noteable = @merge_request
- @commits_count = @merge_request.commits_count + @merge_request.context_commits_count
- @diffs_count = get_diffs_count
- @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
- @current_user_data = Gitlab::Json.dump(UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestCurrentUserEntity))
- @show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
- @file_by_file_default = current_user&.view_diffs_file_by_file
- @coverage_path = coverage_reports_project_merge_request_path(@project, @merge_request, format: :json) if @merge_request.has_coverage_reports?
- @update_current_user_path = expose_path(api_v4_user_preferences_path)
- @endpoint_metadata_url = endpoint_metadata_url(@project, @merge_request)
- @endpoint_diff_batch_url = endpoint_diff_batch_url(@project, @merge_request)
-
- set_pipeline_variables
-
- @number_of_pipelines = @pipelines.size
-
- render
- end
-
- format.json do
- Gitlab::PollingInterval.set_header(response, interval: 10_000)
-
- if params[:serializer] == 'sidebar_extras'
- cache_context = [
- params[:serializer],
- current_user&.cache_key,
- @merge_request.merge_request_assignees.map(&:cache_key),
- @merge_request.merge_request_reviewers.map(&:cache_key)
- ]
-
- render_cached(@merge_request,
- with: serializer,
- cache_context: -> (_) { [Digest::SHA256.hexdigest(cache_context.to_s)] },
- serializer: params[:serializer])
- else
- render json: serializer.represent(@merge_request, serializer: params[:serializer])
- end
- end
-
- format.patch do
- break render_404 unless @merge_request.diff_refs
-
- send_git_patch @project.repository, @merge_request.diff_refs
- end
-
- format.diff do
- break render_404 unless @merge_request.diff_refs
+ show_merge_request
+ end
- send_git_diff @project.repository, @merge_request.diff_refs
- end
- end
+ def diffs
+ show_merge_request
end
- # rubocop:enable Metrics/AbcSize
def commits
# Get context commits from repository
@@ -412,6 +354,77 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
private
+ def show_merge_request
+ close_merge_request_if_no_source_project
+ @merge_request.check_mergeability(async: true)
+
+ respond_to do |format|
+ format.html do
+ # use next to appease Rubocop
+ next render('invalid') if target_branch_missing?
+
+ render_html_page
+ end
+
+ format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 10_000)
+
+ if params[:serializer] == 'sidebar_extras'
+ cache_context = [
+ params[:serializer],
+ current_user&.cache_key,
+ @merge_request.merge_request_assignees.map(&:cache_key),
+ @merge_request.merge_request_reviewers.map(&:cache_key)
+ ]
+
+ render_cached(@merge_request,
+ with: serializer,
+ cache_context: ->(_) { [Digest::SHA256.hexdigest(cache_context.to_s)] },
+ serializer: params[:serializer])
+ else
+ render json: serializer.represent(@merge_request, serializer: params[:serializer])
+ end
+ end
+
+ format.patch do
+ break render_404 unless @merge_request.diff_refs
+
+ send_git_patch @project.repository, @merge_request.diff_refs
+ end
+
+ format.diff do
+ break render_404 unless @merge_request.diff_refs
+
+ send_git_diff @project.repository, @merge_request.diff_refs
+ end
+ end
+ end
+
+ def render_html_page
+ preload_assignees_for_render(@merge_request)
+
+ # Build a note object for comment form
+ @note = @project.notes.new(noteable: @merge_request)
+
+ @noteable = @merge_request
+ @commits_count = @merge_request.commits_count + @merge_request.context_commits_count
+ @diffs_count = get_diffs_count
+ @issuable_sidebar = serializer.represent(@merge_request, serializer: 'sidebar')
+ @current_user_data = Gitlab::Json.dump(UserSerializer.new(project: @project).represent(current_user, {}, MergeRequestCurrentUserEntity))
+ @show_whitespace_default = current_user.nil? || current_user.show_whitespace_in_diffs
+ @file_by_file_default = current_user&.view_diffs_file_by_file
+ @coverage_path = coverage_reports_project_merge_request_path(@project, @merge_request, format: :json) if @merge_request.has_coverage_reports?
+ @update_current_user_path = expose_path(api_v4_user_preferences_path)
+ @endpoint_metadata_url = endpoint_metadata_url(@project, @merge_request)
+ @endpoint_diff_batch_url = endpoint_diff_batch_url(@project, @merge_request)
+
+ set_pipeline_variables
+
+ @number_of_pipelines = @pipelines.size
+
+ render
+ end
+
def get_diffs_count
if show_only_context_commits?
@merge_request.context_commits_diff.raw_diffs.size
diff --git a/app/controllers/projects/metrics_dashboard_controller.rb b/app/controllers/projects/metrics_dashboard_controller.rb
index b78ee6ca917..08757d11912 100644
--- a/app/controllers/projects/metrics_dashboard_controller.rb
+++ b/app/controllers/projects/metrics_dashboard_controller.rb
@@ -9,10 +9,6 @@ module Projects
include Gitlab::Utils::StrongMemoize
before_action :authorize_metrics_dashboard!
- before_action do
- push_frontend_feature_flag(:prometheus_computed_alerts)
- push_frontend_feature_flag(:disable_metric_dashboard_refresh_rate)
- end
feature_category :metrics
urgency :low
diff --git a/app/controllers/projects/ml/candidates_controller.rb b/app/controllers/projects/ml/candidates_controller.rb
new file mode 100644
index 00000000000..b702edb858e
--- /dev/null
+++ b/app/controllers/projects/ml/candidates_controller.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Projects
+ module Ml
+ class CandidatesController < ApplicationController
+ before_action :check_feature_flag
+
+ feature_category :mlops
+
+ def show
+ @candidate = ::Ml::Candidate.with_project_id_and_iid(@project.id, params['iid'])
+
+ render_404 unless @candidate.present?
+ end
+
+ private
+
+ def check_feature_flag
+ render_404 unless Feature.enabled?(:ml_experiment_tracking, @project)
+ end
+ end
+ end
+end
diff --git a/app/controllers/projects/ml/experiments_controller.rb b/app/controllers/projects/ml/experiments_controller.rb
index 749586791ac..c82a959d612 100644
--- a/app/controllers/projects/ml/experiments_controller.rb
+++ b/app/controllers/projects/ml/experiments_controller.rb
@@ -3,7 +3,6 @@
module Projects
module Ml
class ExperimentsController < ::Projects::ApplicationController
- include Projects::Ml::ExperimentsHelper
before_action :check_feature_flag
feature_category :mlops
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 84ac9fb01fd..aa0838752e2 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -6,7 +6,7 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :assign_options
before_action :assign_commit
@@ -14,7 +14,13 @@ class Projects::NetworkController < Projects::ApplicationController
urgency :low, [:show]
def show
- @url = project_network_path(@project, @ref, @options.merge(format: :json))
+ @url = if Feature.enabled?(:use_ref_type_parameter, @project)
+ project_network_path(@project, @ref, @options.merge(format: :json, ref_type: ref_type))
+ else
+ project_network_path(@project, @ref, @options.merge(format: :json))
+ end
+
+ @ref_type = ref_type
@commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
respond_to do |format|
diff --git a/app/controllers/projects/performance_monitoring/dashboards_controller.rb b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
index 8acbc17aef3..d043f8d0b9f 100644
--- a/app/controllers/projects/performance_monitoring/dashboards_controller.rb
+++ b/app/controllers/projects/performance_monitoring/dashboards_controller.rb
@@ -70,7 +70,7 @@ module Projects
end
def validate_required_params!
- params.require(%i(branch file_name dashboard commit_message))
+ params.require(%i[branch file_name dashboard commit_message])
end
def redirect_safe_branch_name
@@ -78,7 +78,7 @@ module Projects
end
def dashboard_params
- params.permit(%i(branch file_name dashboard commit_message)).to_h
+ params.permit(%i[branch file_name dashboard commit_message]).to_h
end
def file_content_params
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 7d1a75ae449..db77127cb0a 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -24,11 +24,6 @@ class Projects::PipelinesController < Projects::ApplicationController
before_action :ensure_pipeline, only: [:show, :downloadable_artifacts]
before_action :reject_if_build_artifacts_size_refreshing!, only: [:destroy]
- before_action do
- push_frontend_feature_flag(:pipeline_tabs_vue, @project)
- push_frontend_feature_flag(:run_pipeline_graphql, @project)
- end
-
# Will be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/225596
before_action :redirect_for_legacy_scope_filter, only: [:index], if: -> { request.format.html? }
diff --git a/app/controllers/projects/protected_branches_controller.rb b/app/controllers/projects/protected_branches_controller.rb
index 8c70ef446a2..baa4607dcb6 100644
--- a/app/controllers/projects/protected_branches_controller.rb
+++ b/app/controllers/projects/protected_branches_controller.rb
@@ -1,6 +1,12 @@
# frozen_string_literal: true
class Projects::ProtectedBranchesController < Projects::ProtectedRefsController
+ def show
+ super
+
+ render 'protected_branches/show'
+ end
+
protected
def project_refs
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 9707b70f26f..924de0ee7ea 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -12,7 +12,7 @@ class Projects::RawController < Projects::ApplicationController
before_action :set_ref_and_path
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :check_show_rate_limit!, only: [:show], unless: :external_storage_request?
before_action :redirect_to_external_storage, only: :show, if: :static_objects_external_storage_enabled?
@@ -21,7 +21,7 @@ class Projects::RawController < Projects::ApplicationController
def show
@blob = @repository.blob_at(@ref, @path, limit: Gitlab::Git::Blob::LFS_POINTER_MAX_SIZE)
- send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: Guest.can?(:download_code, @project))
+ send_blob(@repository, @blob, inline: (params[:inline] != 'false'), allow_caching: Guest.can?(:read_code, @project))
end
private
diff --git a/app/controllers/projects/refs_controller.rb b/app/controllers/projects/refs_controller.rb
index 72af3280a39..8ac6d872aae 100644
--- a/app/controllers/projects/refs_controller.rb
+++ b/app/controllers/projects/refs_controller.rb
@@ -9,7 +9,7 @@ class Projects::RefsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :validate_ref_id
before_action :assign_ref_vars
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
feature_category :source_code_management
urgency :low, [:switch, :logs_tree]
@@ -24,9 +24,17 @@ class Projects::RefsController < Projects::ApplicationController
when "blob"
project_blob_path(@project, @id)
when "graph"
- project_network_path(@project, @id, @options)
+ if Feature.enabled?(:use_ref_type_parameter, @project)
+ project_network_path(@project, @id, ref_type: ref_type)
+ else
+ project_network_path(@project, @id, @options)
+ end
when "graphs"
- project_graph_path(@project, @id)
+ if Feature.enabled?(:use_ref_type_parameter, @project)
+ project_graph_path(@project, @id, ref_type: ref_type)
+ else
+ project_graph_path(@project, @id)
+ end
when "find_file"
project_find_file_path(@project, @id)
when "graphs_commits"
@@ -34,7 +42,11 @@ class Projects::RefsController < Projects::ApplicationController
when "badges"
project_settings_ci_cd_path(@project, ref: @id)
else
- project_commits_path(@project, @id)
+ if Feature.enabled?(:use_ref_type_parameter, @project)
+ project_commits_path(@project, @id, ref_type: ref_type)
+ else
+ project_commits_path(@project, @id)
+ end
end
redirect_to new_path
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index ffe95bf4fee..6c663c4694a 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -23,10 +23,6 @@ module Projects
def destroy
image.delete_scheduled!
- unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker)
- DeleteContainerRepositoryWorker.perform_async(current_user.id, image.id) # rubocop:disable CodeReuse/Worker
- end
-
track_package_event(:delete_repository, :container)
respond_to do |format|
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index 5946c43b134..6f896244acb 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -11,7 +11,7 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- return head(403) unless can?(current_user, :assign_runner, @runner)
+ return head(:forbidden) unless can?(current_user, :assign_runner, @runner)
path = project_runners_path(project)
diff --git a/app/controllers/projects/service_desk_controller.rb b/app/controllers/projects/service_desk_controller.rb
index aa0e70121df..8f576b8d72b 100644
--- a/app/controllers/projects/service_desk_controller.rb
+++ b/app/controllers/projects/service_desk_controller.rb
@@ -29,7 +29,7 @@ class Projects::ServiceDeskController < Projects::ApplicationController
end
def allowed_update_attributes
- %i(issue_template_key outgoing_name project_key)
+ %i[issue_template_key outgoing_name project_key]
end
def service_desk_attributes
diff --git a/app/controllers/projects/service_ping_controller.rb b/app/controllers/projects/service_ping_controller.rb
index 43c249afd8e..cfc322b47e7 100644
--- a/app/controllers/projects/service_ping_controller.rb
+++ b/app/controllers/projects/service_ping_controller.rb
@@ -10,7 +10,7 @@ class Projects::ServicePingController < Projects::ApplicationController
Gitlab::UsageDataCounters::WebIdeCounter.increment_previews_count
- head(200)
+ head(:ok)
end
def web_ide_clientside_preview_success
@@ -20,12 +20,12 @@ class Projects::ServicePingController < Projects::ApplicationController
Gitlab::UsageDataCounters::EditorUniqueCounter.track_live_preview_edit_action(author: current_user,
project: project)
- head(200)
+ head(:ok)
end
def web_ide_pipelines_count
Gitlab::UsageDataCounters::WebIdeCounter.increment_pipelines_count
- head(200)
+ head(:ok)
end
end
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 8aef1c3d24d..cf07de4dc29 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -18,6 +18,9 @@ module Projects
urgency :low
def show
+ @entity = :project
+ @variable_limit = ::Plan.default.actual_limits.project_ci_variables
+
if Feature.enabled?(:ci_pipeline_triggers_settings_vue_ui, @project)
triggers = ::Ci::TriggerSerializer.new.represent(
@project.triggers, current_user: current_user, project: @project
@@ -122,11 +125,13 @@ module Projects
.page(params[:specific_page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
.with_tags
- @shared_runners = ::Ci::Runner.instance_type.active.with_tags
-
- @shared_runners_count = @shared_runners.count(:all)
+ active_shared_runners = ::Ci::Runner.instance_type.active
+ @shared_runners_count = active_shared_runners.count
+ @shared_runners = active_shared_runners.page(params[:shared_runners_page]).per(NUMBER_OF_RUNNERS_PER_PAGE).with_tags
- @group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id).with_tags
+ parent_group_runners = ::Ci::Runner.belonging_to_parent_group_of_project(@project.id)
+ @group_runners_count = parent_group_runners.count
+ @group_runners = parent_group_runners.page(params[:group_runners_page]).per(NUMBER_OF_RUNNERS_PER_PAGE).with_tags
end
def define_ci_variables
diff --git a/app/controllers/projects/settings/integrations_controller.rb b/app/controllers/projects/settings/integrations_controller.rb
index 2bbcd9fe20c..16c1373df2b 100644
--- a/app/controllers/projects/settings/integrations_controller.rb
+++ b/app/controllers/projects/settings/integrations_controller.rb
@@ -79,7 +79,7 @@ module Projects
return {
error: true,
message: _('Validations failed.'),
- service_response: integration.errors.full_messages.join(','),
+ service_response: integration.errors.full_messages.join(', '),
test_failed: false
}
end
@@ -90,7 +90,7 @@ module Projects
return {
error: true,
message: s_('Integrations|Connection failed. Check your integration settings.'),
- service_response: result[:message].to_s,
+ service_response: result[:result].to_s,
test_failed: true
}
end
diff --git a/app/controllers/projects/settings/repository_controller.rb b/app/controllers/projects/settings/repository_controller.rb
index 90988645d3a..6d099aa8b3d 100644
--- a/app/controllers/projects/settings/repository_controller.rb
+++ b/app/controllers/projects/settings/repository_controller.rb
@@ -95,6 +95,14 @@ module Projects
@protected_tags_count = @protected_tags.reduce(0) { |sum, tag| sum + tag.matching(@project.repository.tag_names).size }
+ if Feature.enabled?(:group_protected_branches)
+ @protected_group_branches = if @project.root_namespace.is_a?(Group)
+ @project.root_namespace.protected_branches.order(:name).page(params[:page])
+ else
+ []
+ end
+ end
+
load_gon_index
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/snippets/application_controller.rb b/app/controllers/projects/snippets/application_controller.rb
index 8ee12bf3795..b8faf464531 100644
--- a/app/controllers/projects/snippets/application_controller.rb
+++ b/app/controllers/projects/snippets/application_controller.rb
@@ -4,7 +4,7 @@ class Projects::Snippets::ApplicationController < Projects::ApplicationControlle
include FindSnippet
include SnippetAuthorizations
- feature_category :snippets
+ feature_category :source_code_management
private
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 847b1baca10..3c1735c728c 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -7,7 +7,7 @@ class Projects::TagsController < Projects::ApplicationController
# Authorize
before_action :require_non_empty_project
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :authorize_admin_tag!, only: [:new, :create, :destroy]
feature_category :source_code_management
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index fea2689db14..737a6290431 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -13,11 +13,10 @@ class Projects::TreeController < Projects::ApplicationController
before_action :require_non_empty_project, except: [:new, :create]
before_action :assign_ref_vars
before_action :assign_dir_vars, only: [:create_dir]
- before_action :authorize_download_code!
+ before_action :authorize_read_code!
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
- push_frontend_feature_flag(:lazy_load_commits, @project)
push_frontend_feature_flag(:highlight_js, @project)
push_frontend_feature_flag(:file_line_blame, @project)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index a8f062bd7c1..a83ccccbeae 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -47,6 +47,6 @@ class Projects::VariablesController < Projects::ApplicationController
end
def variable_params_attributes
- %i[id variable_type key secret_value protected masked environment_scope _destroy]
+ %i[id variable_type key secret_value protected masked raw environment_scope _destroy]
end
end
diff --git a/app/controllers/projects/work_items_controller.rb b/app/controllers/projects/work_items_controller.rb
index a7e59a28fb7..a118c6986f7 100644
--- a/app/controllers/projects/work_items_controller.rb
+++ b/app/controllers/projects/work_items_controller.rb
@@ -3,6 +3,7 @@
class Projects::WorkItemsController < Projects::ApplicationController
before_action do
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_frontend_feature_flag(:use_iid_in_work_items_path, project)
end
@@ -10,3 +11,5 @@ class Projects::WorkItemsController < Projects::ApplicationController
feature_category :team_planning
urgency :low
end
+
+Projects::WorkItemsController.prepend_mod
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a5dacbf7f2f..886819fe778 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -26,7 +26,7 @@ class ProjectsController < Projects::ApplicationController
before_action :verify_git_import_enabled, only: [:create]
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
- before_action :authorize_download_code!, only: [:refs]
+ before_action :authorize_read_code!, only: [:refs]
# Authorize
before_action :authorize_admin_project!, only: [:edit, :update, :housekeeping, :download_export, :export, :remove_export, :generate_new_export]
@@ -37,21 +37,17 @@ class ProjectsController < Projects::ApplicationController
before_action :check_export_rate_limit!, only: [:export, :download_export, :generate_new_export]
before_action do
- push_frontend_feature_flag(:lazy_load_commits, @project)
push_frontend_feature_flag(:highlight_js, @project)
push_frontend_feature_flag(:file_line_blame, @project)
push_frontend_feature_flag(:increase_page_size_exponentially, @project)
push_licensed_feature(:file_locks) if @project.present? && @project.licensed_feature_available?(:file_locks)
push_licensed_feature(:security_orchestration_policies) if @project.present? && @project.licensed_feature_available?(:security_orchestration_policies)
push_force_frontend_feature_flag(:work_items, @project&.work_items_feature_flag_enabled?)
+ push_force_frontend_feature_flag(:work_items_mvc, @project&.work_items_mvc_feature_flag_enabled?)
push_force_frontend_feature_flag(:work_items_mvc_2, @project&.work_items_mvc_2_feature_flag_enabled?)
push_frontend_feature_flag(:package_registry_access_level)
end
- before_action only: :edit do
- push_frontend_feature_flag(:split_operations_visibility_permissions, @project)
- end
-
layout :determine_layout
feature_category :projects, [
@@ -369,7 +365,7 @@ class ProjectsController < Projects::ApplicationController
def render_landing_page
Gitlab::Tracking.event('project_overview', 'render', user: current_user, project: @project.project)
- if can?(current_user, :download_code, @project)
+ if can?(current_user, :read_code, @project)
return render 'projects/no_repo' unless @project.repository_exists?
render 'projects/empty' if @project.empty_repo?
@@ -433,17 +429,11 @@ class ProjectsController < Projects::ApplicationController
security_and_compliance_access_level
container_registry_access_level
releases_access_level
- ] + operations_feature_attributes
- end
-
- def operations_feature_attributes
- if Feature.enabled?(:split_operations_visibility_permissions, project)
- %i[
- environments_access_level feature_flags_access_level monitor_access_level infrastructure_access_level
- ]
- else
- %i[operations_access_level]
- end
+ environments_access_level
+ feature_flags_access_level
+ monitor_access_level
+ infrastructure_access_level
+ ]
end
def project_setting_attributes
@@ -520,14 +510,6 @@ class ProjectsController < Projects::ApplicationController
false
end
- def project_view_files?
- if current_user
- current_user.project_view == 'files'
- else
- project_view_files_allowed?
- end
- end
-
# Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
# This way we avoid to access the repository.ref_names.
@@ -540,10 +522,6 @@ class ProjectsController < Projects::ApplicationController
project.repository.root_ref
end
- def project_view_files_allowed?
- !project.empty_repo? && can?(current_user, :download_code, project)
- end
-
def build_canonical_path(project)
params[:namespace_id] = project.namespace.to_param
params[:id] = project.to_param
diff --git a/app/controllers/registrations/welcome_controller.rb b/app/controllers/registrations/welcome_controller.rb
index a49b82319da..4a42632a980 100644
--- a/app/controllers/registrations/welcome_controller.rb
+++ b/app/controllers/registrations/welcome_controller.rb
@@ -14,12 +14,16 @@ module Registrations
def show
return redirect_to path_for_signed_in_user(current_user) if completed_welcome_step?
+
+ track_event('render')
end
def update
result = ::Users::SignupService.new(current_user, update_params).execute
if result[:status] == :success
+ track_event('successfully_submitted_form')
+
return redirect_to issues_dashboard_path(assignee_username: current_user.username) if show_tasks_to_be_done?
return redirect_to update_success_path if show_signup_onboarding?
@@ -86,6 +90,10 @@ module Registrations
# overridden in EE
def update_success_path
end
+
+ # overridden in EE
+ def track_event(category)
+ end
end
end
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index 995303a631a..11f9f1cf0c6 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -15,10 +15,10 @@ class RegistrationsController < Devise::RegistrationsController
layout 'devise'
prepend_before_action :check_captcha, only: :create
+ before_action :ensure_first_name_and_last_name_not_empty, only: :create
before_action :ensure_destroy_prerequisites_met, only: [:destroy]
before_action :init_preferred_language, only: :new
before_action :load_recaptcha, only: :new
- before_action :set_invite_params, only: :new
before_action only: [:create] do
check_rate_limit!(:user_sign_up, scope: request.ip)
end
@@ -32,11 +32,11 @@ class RegistrationsController < Devise::RegistrationsController
def new
@resource = build_resource
+ set_invite_params
end
def create
- set_user_state
- set_custom_confirmation_token
+ set_resource_fields
super do |new_user|
accept_pending_invitations if new_user.persisted?
@@ -111,8 +111,11 @@ class RegistrationsController < Devise::RegistrationsController
super
end
+ # overridden by EE module
def after_request_hook(user)
- # overridden by EE module
+ return unless user.persisted?
+
+ Gitlab::Tracking.event(self.class.name, 'successfully_submitted_form', user: user)
end
def after_sign_up_path_for(user)
@@ -132,6 +135,7 @@ class RegistrationsController < Devise::RegistrationsController
return identity_verification_redirect_path if custom_confirmation_enabled?
+ Gitlab::Tracking.event(self.class.name, 'render', user: resource)
users_almost_there_path(email: resource.email)
end
@@ -172,6 +176,21 @@ class RegistrationsController < Devise::RegistrationsController
render action: 'new'
end
+ def ensure_first_name_and_last_name_not_empty
+ # The key here will be affected by feature flag 'arkose_labs_signup_challenge'
+ # When flag is disabled, the key will be 'user' because #check_captcha will remove 'new_' prefix
+ # When flag is enabled, #check_captcha will be skipped, so the key will have 'new_' prefix
+ first_name = params.dig(resource_name, :first_name) || params.dig("new_#{resource_name}", :first_name)
+ last_name = params.dig(resource_name, :last_name) || params.dig("new_#{resource_name}", :last_name)
+
+ return if first_name.present? && last_name.present?
+
+ resource.errors.add(_('First name'), _("cannot be blank")) if first_name.blank?
+ resource.errors.add(_('Last name'), _("cannot be blank")) if last_name.blank?
+
+ render action: 'new'
+ end
+
def pending_approval?
return false unless Gitlab::CurrentSettings.require_admin_approval_after_user_signup
@@ -211,18 +230,22 @@ class RegistrationsController < Devise::RegistrationsController
Gitlab::Recaptcha.load_configurations!
end
- def set_user_state
+ # overridden by EE module
+ def set_resource_fields
return unless set_blocked_pending_approval?
resource.state = User::BLOCKED_PENDING_APPROVAL_STATE
end
+ # overridden by EE module
def set_blocked_pending_approval?
Gitlab::CurrentSettings.require_admin_approval_after_user_signup
end
def set_invite_params
- @invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
+ if resource.email.blank? && params[:invite_email].present?
+ resource.email = @invite_email = ActionController::Base.helpers.sanitize(params[:invite_email])
+ end
end
def after_pending_invitations_hook
@@ -251,10 +274,6 @@ class RegistrationsController < Devise::RegistrationsController
# overridden by EE module
end
- def set_custom_confirmation_token
- # overridden by EE module
- end
-
def send_custom_confirmation_instructions
# overridden by EE module
end
diff --git a/app/controllers/repositories/lfs_locks_api_controller.rb b/app/controllers/repositories/lfs_locks_api_controller.rb
index f36126d67ff..ea858d63236 100644
--- a/app/controllers/repositories/lfs_locks_api_controller.rb
+++ b/app/controllers/repositories/lfs_locks_api_controller.rb
@@ -54,9 +54,9 @@ module Repositories
def error_payload(message, custom_attrs = {})
custom_attrs.merge({
- message: message,
- documentation_url: help_url
- })
+ message: message,
+ documentation_url: help_url
+ })
end
def split_by_owner(locks)
@@ -72,7 +72,7 @@ module Repositories
end
def upload_request?
- %w(create unlock verify).include?(params[:action])
+ %w[create unlock verify].include?(params[:action])
end
def lfs_params
diff --git a/app/controllers/repositories/lfs_storage_controller.rb b/app/controllers/repositories/lfs_storage_controller.rb
index d54b51b463a..22f1a81b95b 100644
--- a/app/controllers/repositories/lfs_storage_controller.rb
+++ b/app/controllers/repositories/lfs_storage_controller.rb
@@ -49,7 +49,7 @@ module Repositories
validate_uploaded_file!
if store_file!(oid, size)
- head 200, content_type: LfsRequest::CONTENT_TYPE
+ head :ok, content_type: LfsRequest::CONTENT_TYPE
else
render plain: 'Unprocessable entity', status: :unprocessable_entity
end
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 5351e3e9e77..66968b34380 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -8,6 +8,7 @@ class SearchController < ApplicationController
include SearchRateLimitable
RESCUE_FROM_TIMEOUT_ACTIONS = [:count, :show, :autocomplete, :aggregations].freeze
+ CODE_SEARCH_LITERALS = %w[blob: extension: path: filename:].freeze
track_custom_event :show,
name: 'i_search_total',
@@ -32,7 +33,10 @@ class SearchController < ApplicationController
before_action only: :show do
push_frontend_feature_flag(:search_page_vertical_nav, current_user)
end
-
+ before_action only: :show do
+ update_scope_for_code_search
+ end
+ before_action :elasticsearch_in_use, only: :show
rescue_from ActiveRecord::QueryCanceled, with: :render_timeout
layout 'search'
@@ -43,6 +47,7 @@ class SearchController < ApplicationController
def show
@project = search_service.project
@group = search_service.group
+ @search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
return unless search_term_valid?
@@ -51,15 +56,11 @@ class SearchController < ApplicationController
@search_term = params[:search]
@sort = params[:sort] || default_sort
- @search_service = Gitlab::View::Presenter::Factory.new(search_service, current_user: current_user).fabricate!
-
@search_level = @search_service.level
@search_type = search_type
@global_search_duration_s = Benchmark.realtime do
@scope = @search_service.scope
- @without_count = @search_service.without_count?
- @show_snippets = @search_service.show_snippets?
@search_results = @search_service.search_results
@search_objects = @search_service.search_objects
@search_highlight = @search_service.search_highlight
@@ -118,8 +119,22 @@ class SearchController < ApplicationController
def opensearch
end
+ def elasticsearch_in_use
+ search_service.respond_to?(:use_elasticsearch?) && search_service.use_elasticsearch?
+ end
+ strong_memoize_attr :elasticsearch_in_use
+
private
+ def update_scope_for_code_search
+ return if params[:scope] == 'blobs'
+ return unless params[:search].present?
+
+ if CODE_SEARCH_LITERALS.any? { |literal| literal.in? params[:search] }
+ redirect_to search_path(safe_params.except(:controller, :action).merge(scope: 'blobs'))
+ end
+ end
+
# overridden in EE
def default_sort
'created_desc'
diff --git a/app/controllers/snippets/application_controller.rb b/app/controllers/snippets/application_controller.rb
index f259f4569ef..64adc4e6611 100644
--- a/app/controllers/snippets/application_controller.rb
+++ b/app/controllers/snippets/application_controller.rb
@@ -4,7 +4,7 @@ class Snippets::ApplicationController < ApplicationController
include FindSnippet
include SnippetAuthorizations
- feature_category :snippets
+ feature_category :source_code_management
private
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index 8a4e8edbf3c..9e23eef4178 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -8,7 +8,7 @@ class Snippets::NotesController < ApplicationController
before_action :authorize_read_snippet!, only: [:show, :index]
before_action :authorize_create_note!, only: [:create]
- feature_category :snippets
+ feature_category :source_code_management
private
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index 0f03333d793..f23e513e419 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -31,8 +31,7 @@ class UsersController < ApplicationController
:followers, :following, :calendar, :calendar_activities,
:exists, :activity, :follow, :unfollow, :ssh_keys]
- feature_category :snippets, [:snippets]
- feature_category :source_code_management, [:gpg_keys]
+ feature_category :source_code_management, [:snippets, :gpg_keys]
# TODO: Set higher urgency after resolving https://gitlab.com/gitlab-org/gitlab/-/issues/357914
urgency :low, [:show, :calendar_activities, :contributed, :activity, :projects, :groups, :calendar, :snippets]
diff --git a/app/controllers/web_ide/remote_ide_controller.rb b/app/controllers/web_ide/remote_ide_controller.rb
new file mode 100644
index 00000000000..fe70e78b1e5
--- /dev/null
+++ b/app/controllers/web_ide/remote_ide_controller.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module WebIde
+ class RemoteIdeController < ApplicationController
+ include VSCodeCDNCSP
+
+ rescue_from URI::InvalidComponentError, with: :render_404
+
+ before_action :allow_remote_ide_content_security_policy
+
+ feature_category :remote_development
+
+ urgency :low
+
+ def index
+ return render_404 unless Feature.enabled?(:vscode_web_ide, current_user)
+
+ render layout: 'fullscreen', locals: { minimal: true, data: root_element_data }
+ end
+
+ private
+
+ def allow_remote_ide_content_security_policy
+ return if request.content_security_policy.directives.blank?
+
+ default_src = Array(request.content_security_policy.directives['default-src'] || [])
+
+ request.content_security_policy.directives['connect-src'] ||= default_src
+ request.content_security_policy.directives['connect-src'].concat(connect_src_urls)
+ end
+
+ def connect_src_urls
+ # It's okay if "port" is null
+ host, port = params.require(:remote_host).split(':')
+
+ # This could throw URI::InvalidComponentError. We go ahead and let it throw
+ # and let the controller recover with a bad_request response
+ %w[ws wss http https].map { |scheme| URI::Generic.build(scheme: scheme, host: host, port: port).to_s }
+ end
+
+ def root_element_data
+ {
+ connection_token: params.fetch(:connection_token, ''),
+ remote_host: params.require(:remote_host),
+ remote_path: params.fetch(:remote_path, ''),
+ return_url: params.fetch(:return_url, ''),
+ csp_nonce: content_security_policy_nonce
+ }
+ end
+ end
+end
diff --git a/app/events/gitlab_subscriptions/renewed_event.rb b/app/events/gitlab_subscriptions/renewed_event.rb
new file mode 100644
index 00000000000..02bf8ec7095
--- /dev/null
+++ b/app/events/gitlab_subscriptions/renewed_event.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module GitlabSubscriptions
+ class RenewedEvent < Gitlab::EventStore::Event
+ def schema
+ {
+ 'type' => 'object',
+ 'required' => %w[
+ namespace_id
+ ],
+ 'properties' => {
+ 'namespace_id' => { 'type' => 'integer' }
+ }
+ }
+ end
+ end
+end
diff --git a/app/experiments/concerns/project_commit_count.rb b/app/experiments/concerns/project_commit_count.rb
index 706a1a24640..3f08538c21f 100644
--- a/app/experiments/concerns/project_commit_count.rb
+++ b/app/experiments/concerns/project_commit_count.rb
@@ -10,9 +10,9 @@ module ProjectCommitCount
return default_count unless root_ref
Gitlab::GitalyClient::CommitService.new(raw_repo).commit_count(root_ref, {
- all: true, # include all branches
- max_count: max_count # limit as an optimization
- })
+ all: true, # include all branches
+ max_count: max_count # limit as an optimization
+ })
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, exception_details)
diff --git a/app/finders/autocomplete/routes_finder.rb b/app/finders/autocomplete/routes_finder.rb
index 858a4b69376..ecede0c1c1c 100644
--- a/app/finders/autocomplete/routes_finder.rb
+++ b/app/finders/autocomplete/routes_finder.rb
@@ -13,7 +13,7 @@ module Autocomplete
end
def execute
- return [] if @search.blank?
+ return Route.none if @search.blank?
Route
.for_routable(routables)
@@ -30,7 +30,7 @@ module Autocomplete
class NamespacesOnly < self
def routables
- return Namespace.without_project_namespaces if current_user.admin?
+ return Namespace.without_project_namespaces if current_user.can_admin_all_resources?
current_user.namespaces
end
@@ -38,7 +38,7 @@ module Autocomplete
class ProjectsOnly < self
def routables
- return Project.all if current_user.admin?
+ return Project.all if current_user.can_admin_all_resources?
current_user.projects
end
diff --git a/app/finders/ci/freeze_periods_finder.rb b/app/finders/ci/freeze_periods_finder.rb
new file mode 100644
index 00000000000..91df776abe6
--- /dev/null
+++ b/app/finders/ci/freeze_periods_finder.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodsFinder
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ def execute
+ return Ci::FreezePeriod.none unless Ability.allowed?(@current_user, :read_freeze_period, @project)
+
+ @project.freeze_periods
+ end
+ end
+end
diff --git a/app/finders/ci/jobs_finder.rb b/app/finders/ci/jobs_finder.rb
index 152eb271694..1627e41a02d 100644
--- a/app/finders/ci/jobs_finder.rb
+++ b/app/finders/ci/jobs_finder.rb
@@ -16,6 +16,7 @@ module Ci
def execute
builds = init_collection.order_id_desc
+ builds = filter_by_with_artifacts(builds)
filter_by_scope(builds)
rescue Gitlab::Access::AccessDeniedError
type.none
@@ -30,7 +31,7 @@ module Ci
end
def all_jobs
- raise Gitlab::Access::AccessDeniedError unless current_user&.admin?
+ raise Gitlab::Access::AccessDeniedError unless current_user&.can_admin_all_resources?
type.all
end
@@ -72,6 +73,14 @@ module Ci
end
end
+ def filter_by_with_artifacts(builds)
+ if params[:with_artifacts]
+ builds.with_erasable_artifacts
+ else
+ builds
+ end
+ end
+
def filter_by_statuses!(builds)
unknown_statuses = params[:scope] - ::CommitStatus::AVAILABLE_STATUSES
raise ArgumentError, 'Scope contains invalid value(s)' unless unknown_statuses.empty?
diff --git a/app/finders/ci/pipelines_finder.rb b/app/finders/ci/pipelines_finder.rb
index 712d5f8c6fb..4c47517299a 100644
--- a/app/finders/ci/pipelines_finder.rb
+++ b/app/finders/ci/pipelines_finder.rb
@@ -36,6 +36,7 @@ module Ci
items = by_yaml_errors(items)
items = by_updated_at(items)
items = by_source(items)
+ items = by_name(items)
sort_items(items)
end
@@ -152,6 +153,15 @@ module Ci
items
end
+ def by_name(items)
+ return items unless
+ Feature.enabled?(:pipeline_name, project) &&
+ Feature.enabled?(:pipeline_name_search, project) &&
+ params[:name].present?
+
+ items.for_name(params[:name])
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def sort_items(items)
order_by = if ALLOWED_INDEXED_COLUMNS.include?(params[:order_by])
diff --git a/app/finders/ci/runners_finder.rb b/app/finders/ci/runners_finder.rb
index d0d98a59677..136d23939e2 100644
--- a/app/finders/ci/runners_finder.rb
+++ b/app/finders/ci/runners_finder.rb
@@ -10,6 +10,7 @@ module Ci
def initialize(current_user:, params:)
@params = params
@group = params.delete(:group)
+ @project = params.delete(:project)
@current_user = current_user
end
@@ -36,13 +37,19 @@ module Ci
private
def search!
- @group ? group_runners : all_runners
+ if @project && Feature.enabled?(:on_demand_scans_runner_tags, @project)
+ project_runners
+ elsif @group
+ group_runners
+ else
+ all_runners
+ end
@runners = @runners.search(@params[:search]) if @params[:search].present?
end
def all_runners
- raise Gitlab::Access::AccessDeniedError unless @current_user&.admin?
+ raise Gitlab::Access::AccessDeniedError unless @current_user&.can_admin_all_resources?
@runners = Ci::Runner.all
end
@@ -66,6 +73,12 @@ module Ci
end
end
+ def project_runners
+ raise Gitlab::Access::AccessDeniedError unless can?(@current_user, :admin_project, @project)
+
+ @runners = ::Ci::Runner.owned_or_instance_wide(@project.id)
+ end
+
def filter_by_active!
@runners = @runners.active(@params[:active]) if @params.include?(:active)
end
diff --git a/app/finders/clusters/agent_tokens_finder.rb b/app/finders/clusters/agent_tokens_finder.rb
index e241836e1dc..72692777bc6 100644
--- a/app/finders/clusters/agent_tokens_finder.rb
+++ b/app/finders/clusters/agent_tokens_finder.rb
@@ -2,24 +2,30 @@
module Clusters
class AgentTokensFinder
- def initialize(object, current_user, agent_id)
- @object = object
+ include FinderMethods
+
+ def initialize(agent, current_user, params = {})
+ @agent = agent
@current_user = current_user
- @agent_id = agent_id
+ @params = params
end
def execute
- raise_not_found_unless_can_read_cluster
+ return ::Clusters::AgentToken.none unless can_read_cluster_agents?
- object.cluster_agents.find(agent_id).agent_tokens
+ agent.agent_tokens.then { |agent_tokens| by_status(agent_tokens) }
end
private
- attr_reader :object, :current_user, :agent_id
+ attr_reader :agent, :current_user, :params
+
+ def by_status(agent_tokens)
+ params[:status].present? ? agent_tokens.with_status(params[:status]) : agent_tokens
+ end
- def raise_not_found_unless_can_read_cluster
- raise ActiveRecord::RecordNotFound unless current_user&.can?(:read_cluster, object)
+ def can_read_cluster_agents?
+ current_user&.can?(:read_cluster, agent&.project)
end
end
end
diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb
index 5b2139cb941..21869f6f31d 100644
--- a/app/finders/deployments_finder.rb
+++ b/app/finders/deployments_finder.rb
@@ -212,6 +212,7 @@ class DeploymentsFinder
deployable: {
job_artifacts: [],
user: [],
+ metadata: [],
pipeline: {
project: {
route: [],
diff --git a/app/finders/environments/environments_finder.rb b/app/finders/environments/environments_finder.rb
index f2dcba04349..85cd37c267e 100644
--- a/app/finders/environments/environments_finder.rb
+++ b/app/finders/environments/environments_finder.rb
@@ -41,7 +41,13 @@ module Environments
def by_search(environments)
if params[:search].present?
- environments.for_name_like(params[:search], limit: nil)
+ if Feature.enabled?(:enable_environments_search_within_folder, project)
+ Environment.from_union(
+ environments.for_name_like(params[:search], limit: nil),
+ environments.for_name_like_within_folder(params[:search], limit: nil))
+ else
+ environments.for_name_like(params[:search], limit: nil)
+ end
else
environments
end
@@ -57,7 +63,7 @@ module Environments
def by_ids(environments)
if params[:environment_ids].present?
- environments.for_id(params[:environment_ids])
+ environments.id_in(params[:environment_ids])
else
environments
end
diff --git a/app/finders/freeze_periods_finder.rb b/app/finders/freeze_periods_finder.rb
deleted file mode 100644
index 2a9bfbe12ba..00000000000
--- a/app/finders/freeze_periods_finder.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-# frozen_string_literal: true
-
-class FreezePeriodsFinder
- def initialize(project, current_user = nil)
- @project = project
- @current_user = current_user
- end
-
- def execute
- return Ci::FreezePeriod.none unless Ability.allowed?(@current_user, :read_freeze_period, @project)
-
- @project.freeze_periods
- end
-end
diff --git a/app/finders/git_refs_finder.rb b/app/finders/git_refs_finder.rb
index dbe0060d8ae..0492dd9934f 100644
--- a/app/finders/git_refs_finder.rb
+++ b/app/finders/git_refs_finder.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GitRefsFinder
+ include Gitlab::Utils::StrongMemoize
+
def initialize(repository, params = {})
@repository = repository
@params = params
@@ -10,44 +12,28 @@ class GitRefsFinder
attr_reader :repository, :params
- def search
- @params[:search].to_s.presence
- end
-
- def sort
- @params[:sort].to_s.presence || 'name'
- end
-
def by_search(refs)
return refs unless search
- case search
- when ->(v) { v.starts_with?('^') }
- filter_refs_with_prefix(refs, search.slice(1..-1))
- when ->(v) { v.ends_with?('$') }
- filter_refs_with_suffix(refs, search.chop)
- else
- matches = filter_refs_by_name(refs, search)
- set_exact_match_as_first_result(matches, search)
- end
- end
-
- def filter_refs_with_prefix(refs, prefix)
- prefix = prefix.downcase
+ matches = filter_refs(refs, search)
+ return matches if regex_search?
- refs.select { |ref| ref.name.downcase.starts_with?(prefix) }
+ set_exact_match_as_first_result(matches, search)
end
- def filter_refs_with_suffix(refs, suffix)
- suffix = suffix.downcase
-
- refs.select { |ref| ref.name.downcase.ends_with?(suffix) }
+ def search
+ @params[:search].to_s.presence
end
+ strong_memoize_attr :search
- def filter_refs_by_name(refs, term)
- term = term.downcase
+ def sort
+ @params[:sort].to_s.presence || 'name'
+ end
- refs.select { |ref| ref.name.downcase.include?(term) }
+ def filter_refs(refs, term)
+ regex_string = Regexp.quote(term.downcase)
+ regex_string = unescape_regex_operators(regex_string) if regex_search?
+ refs.select { |ref| /#{regex_string}/ === ref.name.downcase }
end
def set_exact_match_as_first_result(matches, term)
@@ -59,4 +45,13 @@ class GitRefsFinder
def find_exact_match_index(matches, term)
matches.index { |ref| ref.name.casecmp(term) == 0 }
end
+
+ def regex_search?
+ Regexp.union('^', '$', '*') === search
+ end
+ strong_memoize_attr :regex_search?, :regex_search
+
+ def unescape_regex_operators(regex_string)
+ regex_string.sub('\^', '^').gsub('\*', '.*?').sub('\$', '$')
+ end
end
diff --git a/app/finders/group_descendants_finder.rb b/app/finders/group_descendants_finder.rb
index 42cd06c8066..033af0f42a6 100644
--- a/app/finders/group_descendants_finder.rb
+++ b/app/finders/group_descendants_finder.rb
@@ -22,7 +22,7 @@
class GroupDescendantsFinder
attr_reader :current_user, :parent_group, :params
- def initialize(current_user: nil, parent_group:, params: {})
+ def initialize(parent_group:, current_user: nil, params: {})
@current_user = current_user
@parent_group = parent_group
@params = params.reverse_merge(non_archived: params[:archived].blank?, not_aimed_for_deletion: true)
diff --git a/app/finders/group_members_finder.rb b/app/finders/group_members_finder.rb
index 4688d561897..47ed623b252 100644
--- a/app/finders/group_members_finder.rb
+++ b/app/finders/group_members_finder.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class GroupMembersFinder < UnionFinder
- RELATIONS = %i(direct inherited descendants shared_from_groups).freeze
- DEFAULT_RELATIONS = %i(direct inherited).freeze
+ RELATIONS = %i[direct inherited descendants shared_from_groups].freeze
+ DEFAULT_RELATIONS = %i[direct inherited].freeze
INVALID_RELATION_TYPE_ERROR_MSG = "is not a valid relation type. Valid relation types are #{RELATIONS.join(', ')}."
RELATIONS_DESCRIPTIONS = {
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index e68a0c8fca9..de6eacbb1e0 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
class MembersFinder
- RELATIONS = %i(direct inherited descendants invited_groups).freeze
- DEFAULT_RELATIONS = %i(direct inherited).freeze
+ RELATIONS = %i[direct inherited descendants invited_groups].freeze
+ DEFAULT_RELATIONS = %i[direct inherited].freeze
# Params can be any of the following:
# sort: string
diff --git a/app/finders/merge_request_target_project_finder.rb b/app/finders/merge_request_target_project_finder.rb
index dc9b28ab0a0..fdb3bac8935 100644
--- a/app/finders/merge_request_target_project_finder.rb
+++ b/app/finders/merge_request_target_project_finder.rb
@@ -5,7 +5,7 @@ class MergeRequestTargetProjectFinder
attr_reader :current_user, :source_project
- def initialize(current_user: nil, source_project:, project_feature: :merge_requests)
+ def initialize(source_project:, current_user: nil, project_feature: :merge_requests)
@current_user = current_user
@source_project = source_project
@project_feature = project_feature
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 42bd7a24888..7890502cf0e 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -65,7 +65,7 @@ class NotesFinder
@target =
if target_type == "commit"
- if Ability.allowed?(@current_user, :download_code, @project)
+ if Ability.allowed?(@current_user, :read_code, @project)
@project.commit(target_id)
end
else
@@ -101,7 +101,7 @@ class NotesFinder
# rubocop: disable CodeReuse/ActiveRecord
def notes_of_any_type
- types = %w(commit issue merge_request snippet)
+ types = %w[commit issue merge_request snippet]
note_relations = types.map { |t| notes_for_type(t) }
note_relations.map! { |notes| search(notes) }
UnionFinder.new.find_union(note_relations, Note.includes(:author)) # rubocop: disable CodeReuse/Finder
@@ -126,7 +126,7 @@ class NotesFinder
# rubocop: disable CodeReuse/ActiveRecord
def notes_for_type(noteable_type)
if noteable_type == "commit"
- if Ability.allowed?(@current_user, :download_code, @project)
+ if Ability.allowed?(@current_user, :read_code, @project)
@project.notes.where(noteable_type: 'Commit')
else
Note.none
diff --git a/app/finders/personal_access_tokens_finder.rb b/app/finders/personal_access_tokens_finder.rb
index 8403c531945..5af08cf0660 100644
--- a/app/finders/personal_access_tokens_finder.rb
+++ b/app/finders/personal_access_tokens_finder.rb
@@ -33,7 +33,7 @@ class PersonalAccessTokensFinder
attr_reader :current_user
def by_current_user(tokens)
- return tokens if current_user.nil? || current_user.admin?
+ return tokens if current_user.nil? || current_user.can_admin_all_resources?
return PersonalAccessToken.none unless Ability.allowed?(current_user, :read_user_personal_access_tokens, params[:user])
tokens
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 126687ae41f..1afd5adeada 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -89,6 +89,7 @@ class ProjectsFinder < UnionFinder
collection = by_not_aimed_for_deletion(collection)
collection = by_last_activity_after(collection)
collection = by_last_activity_before(collection)
+ collection = by_language(collection)
by_repository_storage(collection)
end
@@ -97,12 +98,10 @@ class ProjectsFinder < UnionFinder
current_user.owned_projects
elsif min_access_level?
current_user.authorized_projects(params[:min_access_level])
+ elsif private_only? || impossible_visibility_level?
+ current_user.authorized_projects
else
- if private_only? || impossible_visibility_level?
- current_user.authorized_projects
- else
- Project.public_or_visible_to_user(current_user)
- end
+ Project.public_or_visible_to_user(current_user)
end
end
@@ -239,6 +238,14 @@ class ProjectsFinder < UnionFinder
end
end
+ def by_language(items)
+ if Feature.enabled?(:project_language_search, current_user) && params[:language].present?
+ items.with_programming_language_id(params[:language])
+ else
+ items
+ end
+ end
+
def sort(items)
if params[:sort].present?
items.sort_by_attribute(params[:sort])
diff --git a/app/finders/releases/group_releases_finder.rb b/app/finders/releases/group_releases_finder.rb
index 08530f63ea6..67784d6579c 100644
--- a/app/finders/releases/group_releases_finder.rb
+++ b/app/finders/releases/group_releases_finder.rb
@@ -33,8 +33,8 @@ module Releases
Gitlab::Pagination::Keyset::InOperatorOptimization::QueryBuilder.new(
scope: releases_scope,
array_scope: Project.for_group_and_its_subgroups(parent).select(:id),
- array_mapping_scope: -> (project_id_expression) { Release.where(Release.arel_table[:project_id].eq(project_id_expression)) },
- finder_query: -> (order_by, id_expression) { Release.where(Release.arel_table[:id].eq(id_expression)) }
+ array_mapping_scope: ->(project_id_expression) { Release.where(Release.arel_table[:project_id].eq(project_id_expression)) },
+ finder_query: ->(order_by, id_expression) { Release.where(Release.arel_table[:id].eq(id_expression)) }
)
.execute
end
diff --git a/app/finders/repositories/tree_finder.rb b/app/finders/repositories/tree_finder.rb
index 2ea5a8856ec..231c1de1513 100644
--- a/app/finders/repositories/tree_finder.rb
+++ b/app/finders/repositories/tree_finder.rb
@@ -1,15 +1,13 @@
# frozen_string_literal: true
module Repositories
- class TreeFinder < GitRefsFinder
- attr_reader :user_project
-
+ class TreeFinder
CommitMissingError = Class.new(StandardError)
- def initialize(user_project, params = {})
- super(user_project.repository, params)
-
- @user_project = user_project
+ def initialize(project, params = {})
+ @project = project
+ @repository = project.repository
+ @params = params
end
def execute(gitaly_pagination: false)
@@ -17,15 +15,15 @@ module Repositories
request_params = { recursive: recursive }
request_params[:pagination_params] = pagination_params if gitaly_pagination
- tree = user_project.repository.tree(commit.id, path, **request_params)
- tree.sorted_entries
+ repository.tree(commit.id, path, **request_params).sorted_entries
end
def total
# This is inefficient and we'll look at replacing this implementation
- Gitlab::Cache.fetch_once([user_project, repository.commit, :tree_size, commit.id, path, recursive]) do
- user_project.repository.tree(commit.id, path, recursive: recursive).entries.size
+ cache_key = [project, repository.commit, :tree_size, commit.id, path, recursive]
+ Gitlab::Cache.fetch_once(cache_key) do
+ repository.tree(commit.id, path, recursive: recursive).entries.size
end
end
@@ -35,12 +33,14 @@ module Repositories
private
+ attr_reader :project, :repository, :params
+
def commit
- @commit ||= user_project.commit(ref)
+ @commit ||= project.commit(ref)
end
def ref
- params[:ref] || user_project.default_branch
+ params[:ref] || project.default_branch
end
def path
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index e83018ed24c..0bf31ea33dd 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -24,7 +24,7 @@ class TodosFinder
NONE = '0'
- TODO_TYPES = Set.new(%w(Issue MergeRequest DesignManagement::Design AlertManagement::Alert)).freeze
+ TODO_TYPES = Set.new(%w[Issue WorkItem MergeRequest DesignManagement::Design AlertManagement::Alert]).freeze
attr_accessor :current_user, :params
diff --git a/app/finders/users_finder.rb b/app/finders/users_finder.rb
index 9c2462b42a6..11e3c341c1f 100644
--- a/app/finders/users_finder.rb
+++ b/app/finders/users_finder.rb
@@ -55,7 +55,7 @@ class UsersFinder
private
def base_scope
- scope = current_user&.admin? ? User.all : User.without_forbidden_states
+ scope = current_user&.can_admin_all_resources? ? User.all : User.without_forbidden_states
scope.order_id_desc
end
@@ -80,7 +80,7 @@ class UsersFinder
def by_search(users)
return users unless params[:search].present?
- users.search(params[:search], with_private_emails: current_user&.admin?)
+ users.search(params[:search], with_private_emails: current_user&.can_admin_all_resources?)
end
def by_blocked(users)
@@ -97,7 +97,7 @@ class UsersFinder
# rubocop: disable CodeReuse/ActiveRecord
def by_external_identity(users)
- return users unless current_user&.admin? && params[:extern_uid] && params[:provider]
+ return users unless current_user&.can_admin_all_resources? && params[:extern_uid] && params[:provider]
users.joins(:identities).merge(Identity.with_extern_uid(params[:provider], params[:extern_uid]))
end
diff --git a/app/graphql/graphql_triggers.rb b/app/graphql/graphql_triggers.rb
index 710e7fe110c..7f83b62a2ff 100644
--- a/app/graphql/graphql_triggers.rb
+++ b/app/graphql/graphql_triggers.rb
@@ -44,6 +44,14 @@ module GraphqlTriggers
merge_request
)
end
+
+ def self.merge_request_approval_state_updated(merge_request)
+ GitlabSchema.subscriptions.trigger(
+ 'mergeRequestApprovalStateUpdated',
+ { issuable_id: merge_request.to_gid },
+ merge_request
+ )
+ end
end
GraphqlTriggers.prepend_mod
diff --git a/app/graphql/mutations/alert_management/alerts/set_assignees.rb b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
index c986111d290..500e2b868b1 100644
--- a/app/graphql/mutations/alert_management/alerts/set_assignees.rb
+++ b/app/graphql/mutations/alert_management/alerts/set_assignees.rb
@@ -20,7 +20,7 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = set_assignees(alert, args[:assignee_usernames], args[:operation_mode])
- track_usage_event(:incident_management_alert_assigned, current_user.id)
+ track_alert_events('incident_management_alert_assigned', alert)
prepare_response(result)
end
diff --git a/app/graphql/mutations/alert_management/alerts/todo/create.rb b/app/graphql/mutations/alert_management/alerts/todo/create.rb
index 2a1056e8f64..999c0bec5af 100644
--- a/app/graphql/mutations/alert_management/alerts/todo/create.rb
+++ b/app/graphql/mutations/alert_management/alerts/todo/create.rb
@@ -11,7 +11,7 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = ::AlertManagement::Alerts::Todo::CreateService.new(alert, current_user).execute
- track_usage_event(:incident_management_alert_todo, current_user.id)
+ track_alert_events('incident_management_alert_todo', alert)
prepare_response(result)
end
diff --git a/app/graphql/mutations/alert_management/base.rb b/app/graphql/mutations/alert_management/base.rb
index d01f200107c..2eef6bb9db7 100644
--- a/app/graphql/mutations/alert_management/base.rb
+++ b/app/graphql/mutations/alert_management/base.rb
@@ -39,6 +39,24 @@ module Mutations
::AlertManagement::AlertsFinder.new(current_user, project, args).execute.first
end
+
+ def track_alert_events(event, alert)
+ project = alert.project
+ namespace = project.namespace
+ track_usage_event(event, current_user.id)
+
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
+
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ event,
+ project: project,
+ namespace: namespace,
+ user: current_user,
+ label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
+ )
+ end
end
end
end
diff --git a/app/graphql/mutations/alert_management/create_alert_issue.rb b/app/graphql/mutations/alert_management/create_alert_issue.rb
index 77a7d7a4147..7c8de6365e7 100644
--- a/app/graphql/mutations/alert_management/create_alert_issue.rb
+++ b/app/graphql/mutations/alert_management/create_alert_issue.rb
@@ -9,7 +9,7 @@ module Mutations
alert = authorized_find!(project_path: args[:project_path], iid: args[:iid])
result = create_alert_issue(alert, current_user)
- track_usage_event(:incident_management_incident_created, current_user.id)
+ track_alert_events('incident_management_incident_created', alert)
track_usage_event(:incident_management_alert_create_incident, current_user.id)
prepare_response(alert, result)
diff --git a/app/graphql/mutations/alert_management/update_alert_status.rb b/app/graphql/mutations/alert_management/update_alert_status.rb
index 21566c7d66f..be271a7d795 100644
--- a/app/graphql/mutations/alert_management/update_alert_status.rb
+++ b/app/graphql/mutations/alert_management/update_alert_status.rb
@@ -13,7 +13,7 @@ module Mutations
alert = authorized_find!(project_path: project_path, iid: iid)
result = update_status(alert, status)
- track_usage_event(:incident_management_alert_status_changed, current_user.id)
+ track_alert_events('incident_management_alert_status_changed', alert)
prepare_response(result)
end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/create.rb b/app/graphql/mutations/ci/pipeline_schedule/create.rb
new file mode 100644
index 00000000000..65b355cd80f
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/create.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class Create < BaseMutation
+ graphql_name 'PipelineScheduleCreate'
+
+ include FindsProject
+
+ authorize :create_pipeline_schedule
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the project the pipeline schedule is associated with.'
+
+ argument :description, GraphQL::Types::String,
+ required: true,
+ description: 'Description of the pipeline schedule.'
+
+ argument :cron, GraphQL::Types::String,
+ required: true,
+ description: 'Cron expression of the pipeline schedule.'
+
+ argument :cron_timezone, GraphQL::Types::String,
+ required: false,
+ description:
+ <<-STR
+ Cron time zone supported by ActiveSupport::TimeZone.
+ For example: "Pacific Time (US & Canada)" (default: "UTC").
+ STR
+
+ argument :ref, GraphQL::Types::String,
+ required: true,
+ description: 'Ref of the pipeline schedule.'
+
+ argument :active, GraphQL::Types::Boolean,
+ required: false,
+ description: 'Indicates if the pipeline schedule should be active or not.'
+
+ argument :variables, [Mutations::Ci::PipelineSchedule::VariableInputType],
+ required: false,
+ description: 'Variables for the pipeline schedule.'
+
+ field :pipeline_schedule,
+ Types::Ci::PipelineScheduleType,
+ description: 'Created pipeline schedule.'
+
+ def resolve(project_path:, variables: [], **pipeline_schedule_attrs)
+ project = authorized_find!(project_path)
+
+ params = pipeline_schedule_attrs.merge(variables_attributes: variables.map(&:to_h))
+
+ schedule = ::Ci::CreatePipelineScheduleService
+ .new(project, current_user, params)
+ .execute
+
+ unless schedule.persisted?
+ return {
+ pipeline_schedule: nil, errors: schedule.errors.full_messages
+ }
+ end
+
+ {
+ pipeline_schedule: schedule,
+ errors: []
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/play.rb b/app/graphql/mutations/ci/pipeline_schedule/play.rb
new file mode 100644
index 00000000000..056890852c9
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/play.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class Play < Base
+ graphql_name 'PipelineSchedulePlay'
+
+ authorize :play_pipeline_schedule
+
+ field :pipeline_schedule,
+ Types::Ci::PipelineScheduleType,
+ null: true,
+ description: 'Pipeline schedule after mutation.'
+
+ def resolve(id:)
+ schedule = authorized_find!(id: id)
+
+ job_id = ::Ci::PipelineScheduleService
+ .new(schedule.project, current_user)
+ .execute(schedule)
+
+ if job_id
+ { pipeline_schedule: schedule, errors: [] }
+ else
+ { pipeline_schedule: nil, errors: ['Unable to schedule a pipeline to run immediately.'] }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb
new file mode 100644
index 00000000000..54a6ad92448
--- /dev/null
+++ b/app/graphql/mutations/ci/pipeline_schedule/variable_input_type.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Ci
+ module PipelineSchedule
+ class VariableInputType < Types::BaseInputObject
+ graphql_name 'PipelineScheduleVariableInput'
+
+ description 'Attributes for the pipeline schedule variable.'
+
+ argument :key, GraphQL::Types::String, required: true, description: 'Name of the variable.'
+
+ argument :value, GraphQL::Types::String, required: true, description: 'Value of the variable.'
+
+ argument :variable_type, Types::Ci::VariableTypeEnum, required: true, description: 'Type of the variable.'
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/ci/runner/update.rb b/app/graphql/mutations/ci/runner/update.rb
index 3c99cde60a4..4f0bf19f09c 100644
--- a/app/graphql/mutations/ci/runner/update.rb
+++ b/app/graphql/mutations/ci/runner/update.rb
@@ -54,7 +54,7 @@ module Mutations
argument :associated_projects, [::Types::GlobalIDType[::Project]],
required: false,
description: 'Projects associated with the runner. Available only for project runners.',
- prepare: -> (global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } }
+ prepare: ->(global_ids, ctx) { global_ids&.filter_map { |gid| gid.model_id.to_i } }
field :runner,
Types::Ci::RunnerType,
diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb
index a99a54fa5ed..c10e1633350 100644
--- a/app/graphql/mutations/clusters/agent_tokens/create.rb
+++ b/app/graphql/mutations/clusters/agent_tokens/create.rb
@@ -49,9 +49,9 @@ module Mutations
payload = result.payload
{
- secret: payload[:secret],
- token: payload[:token],
- errors: Array.wrap(result.message)
+ secret: payload[:secret],
+ token: payload[:token],
+ errors: Array.wrap(result.message)
}
end
diff --git a/app/graphql/mutations/container_repositories/destroy.rb b/app/graphql/mutations/container_repositories/destroy.rb
index fe1c3fe4e61..c3bd7acf444 100644
--- a/app/graphql/mutations/container_repositories/destroy.rb
+++ b/app/graphql/mutations/container_repositories/destroy.rb
@@ -22,10 +22,6 @@ module Mutations
container_repository.delete_scheduled!
- unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker)
- DeleteContainerRepositoryWorker.perform_async(current_user.id, container_repository.id) # rubocop:disable CodeReuse/Worker
- end
-
track_event(:delete_repository, :container)
{
diff --git a/app/graphql/mutations/incident_management/timeline_event/update.rb b/app/graphql/mutations/incident_management/timeline_event/update.rb
index 1f53bdc19cb..b35feed3082 100644
--- a/app/graphql/mutations/incident_management/timeline_event/update.rb
+++ b/app/graphql/mutations/incident_management/timeline_event/update.rb
@@ -18,6 +18,10 @@ module Mutations
required: false,
description: 'Timestamp when the event occurred.'
+ argument :timeline_event_tag_names, [GraphQL::Types::String],
+ required: false,
+ description: copy_field_description(Types::IncidentManagement::TimelineEventType, :timeline_event_tags)
+
def resolve(id:, **args)
timeline_event = authorized_find!(id: id)
diff --git a/app/graphql/mutations/issues/link_alerts.rb b/app/graphql/mutations/issues/link_alerts.rb
new file mode 100644
index 00000000000..c45e90c598f
--- /dev/null
+++ b/app/graphql/mutations/issues/link_alerts.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class LinkAlerts < Base
+ graphql_name 'IssueLinkAlerts'
+
+ argument :alert_references, [GraphQL::Types::String],
+ required: true,
+ description: 'Alerts references to be linked to the incident.'
+
+ authorize :admin_issue
+
+ def resolve(project_path:, iid:, alert_references:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+
+ ::IncidentManagement::LinkAlerts::CreateService.new(issue, current_user, alert_references).execute
+
+ {
+ issue: issue,
+ errors: errors_on_object(issue)
+ }
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/unlink_alert.rb b/app/graphql/mutations/issues/unlink_alert.rb
new file mode 100644
index 00000000000..a11af4133cf
--- /dev/null
+++ b/app/graphql/mutations/issues/unlink_alert.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Issues
+ class UnlinkAlert < Base
+ graphql_name 'IssueUnlinkAlert'
+
+ argument :alert_id, ::Types::GlobalIDType[::AlertManagement::Alert],
+ required: true,
+ description: 'Global ID of the alert to unlink from the incident.'
+
+ authorize :admin_issue
+
+ def resolve(project_path:, iid:, alert_id:)
+ issue = authorized_find!(project_path: project_path, iid: iid)
+ alert = find_alert_by_gid(alert_id)
+
+ result = ::IncidentManagement::LinkAlerts::DestroyService.new(issue, current_user, alert).execute
+
+ {
+ issue: issue,
+ errors: result.errors
+ }
+ end
+
+ private
+
+ def find_alert_by_gid(alert_id)
+ ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(alert_id, expected_type: ::AlertManagement::Alert))
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/notes/create/diff_note.rb b/app/graphql/mutations/notes/create/diff_note.rb
index 7b8c06fd104..df2bd55106e 100644
--- a/app/graphql/mutations/notes/create/diff_note.rb
+++ b/app/graphql/mutations/notes/create/diff_note.rb
@@ -31,10 +31,10 @@ module Mutations
def create_note_params(noteable, args)
super(noteable, args).merge({
- type: 'DiffNote',
- position: position(noteable, args),
- merge_request_diff_head_sha: args[:position][:head_sha]
- })
+ type: 'DiffNote',
+ position: position(noteable, args),
+ merge_request_diff_head_sha: args[:position][:head_sha]
+ })
end
def position(noteable, args)
diff --git a/app/graphql/mutations/notes/create/image_diff_note.rb b/app/graphql/mutations/notes/create/image_diff_note.rb
index d94fd4d6ff8..3de93e4f5c1 100644
--- a/app/graphql/mutations/notes/create/image_diff_note.rb
+++ b/app/graphql/mutations/notes/create/image_diff_note.rb
@@ -15,9 +15,9 @@ module Mutations
def create_note_params(noteable, args)
super(noteable, args).merge({
- type: 'DiffNote',
- position: position(noteable, args)
- })
+ type: 'DiffNote',
+ position: position(noteable, args)
+ })
end
def position(noteable, args)
diff --git a/app/graphql/mutations/notes/create/note.rb b/app/graphql/mutations/notes/create/note.rb
index 4d6f056de09..9b105b7fe1c 100644
--- a/app/graphql/mutations/notes/create/note.rb
+++ b/app/graphql/mutations/notes/create/note.rb
@@ -31,9 +31,9 @@ module Mutations
end
super(noteable, args).merge({
- in_reply_to_discussion_id: discussion_id,
- merge_request_diff_head_sha: args[:merge_request_diff_head_sha]
- })
+ in_reply_to_discussion_id: discussion_id,
+ merge_request_diff_head_sha: args[:merge_request_diff_head_sha]
+ })
end
def authorize_discussion!(discussion)
diff --git a/app/graphql/mutations/timelogs/create.rb b/app/graphql/mutations/timelogs/create.rb
index bab7508454e..1be023eed8a 100644
--- a/app/graphql/mutations/timelogs/create.rb
+++ b/app/graphql/mutations/timelogs/create.rb
@@ -11,7 +11,7 @@ module Mutations
description: 'Amount of time spent.'
argument :spent_at,
- Types::DateType,
+ Types::TimeType,
required: true,
description: 'When the time was spent.'
@@ -28,8 +28,12 @@ module Mutations
authorize :create_timelog
def resolve(issuable_id:, time_spent:, spent_at:, summary:, **args)
- issuable = authorized_find!(id: issuable_id)
parsed_time_spent = Gitlab::TimeTrackingFormatter.parse(time_spent)
+ if parsed_time_spent.nil?
+ return { timelog: nil, errors: [_('Time spent must be formatted correctly. For example: 1h 30m.')] }
+ end
+
+ issuable = authorized_find!(id: issuable_id)
result = ::Timelogs::CreateService.new(
issuable, parsed_time_spent, spent_at, summary, current_user
diff --git a/app/graphql/mutations/todos/restore_many.rb b/app/graphql/mutations/todos/restore_many.rb
index 20913a9e7da..f2f944860c2 100644
--- a/app/graphql/mutations/todos/restore_many.rb
+++ b/app/graphql/mutations/todos/restore_many.rb
@@ -23,9 +23,9 @@ module Mutations
updated_ids = restore(todos)
{
- updated_ids: updated_ids,
- todos: Todo.id_in(updated_ids),
- errors: errors_on_objects(todos)
+ updated_ids: updated_ids,
+ todos: Todo.id_in(updated_ids),
+ errors: errors_on_objects(todos)
}
end
diff --git a/app/graphql/mutations/work_items/create.rb b/app/graphql/mutations/work_items/create.rb
index 793e5d3caf8..a4efffb69c1 100644
--- a/app/graphql/mutations/work_items/create.rb
+++ b/app/graphql/mutations/work_items/create.rb
@@ -73,3 +73,5 @@ module Mutations
end
end
end
+
+Mutations::WorkItems::Create.prepend_mod
diff --git a/app/graphql/resolvers/base_resolver.rb b/app/graphql/resolvers/base_resolver.rb
index 2b54a3fdd55..6f847221f1b 100644
--- a/app/graphql/resolvers/base_resolver.rb
+++ b/app/graphql/resolvers/base_resolver.rb
@@ -15,6 +15,13 @@ module Resolvers
@calls_gitaly = true
end
+ # This is a flag to allow us to use `complexity_multiplier` to compute complexity for connection
+ # fields(see BaseField#connection_complexity_multiplier) in resolvers that do external connection pagination,
+ # thus disabling the default `connection` option(see self.field_options method above).
+ def self.calculate_ext_conn_complexity
+ false
+ end
+
def self.field_options
extra_options = {
requires_argument: @requires_argument,
@@ -116,7 +123,7 @@ module Resolvers
# When fetching many items, additional complexity is added to the field
# depending on how many items is fetched. For each item we add 1% of the
# original complexity - this means that loading 100 items (our default
- # maxp_age_size limit) doubles the original complexity.
+ # max_page_size limit) doubles the original complexity.
#
# Complexity is not increased when searching by specific ID(s), because
# complexity difference is minimal in this case.
diff --git a/app/graphql/resolvers/ci/project_runners_resolver.rb b/app/graphql/resolvers/ci/project_runners_resolver.rb
new file mode 100644
index 00000000000..378fa73c065
--- /dev/null
+++ b/app/graphql/resolvers/ci/project_runners_resolver.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class ProjectRunnersResolver < RunnersResolver
+ type Types::Ci::RunnerType.connection_type, null: true
+
+ def parent_param
+ raise 'Expected project missing' unless parent.is_a?(Project)
+
+ { project: parent }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runner_groups_resolver.rb b/app/graphql/resolvers/ci/runner_groups_resolver.rb
new file mode 100644
index 00000000000..3360e820bd2
--- /dev/null
+++ b/app/graphql/resolvers/ci/runner_groups_resolver.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Ci
+ class RunnerGroupsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include ResolvesGroups
+
+ type Types::GroupConnection, null: true
+
+ authorize :read_runner
+ authorizes_object!
+
+ alias_method :runner, :object
+
+ def resolve_with_lookahead(**args)
+ return unless runner.group_type?
+
+ BatchLoader::GraphQL.for(runner.id).batch(key: :runner_namespaces) do |runner_ids, loader|
+ plucked_runner_and_namespace_ids =
+ ::Ci::RunnerNamespace
+ .for_runner(runner_ids)
+ .select(:runner_id, :namespace_id)
+ .pluck(:runner_id, :namespace_id) # rubocop: disable CodeReuse/ActiveRecord)
+
+ namespace_ids = plucked_runner_and_namespace_ids.collect(&:last).uniq
+ groups = apply_lookahead(::Group.id_in(namespace_ids))
+ Preloaders::GroupPolicyPreloader.new(groups, current_user).execute
+ groups_by_id = groups.index_by(&:id)
+
+ runner_group_ids_by_runner_id =
+ plucked_runner_and_namespace_ids
+ .group_by { |runner_id, _namespace_id| runner_id }
+ .transform_values { |values| values.filter_map { |_runner_id, namespace_id| groups_by_id[namespace_id] } }
+
+ runner_ids.each do |runner_id|
+ runner_namespaces = runner_group_ids_by_runner_id[runner_id] || []
+
+ loader.call(runner_id, runner_namespaces)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/ci/runner_jobs_resolver.rb b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
index de00aadaea8..b818be3f018 100644
--- a/app/graphql/resolvers/ci/runner_jobs_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_jobs_resolver.rb
@@ -27,9 +27,17 @@ module Resolvers
def preloads
{
- previous_stage_jobs_and_needs: [:needs, :pipeline],
+ previous_stage_jobs_or_needs: [:needs, :pipeline],
artifacts: [:job_artifacts],
- pipeline: [:user]
+ pipeline: [:user],
+ detailed_status: [
+ :metadata,
+ { pipeline: [:merge_request] },
+ { project: [:route, { namespace: :route }] }
+ ],
+ commit_path: [:pipeline, { project: [:route, { namespace: [:route] }] }],
+ short_sha: [:pipeline],
+ tags: [:tags]
}
end
end
diff --git a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
index da8fab93619..f4e044b81c9 100644
--- a/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_owner_project_resolver.rb
@@ -13,20 +13,22 @@ module Resolvers
resolve_owner
end
- def preloads
- {
- full_path: [:route]
- }
- end
-
private
- def filtered_preloads
- selection = lookahead
+ def node_selection(selection = lookahead)
+ # There are no nodes or edges selections in RunnerOwnerProjectResolver, but rather a project directly
+ selection
+ end
+
+ def unconditional_includes
+ [:project_feature]
+ end
- preloads.each.flat_map do |name, requirements|
- selection&.selects?(name) ? requirements : []
- end
+ def preloads
+ {
+ full_path: [:route, { namespace: [:route] }],
+ web_url: [:route, { namespace: [:route] }]
+ }
end
def resolve_owner
@@ -48,7 +50,7 @@ module Resolvers
.transform_values { |runner_projects| runner_projects.first.project_id }
project_ids = owner_project_id_by_runner_id.values.uniq
- projects = Project.where(id: project_ids)
+ projects = apply_lookahead(Project.id_in(project_ids))
Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute
projects_by_id = projects.index_by(&:id)
diff --git a/app/graphql/resolvers/ci/runner_projects_resolver.rb b/app/graphql/resolvers/ci/runner_projects_resolver.rb
index af9a67acfda..2a2d63f85de 100644
--- a/app/graphql/resolvers/ci/runner_projects_resolver.rb
+++ b/app/graphql/resolvers/ci/runner_projects_resolver.rb
@@ -40,6 +40,7 @@ module Resolvers
params: project_finder_params(args),
project_ids_relation: project_ids)
.execute
+ projects = apply_lookahead(projects)
Preloaders::ProjectPolicyPreloader.new(projects, current_user).execute
projects_by_id = projects.index_by(&:id)
@@ -58,6 +59,19 @@ module Resolvers
end
# rubocop:enable CodeReuse/ActiveRecord
end
+
+ private
+
+ def unconditional_includes
+ [:project_feature]
+ end
+
+ def preloads
+ super.merge({
+ full_path: [:route, { namespace: [:route] }],
+ web_url: [:route, { namespace: [:route] }]
+ })
+ end
end
end
end
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
index 9740bc6bb6a..b7355a1752e 100644
--- a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -14,18 +14,7 @@ module Resolvers
description: 'Status of the token.'
def resolve(**args)
- return ::Clusters::AgentToken.none unless can_read_agent_tokens?
-
- tokens = agent.agent_tokens
- tokens = tokens.with_status(args[:status]) if args[:status].present?
-
- tokens
- end
-
- private
-
- def can_read_agent_tokens?
- current_user.can?(:read_cluster, project)
+ ::Clusters::AgentTokensFinder.new(agent, current_user, args).execute
end
end
end
diff --git a/app/graphql/resolvers/concerns/looks_ahead.rb b/app/graphql/resolvers/concerns/looks_ahead.rb
index 81099c04e9f..1d532eb2486 100644
--- a/app/graphql/resolvers/concerns/looks_ahead.rb
+++ b/app/graphql/resolvers/concerns/looks_ahead.rb
@@ -39,7 +39,7 @@ module LooksAhead
def filtered_preloads
nodes = node_selection
- return [] unless nodes
+ return [] unless nodes&.selected?
selected_fields = nodes.selections.map(&:name)
root_level_preloads = preloads_from_node_selection(selected_fields, preloads)
@@ -65,13 +65,13 @@ module LooksAhead
end.flatten
end
- def node_selection
- return unless lookahead
+ def node_selection(selection = lookahead)
+ return selection unless selection&.selected?
+ return selection.selection(:edges).selection(:node) if selection.selects?(:edges)
- if lookahead.selects?(:nodes)
- lookahead.selection(:nodes)
- elsif lookahead.selects?(:edges)
- lookahead.selection(:edges).selection(:node)
- end
+ # Will return a NullSelection object if :nodes is not a selection. This
+ # is better than returning nil as we can continue chaining selections on
+ # without raising errors.
+ selection.selection(:nodes)
end
end
diff --git a/app/graphql/resolvers/concerns/resolves_groups.rb b/app/graphql/resolvers/concerns/resolves_groups.rb
index 2a3dce80057..1268e74fd58 100644
--- a/app/graphql/resolvers/concerns/resolves_groups.rb
+++ b/app/graphql/resolvers/concerns/resolves_groups.rb
@@ -22,6 +22,7 @@ module ResolvesGroups
custom_emoji: [:custom_emoji],
full_path: [:route],
path: [:route],
+ web_url: [:route],
dependency_proxy_blob_count: [:dependency_proxy_blobs],
dependency_proxy_blobs: [:dependency_proxy_blobs],
dependency_proxy_image_count: [:dependency_proxy_manifests],
diff --git a/app/graphql/resolvers/environments/nested_environments_resolver.rb b/app/graphql/resolvers/environments/nested_environments_resolver.rb
new file mode 100644
index 00000000000..f043270beca
--- /dev/null
+++ b/app/graphql/resolvers/environments/nested_environments_resolver.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Environments
+ class NestedEnvironmentsResolver < EnvironmentsResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ type Types::NestedEnvironmentType, null: true
+
+ authorizes_object!
+ authorize :read_environment
+
+ def resolve(**args)
+ offset_pagination(super(**args).nested)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/environments_resolver.rb b/app/graphql/resolvers/environments_resolver.rb
index f265e2183d0..aca1a36f0f5 100644
--- a/app/graphql/resolvers/environments_resolver.rb
+++ b/app/graphql/resolvers/environments_resolver.rb
@@ -14,6 +14,10 @@ module Resolvers
required: false,
description: 'States of environments that should be included in result.'
+ argument :type, GraphQL::Types::String,
+ required: false,
+ description: 'Search query for environment type.'
+
type Types::EnvironmentType, null: true
alias_method :project, :object
diff --git a/app/graphql/resolvers/group_packages_resolver.rb b/app/graphql/resolvers/group_packages_resolver.rb
index e6a6abb39dd..ae578390fd5 100644
--- a/app/graphql/resolvers/group_packages_resolver.rb
+++ b/app/graphql/resolvers/group_packages_resolver.rb
@@ -13,9 +13,9 @@ module Resolvers
default_value: :created_desc
GROUP_SORT_TO_PARAMS_MAP = SORT_TO_PARAMS_MAP.merge({
- project_path_desc: { order_by: 'project_path', sort: 'desc' },
- project_path_asc: { order_by: 'project_path', sort: 'asc' }
- }).freeze
+ project_path_desc: { order_by: 'project_path', sort: 'desc' },
+ project_path_asc: { order_by: 'project_path', sort: 'asc' }
+ }).freeze
def resolve(sort:, **filters)
return unless packages_available?
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index e3102a7d32a..3e61ba755d8 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -12,6 +12,11 @@ module Resolvers
# see app/graphql/types/issue_connection.rb
type 'Types::IssueConnection', null: true
+ before_connection_authorization do |nodes, current_user|
+ projects = nodes.map(&:project)
+ ::Preloaders::UserMaxAccessLevelInProjectsPreloader.new(projects, current_user).execute
+ end
+
def resolve_with_lookahead(**args)
return unless Feature.enabled?(:root_level_issues_query)
diff --git a/app/graphql/resolvers/package_details_resolver.rb b/app/graphql/resolvers/package_details_resolver.rb
index b77c6b1112b..c565fcb70e3 100644
--- a/app/graphql/resolvers/package_details_resolver.rb
+++ b/app/graphql/resolvers/package_details_resolver.rb
@@ -11,6 +11,14 @@ module Resolvers
description: 'Global ID of the package.'
def resolve(id:)
+ Gitlab::Graphql::Lazy.with_value(find_object(id: id)) do |package|
+ package if package.default?
+ end
+ end
+
+ private
+
+ def find_object(id:)
GitlabSchema.find_by_gid(id)
end
end
diff --git a/app/graphql/resolvers/package_pipelines_resolver.rb b/app/graphql/resolvers/package_pipelines_resolver.rb
index 9ff77f02547..7f610915489 100644
--- a/app/graphql/resolvers/package_pipelines_resolver.rb
+++ b/app/graphql/resolvers/package_pipelines_resolver.rb
@@ -18,7 +18,7 @@ module Resolvers
# This returns a promise for a connection of promises for pipelines:
# Lazy[Connection[Lazy[Pipeline]]] structure
- def resolve(first: nil, last: nil, after: nil, before: nil, lookahead:)
+ def resolve(lookahead:, first: nil, last: nil, after: nil, before: nil)
default_value = default_value_for(first: first, last: last, after: after, before: before)
BatchLoader::GraphQL.for(package.id)
.batch(default_value: default_value) do |package_ids, loader|
diff --git a/app/graphql/resolvers/paginated_tree_resolver.rb b/app/graphql/resolvers/paginated_tree_resolver.rb
index c7e9e522c25..6c4e978125e 100644
--- a/app/graphql/resolvers/paginated_tree_resolver.rb
+++ b/app/graphql/resolvers/paginated_tree_resolver.rb
@@ -41,7 +41,10 @@ module Resolvers
next_cursor = tree.cursor&.next_cursor
Gitlab::Graphql::ExternallyPaginatedArray.new(cursor, next_cursor, *tree)
rescue Gitlab::Git::CommandError => e
- raise Gitlab::Graphql::Errors::ArgumentError, e
+ raise Gitlab::Graphql::Errors::BaseError.new(
+ e,
+ extensions: { code: e.code, gitaly_code: e.status, service: e.service }
+ )
end
def self.field_options
diff --git a/app/graphql/resolvers/project_jobs_resolver.rb b/app/graphql/resolvers/project_jobs_resolver.rb
index 4d13a4a3fae..2bf71679dbf 100644
--- a/app/graphql/resolvers/project_jobs_resolver.rb
+++ b/app/graphql/resolvers/project_jobs_resolver.rb
@@ -14,10 +14,18 @@ module Resolvers
required: false,
description: 'Filter jobs by status.'
+ argument :with_artifacts, ::GraphQL::Types::Boolean,
+ required: false,
+ description: 'Filter by artifacts presence.'
+
alias_method :project, :object
- def resolve_with_lookahead(statuses: nil)
- jobs = ::Ci::JobsFinder.new(current_user: current_user, project: project, params: { scope: statuses }).execute
+ def resolve_with_lookahead(statuses: nil, with_artifacts: nil)
+ jobs = ::Ci::JobsFinder.new(
+ current_user: current_user, project: project, params: {
+ scope: statuses, with_artifacts: with_artifacts
+ }
+ ).execute
apply_lookahead(jobs)
end
@@ -26,7 +34,7 @@ module Resolvers
def preloads
{
- previous_stage_jobs_and_needs: [:needs, :pipeline],
+ previous_stage_jobs_or_needs: [:needs, :pipeline],
artifacts: [:job_artifacts],
pipeline: [:user]
}
diff --git a/app/graphql/resolvers/projects/fork_details_resolver.rb b/app/graphql/resolvers/projects/fork_details_resolver.rb
new file mode 100644
index 00000000000..fcc13a1bc1e
--- /dev/null
+++ b/app/graphql/resolvers/projects/fork_details_resolver.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Projects
+ class ForkDetailsResolver < BaseResolver
+ type Types::Projects::ForkDetailsType, null: true
+
+ argument :ref, GraphQL::Types::String,
+ required: false,
+ description: 'Ref of the fork. Default value is HEAD.'
+
+ alias_method :project, :object
+
+ def resolve(**args)
+ return unless project.forked?
+
+ ::Projects::Forks::DivergenceCounts.new(project, args[:ref]).counts
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb
new file mode 100644
index 00000000000..b40d85e8003
--- /dev/null
+++ b/app/graphql/resolvers/work_items/work_item_discussions_resolver.rb
@@ -0,0 +1,68 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module WorkItems
+ class WorkItemDiscussionsResolver < BaseResolver
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ extension Gitlab::Graphql::Extensions::ForwardOnlyExternallyPaginatedArrayExtension
+
+ authorize :read_work_item
+ authorizes_object!
+
+ # this resolver may be calling gitaly as part of parsing notes that contain commit references
+ calls_gitaly!
+
+ alias_method :notes_widget, :object
+
+ argument :filter, Types::WorkItems::NotesFilterTypeEnum,
+ required: false,
+ default_value: Types::WorkItems::NotesFilterTypeEnum.default_value,
+ description: 'Type of notes collection: ALL_NOTES, ONLY_COMMENTS, ONLY_ACTIVITY.'
+
+ type Types::Notes::DiscussionType.connection_type, null: true
+
+ def resolve(**args)
+ finder = Issuable::DiscussionsListService.new(current_user, work_item, params(args))
+
+ Gitlab::Graphql::ExternallyPaginatedArray.new(
+ finder.paginator.cursor_for_previous_page,
+ finder.paginator.cursor_for_next_page,
+ *finder.execute
+ )
+ end
+
+ def self.field_options
+ # we manage the pagination manually through external array, so opt out of the connection field extension
+ super.merge(connection: false)
+ end
+
+ def self.calculate_ext_conn_complexity
+ true
+ end
+
+ def self.complexity_multiplier(args)
+ 0.05
+ end
+
+ private
+
+ def work_item
+ notes_widget.work_item
+ end
+ strong_memoize_attr :work_item
+
+ def params(args)
+ {
+ notes_filter: args[:filter],
+ cursor: args[:after],
+ per_page: self.class.nodes_limit(args, @field, context: context)
+ }
+ end
+
+ def self.nodes_limit(args, field, **kwargs)
+ page_size = field&.max_page_size || kwargs[:context]&.schema&.default_max_page_size
+ [args[:first], page_size].compact.min
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/work_items_resolver.rb b/app/graphql/resolvers/work_items_resolver.rb
index 42f4f99d4a9..a3de875c196 100644
--- a/app/graphql/resolvers/work_items_resolver.rb
+++ b/app/graphql/resolvers/work_items_resolver.rb
@@ -55,6 +55,7 @@ module Resolvers
last_edited_by: :last_edited_by,
assignees: :assignees,
parent: :work_item_parent,
+ children: { work_item_children: [:author, { project: :project_feature }] },
labels: :labels,
milestone: :milestone
}
diff --git a/app/graphql/types/alert_management/alert_type.rb b/app/graphql/types/alert_management/alert_type.rb
index a0d19229d3d..a13453f9194 100644
--- a/app/graphql/types/alert_management/alert_type.rb
+++ b/app/graphql/types/alert_management/alert_type.rb
@@ -13,6 +13,11 @@ module Types
authorize :read_alert_management_alert
+ field :id,
+ GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the alert.'
+
field :iid,
GraphQL::Types::ID,
null: false,
@@ -116,7 +121,10 @@ module Types
null: true,
description: 'Runbook for the alert as defined in alert details.'
- field :todos, description: 'To-do items of the current user for the alert.', resolver: Resolvers::TodosResolver
+ field :todos,
+ Types::TodoType.connection_type,
+ description: 'To-do items of the current user for the alert.',
+ resolver: Resolvers::TodosResolver
field :details_url,
GraphQL::Types::String,
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 36ba3399754..615c143a0b9 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -135,15 +135,16 @@ module Types
:resolver_complexity, args, child_complexity: child_complexity
).to_i
complexity += 1 if calls_gitaly?
- complexity += complexity * connection_complexity_multiplier(ctx, args)
+ ext_conn = resolver&.try(:calculate_ext_conn_complexity)
+ complexity += complexity * connection_complexity_multiplier(ctx, args, calculate_ext_conn_complexity: ext_conn)
complexity.to_i
end
end
- def connection_complexity_multiplier(ctx, args)
+ def connection_complexity_multiplier(ctx, args, calculate_ext_conn_complexity:)
# Resolvers may add extra complexity depending on number of items being loaded.
- return 0 unless connection?
+ return 0 if !connection? && !calculate_ext_conn_complexity
page_size = max_page_size || ctx.schema.default_max_page_size
limit_value = [args[:first], args[:last], page_size].compact.min
diff --git a/app/graphql/types/ci/config_variable_type.rb b/app/graphql/types/ci/config_variable_type.rb
index 5b5890fd5a5..020af5b2444 100644
--- a/app/graphql/types/ci/config_variable_type.rb
+++ b/app/graphql/types/ci/config_variable_type.rb
@@ -19,6 +19,7 @@ module Types
description: 'Value of the variable.'
field :value_options, [GraphQL::Types::String],
+ hash_key: :options,
null: true,
description: 'Value options for the variable.'
end
diff --git a/app/graphql/types/ci/freeze_period_status_enum.rb b/app/graphql/types/ci/freeze_period_status_enum.rb
new file mode 100644
index 00000000000..aebd0f537e9
--- /dev/null
+++ b/app/graphql/types/ci/freeze_period_status_enum.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class FreezePeriodStatusEnum < BaseEnum
+ graphql_name 'CiFreezePeriodStatus'
+ description 'Deploy freeze period status'
+
+ value 'ACTIVE', value: :active, description: 'Freeze period is active.'
+ value 'INACTIVE', value: :inactive, description: 'Freeze period is inactive.'
+ end
+ end
+end
diff --git a/app/graphql/types/ci/freeze_period_type.rb b/app/graphql/types/ci/freeze_period_type.rb
new file mode 100644
index 00000000000..6a3f2ed8fa4
--- /dev/null
+++ b/app/graphql/types/ci/freeze_period_type.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class FreezePeriodType < BaseObject
+ graphql_name 'CiFreezePeriod'
+ description 'Represents a deployment freeze window of a project'
+
+ authorize :read_freeze_period
+
+ present_using ::Ci::FreezePeriodPresenter
+
+ field :status, Types::Ci::FreezePeriodStatusEnum,
+ description: 'Freeze period status.',
+ null: false
+
+ field :start_cron, GraphQL::Types::String,
+ description: 'Start of the freeze period in cron format.',
+ null: false,
+ method: :freeze_start
+
+ field :end_cron, GraphQL::Types::String,
+ description: 'End of the freeze period in cron format.',
+ null: false,
+ method: :freeze_end
+
+ field :cron_timezone, GraphQL::Types::String,
+ description: 'Time zone for the cron fields, defaults to UTC if not provided.',
+ null: true
+
+ field :start_time, Types::TimeType,
+ description: 'Timestamp (UTC) of when the current/next active period starts.',
+ null: true
+
+ field :end_time, Types::TimeType,
+ description: 'Timestamp (UTC) of when the current/next active period ends.',
+ null: true,
+ method: :time_end_from_now
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_schedule_type.rb b/app/graphql/types/ci/pipeline_schedule_type.rb
index 04f9fc78a92..904fa3f1c72 100644
--- a/app/graphql/types/ci/pipeline_schedule_type.rb
+++ b/app/graphql/types/ci/pipeline_schedule_type.rb
@@ -5,6 +5,8 @@ module Types
class PipelineScheduleType < BaseObject
graphql_name 'PipelineSchedule'
+ description 'Represents a pipeline schedule'
+
connection_type_class(Types::CountableConnectionType)
expose_permissions Types::PermissionTypes::Ci::PipelineSchedules
@@ -17,7 +19,9 @@ module Types
field :owner, ::Types::UserType, null: false, description: 'Owner of the pipeline schedule.'
- field :active, GraphQL::Types::Boolean, null: false, description: 'Indicates if a pipeline schedule is active.'
+ field :active, GraphQL::Types::Boolean, null: false, description: 'Indicates if the pipeline schedule is active.'
+
+ field :project, ::Types::ProjectType, null: true, description: 'Project of the pipeline schedule.'
field :next_run_at, Types::TimeType, null: false, description: 'Time when the next pipeline will run.'
@@ -26,20 +30,50 @@ module Types
field :last_pipeline, PipelineType, null: true, description: 'Last pipeline object.'
field :ref_for_display, GraphQL::Types::String,
- null: true, description: 'Git ref for the pipeline schedule.', method: :ref_for_display
-
- field :ref_path, GraphQL::Types::String, null: true, description: 'Path to the ref that triggered the pipeline.'
+ null: true, description: 'Git ref for the pipeline schedule.'
field :for_tag, GraphQL::Types::Boolean,
null: false, description: 'Indicates if a pipelines schedule belongs to a tag.', method: :for_tag?
- field :cron, GraphQL::Types::String, null: false, description: 'Cron notation for the schedule.'
+ field :edit_path, GraphQL::Types::String,
+ null: true,
+ description: 'Edit path of the pipeline schedule.',
+ authorize: :update_pipeline_schedule
+
+ field :variables,
+ Types::Ci::PipelineScheduleVariableType.connection_type,
+ null: true,
+ description: 'Pipeline schedule variables.',
+ authorize: :read_pipeline_schedule_variables
+
+ field :ref, GraphQL::Types::String,
+ null: true, description: 'Ref of the pipeline schedule.', method: :ref_for_display
+
+ field :ref_path, GraphQL::Types::String,
+ null: true,
+ description: 'Path to the ref that triggered the pipeline.'
- field :cron_timezone, GraphQL::Types::String, null: false, description: 'Timezone for the pipeline schedule.'
+ field :cron, GraphQL::Types::String,
+ null: false,
+ description: 'Cron notation for the schedule.'
+
+ field :cron_timezone, GraphQL::Types::String,
+ null: false,
+ description: 'Timezone for the pipeline schedule.'
+
+ field :created_at, Types::TimeType,
+ null: false, description: 'Timestamp of when the pipeline schedule was created.'
+
+ field :updated_at, Types::TimeType,
+ 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)
end
+
+ def edit_path
+ ::Gitlab::Routing.url_helpers.edit_project_pipeline_schedule_path(object.project, object)
+ end
end
end
end
diff --git a/app/graphql/types/ci/pipeline_schedule_variable_type.rb b/app/graphql/types/ci/pipeline_schedule_variable_type.rb
new file mode 100644
index 00000000000..1cb407bc2e4
--- /dev/null
+++ b/app/graphql/types/ci/pipeline_schedule_variable_type.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class PipelineScheduleVariableType < BaseObject
+ graphql_name 'PipelineScheduleVariable'
+
+ authorize :read_pipeline_schedule_variables
+
+ implements(VariableInterface)
+ end
+ end
+end
diff --git a/app/graphql/types/ci/pipeline_type.rb b/app/graphql/types/ci/pipeline_type.rb
index 4a523f2edd9..cb561f48b3b 100644
--- a/app/graphql/types/ci/pipeline_type.rb
+++ b/app/graphql/types/ci/pipeline_type.rb
@@ -78,7 +78,7 @@ module Types
resolver: Resolvers::Ci::PipelineStagesResolver
field :user,
- type: Types::UserType,
+ type: 'Types::UserType',
null: true,
description: 'Pipeline user.'
diff --git a/app/graphql/types/ci/runner_job_execution_status_enum.rb b/app/graphql/types/ci/runner_job_execution_status_enum.rb
new file mode 100644
index 00000000000..686ea085199
--- /dev/null
+++ b/app/graphql/types/ci/runner_job_execution_status_enum.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ class RunnerJobExecutionStatusEnum < BaseEnum
+ graphql_name 'CiRunnerJobExecutionStatus'
+
+ value 'IDLE',
+ description: "Runner is idle.",
+ value: :idle,
+ deprecated: { milestone: '15.7', reason: :alpha }
+
+ value 'RUNNING',
+ description: 'Runner is executing jobs.',
+ value: :running,
+ deprecated: { milestone: '15.7', reason: :alpha }
+ end
+ end
+end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index a9c76974850..5d34906f7b8 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -23,6 +23,9 @@ module Types
deprecated: { reason: 'Use paused', milestone: '14.8' }
field :admin_url, GraphQL::Types::String, null: true,
description: 'Admin URL of the runner. Only available for administrators.'
+ field :architecture_name, GraphQL::Types::String, null: true,
+ description: 'Architecture provided by the the runner.',
+ method: :architecture
field :contacted_at, Types::TimeType, null: true,
description: 'Timestamp of last contact from this runner.',
method: :contacted_at
@@ -35,32 +38,39 @@ module Types
field :executor_name, GraphQL::Types::String, null: true,
description: 'Executor last advertised by the runner.',
method: :executor_name
- field :platform_name, GraphQL::Types::String, null: true,
- description: 'Platform provided by the runner.',
- method: :platform
- field :architecture_name, GraphQL::Types::String, null: true,
- description: 'Architecture provided by the the runner.',
- method: :architecture
- field :maintenance_note, GraphQL::Types::String, null: true,
- description: 'Runner\'s maintenance notes.'
- field :groups, ::Types::GroupType.connection_type, null: true,
- description: 'Groups the runner is associated with. For group runners only.'
+ field :groups, 'Types::GroupConnection',
+ null: true,
+ resolver: ::Resolvers::Ci::RunnerGroupsResolver,
+ description: 'Groups the runner is associated with. For group runners only.'
field :id, ::Types::GlobalIDType[::Ci::Runner], null: false,
description: 'ID of the runner.'
field :ip_address, GraphQL::Types::String, null: true,
description: 'IP address of the runner.'
field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
+ field :job_execution_status,
+ Types::Ci::RunnerJobExecutionStatusEnum,
+ null: true,
+ description: 'Job execution status of the runner.',
+ deprecated: { milestone: '15.7', reason: :alpha }
field :jobs, ::Types::Ci::JobType.connection_type, null: true,
description: 'Jobs assigned to the runner. This field can only be resolved for one runner in any single request.',
authorize: :read_builds,
resolver: ::Resolvers::Ci::RunnerJobsResolver
field :locked, GraphQL::Types::Boolean, null: true,
description: 'Indicates the runner is locked.'
+ field :maintenance_note, GraphQL::Types::String, null: true,
+ description: 'Runner\'s maintenance notes.'
field :maximum_timeout, GraphQL::Types::Int, null: true,
description: 'Maximum timeout (in seconds) for jobs processed by the runner.'
+ field :owner_project, ::Types::ProjectType, null: true,
+ description: 'Project that owns the runner. For project runners only.',
+ resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver
field :paused, GraphQL::Types::Boolean, null: false,
description: 'Indicates the runner is paused and not available to run jobs.'
+ field :platform_name, GraphQL::Types::String, null: true,
+ description: 'Platform provided by the runner.',
+ method: :platform
field :project_count, GraphQL::Types::Int, null: true,
description: 'Number of projects that the runner is associated with.'
field :projects,
@@ -88,9 +98,6 @@ module Types
method: :token_expires_at
field :version, GraphQL::Types::String, null: true,
description: 'Version of the runner.'
- field :owner_project, ::Types::ProjectType, null: true,
- description: 'Project that owns the runner. For project runners only.',
- resolver: ::Resolvers::Ci::RunnerOwnerProjectResolver
markdown_field :maintenance_note_html, null: true
@@ -99,8 +106,25 @@ module Types
end
def job_count
- # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
- runner.builds.limit(JOB_COUNT_LIMIT + 1).count
+ BatchLoader::GraphQL.for(runner.id).batch(key: :job_count) do |runner_ids, loader, _args|
+ # rubocop: disable CodeReuse/ActiveRecord
+ # We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
+ builds_tbl = ::Ci::Build.arel_table
+ runners_tbl = ::Ci::Runner.arel_table
+ lateral_query = ::Ci::Build.select(1)
+ .where(builds_tbl['runner_id'].eq(runners_tbl['id']))
+ .limit(JOB_COUNT_LIMIT + 1)
+ counts = ::Ci::Runner.joins("JOIN LATERAL (#{lateral_query.to_sql}) builds_with_limit ON true")
+ .id_in(runner_ids)
+ .select(:id, Arel.star.count.as('count'))
+ .group(:id)
+ .index_by(&:id)
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ runner_ids.each do |runner_id|
+ loader.call(runner_id, counts[runner_id]&.count || 0)
+ end
+ end
end
def admin_url
@@ -111,14 +135,13 @@ module Types
Gitlab::Routing.url_helpers.edit_admin_runner_url(runner) if can_admin_runners?
end
- # rubocop: disable CodeReuse/ActiveRecord
def project_count
BatchLoader::GraphQL.for(runner.id).batch(key: :runner_project_count) do |ids, loader, args|
counts = ::Ci::Runner.project_type
.select(:id, 'COUNT(ci_runner_projects.id) as count')
.left_outer_joins(:runner_projects)
- .where(id: ids)
- .group(:id)
+ .id_in(ids)
+ .group(:id) # rubocop: disable CodeReuse/ActiveRecord
.index_by(&:id)
ids.each do |id|
@@ -126,12 +149,15 @@ module Types
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
- def groups
- return unless runner.group_type?
+ def job_execution_status
+ BatchLoader::GraphQL.for(runner.id).batch(key: :running_builds_exist) do |runner_ids, loader|
+ statuses = ::Ci::Runner.id_in(runner_ids).with_running_builds.index_by(&:id)
- batched_owners(::Ci::RunnerNamespace, Group, :runner_groups, :namespace_id)
+ runner_ids.each do |runner_id|
+ loader.call(runner_id, statuses[runner_id] ? :running : :idle)
+ end
+ end
end
private
@@ -139,29 +165,6 @@ module Types
def can_admin_runners?
context[:current_user]&.can_admin_all_resources?
end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def batched_owners(runner_assoc_type, assoc_type, key, column_name)
- BatchLoader::GraphQL.for(runner.id).batch(key: key) do |runner_ids, loader|
- plucked_runner_and_owner_ids = runner_assoc_type
- .select(:runner_id, column_name)
- .where(runner_id: runner_ids)
- .pluck(:runner_id, column_name)
- # In plucked_runner_and_owner_ids, first() represents the runner ID, and second() the owner ID,
- # so let's group the owner IDs by runner ID
- runner_owner_ids_by_runner_id = plucked_runner_and_owner_ids
- .group_by(&:first)
- .transform_values { |runner_and_owner_id| runner_and_owner_id.map(&:second) }
-
- owner_ids = runner_owner_ids_by_runner_id.values.flatten.uniq
- owners = assoc_type.where(id: owner_ids).index_by(&:id)
-
- runner_ids.each do |runner_id|
- loader.call(runner_id, runner_owner_ids_by_runner_id[runner_id]&.map { |owner_id| owners[owner_id] } || [])
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
end
end
end
diff --git a/app/graphql/types/commit_signature_interface.rb b/app/graphql/types/commit_signature_interface.rb
index 6b0c16e538a..0449a0634ef 100644
--- a/app/graphql/types/commit_signature_interface.rb
+++ b/app/graphql/types/commit_signature_interface.rb
@@ -21,7 +21,8 @@ module Types
description: 'Project of the associated commit.'
orphan_types Types::CommitSignatures::GpgSignatureType,
- Types::CommitSignatures::X509SignatureType
+ Types::CommitSignatures::X509SignatureType,
+ Types::CommitSignatures::SshSignatureType
def self.resolve_type(object, context)
case object
@@ -29,6 +30,8 @@ module Types
Types::CommitSignatures::GpgSignatureType
when ::CommitSignatures::X509CommitSignature
Types::CommitSignatures::X509SignatureType
+ when ::CommitSignatures::SshSignature
+ Types::CommitSignatures::SshSignatureType
else
raise 'Unsupported commit signature type'
end
diff --git a/app/graphql/types/commit_signatures/gpg_signature_type.rb b/app/graphql/types/commit_signatures/gpg_signature_type.rb
index 2a845fff3e2..3baf2d9d21d 100644
--- a/app/graphql/types/commit_signatures/gpg_signature_type.rb
+++ b/app/graphql/types/commit_signatures/gpg_signature_type.rb
@@ -11,6 +11,7 @@ module Types
authorize :download_code
field :user, Types::UserType, null: true,
+ method: :signed_by_user,
description: 'User associated with the key.'
field :gpg_key_user_name, GraphQL::Types::String,
diff --git a/app/graphql/types/commit_signatures/ssh_signature_type.rb b/app/graphql/types/commit_signatures/ssh_signature_type.rb
new file mode 100644
index 00000000000..92eb4f7949a
--- /dev/null
+++ b/app/graphql/types/commit_signatures/ssh_signature_type.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Types
+ module CommitSignatures
+ class SshSignatureType < Types::BaseObject
+ graphql_name 'SshSignature'
+ description 'SSH signature for a signed commit'
+
+ implements Types::CommitSignatureInterface
+
+ authorize :download_code
+
+ field :user, Types::UserType, null: true,
+ method: :signed_by_user,
+ calls_gitaly: true,
+ description: 'User associated with the key.'
+
+ field :key, Types::KeyType,
+ null: true,
+ description: 'SSH key used for the signature.'
+ end
+ end
+end
diff --git a/app/graphql/types/commit_signatures/x509_signature_type.rb b/app/graphql/types/commit_signatures/x509_signature_type.rb
index 9ac96dbc015..2d58c3d5b5d 100644
--- a/app/graphql/types/commit_signatures/x509_signature_type.rb
+++ b/app/graphql/types/commit_signatures/x509_signature_type.rb
@@ -11,6 +11,7 @@ module Types
authorize :download_code
field :user, Types::UserType, null: true,
+ method: :signed_by_user,
calls_gitaly: true,
description: 'User associated with the key.'
diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb
index cb818ac5e92..dfa599e798c 100644
--- a/app/graphql/types/container_repository_type.rb
+++ b/app/graphql/types/container_repository_type.rb
@@ -13,6 +13,7 @@ module Types
field :expiration_policy_cleanup_status, Types::ContainerRepositoryCleanupStatusEnum, null: true, description: 'Tags cleanup status for the container repository.'
field :expiration_policy_started_at, Types::TimeType, null: true, description: 'Timestamp when the cleanup done by the expiration policy was started on the container repository.'
field :id, GraphQL::Types::ID, null: false, description: 'ID of the container repository.'
+ field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.'
field :location, GraphQL::Types::String, null: false, description: 'URL of the container repository.'
field :migration_state, GraphQL::Types::String, null: false, description: 'Migration state of the container repository.'
field :name, GraphQL::Types::String, null: false, description: 'Name of the container repository.'
@@ -21,7 +22,6 @@ module Types
field :status, Types::ContainerRepositoryStatusEnum, null: true, description: 'Status of the container repository.'
field :tags_count, GraphQL::Types::Int, null: false, description: 'Number of tags associated with this image.'
field :updated_at, Types::TimeType, null: false, description: 'Timestamp when the container repository was updated.'
- field :last_cleanup_deleted_tags_count, GraphQL::Types::Int, null: true, description: 'Number of deleted tags from the last cleanup.'
def can_delete
Ability.allowed?(current_user, :update_container_image, object)
diff --git a/app/graphql/types/dependency_proxy/manifest_type.rb b/app/graphql/types/dependency_proxy/manifest_type.rb
index f7e751e30d3..53b7610e490 100644
--- a/app/graphql/types/dependency_proxy/manifest_type.rb
+++ b/app/graphql/types/dependency_proxy/manifest_type.rb
@@ -14,11 +14,11 @@ module Types
field :id, ::Types::GlobalIDType[::DependencyProxy::Manifest], null: false, description: 'ID of the manifest.'
field :image_name, GraphQL::Types::String, null: false, description: 'Name of the image.'
field :size, GraphQL::Types::String, null: false, description: 'Size of the manifest file.'
- field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
field :status,
Types::DependencyProxy::ManifestTypeEnum,
null: false,
description: "Status of the manifest (#{::DependencyProxy::Manifest.statuses.keys.join(', ')})"
+ field :updated_at, Types::TimeType, null: false, description: 'Date of most recent update.'
def image_name
object.file_name.chomp(File.extname(object.file_name))
diff --git a/app/graphql/types/deployment_details_type.rb b/app/graphql/types/deployment_details_type.rb
deleted file mode 100644
index bbb5cc8e3f1..00000000000
--- a/app/graphql/types/deployment_details_type.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-# frozen_string_literal: true
-
-module Types
- class DeploymentDetailsType < DeploymentType
- graphql_name 'DeploymentDetails'
- description 'The details of the deployment'
- authorize :read_deployment
- present_using ::Deployments::DeploymentPresenter
-
- field :tags,
- [Types::DeploymentTagType],
- description: 'Git tags that contain this deployment.',
- calls_gitaly: true
- end
-end
-
-Types::DeploymentDetailsType.prepend_mod_with('Types::DeploymentDetailsType')
diff --git a/app/graphql/types/deployment_type.rb b/app/graphql/types/deployment_type.rb
index 59b59dc4e1d..1c23fd44ea1 100644
--- a/app/graphql/types/deployment_type.rb
+++ b/app/graphql/types/deployment_type.rb
@@ -1,12 +1,6 @@
# frozen_string_literal: true
module Types
- # If you're considering to add a new field in DeploymentType, please follow this guideline:
- # - If the field is preloadable in batch, define it in DeploymentType.
- # In this case, you should extend DeploymentsResolver logic to preload the field. Also, add a new test that
- # fetching the specific field for multiple deployments doesn't cause N+1 query problem.
- # - If the field is NOT preloadable in batch, define it in DeploymentDetailsType.
- # This type can be only fetched for a single deployment, so you don't need to take care of the preloading.
class DeploymentType < BaseObject
graphql_name 'Deployment'
description 'The deployment of an environment'
@@ -15,6 +9,8 @@ module Types
authorize :read_deployment
+ expose_permissions Types::PermissionTypes::Deployment
+
field :id,
GraphQL::Types::ID,
description: 'Global ID of the deployment.'
@@ -65,5 +61,15 @@ module Types
Types::UserType,
description: 'User who executed the deployment.',
method: :deployed_by
+
+ field :tags,
+ [Types::DeploymentTagType],
+ description: 'Git tags that contain this deployment. ' \
+ 'This field can only be resolved for one deployment in any single request.',
+ calls_gitaly: true do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
end
end
+
+Types::DeploymentType.prepend_mod_with('Types::DeploymentType')
diff --git a/app/graphql/types/environment_type.rb b/app/graphql/types/environment_type.rb
index dd2286d333d..5f58fc38540 100644
--- a/app/graphql/types/environment_type.rb
+++ b/app/graphql/types/environment_type.rb
@@ -9,6 +9,12 @@ module Types
authorize :read_environment
+ expose_permissions Types::PermissionTypes::Environment,
+ description: 'Permissions for the current user on the resource. '\
+ 'This field can only be resolved for one environment in any single request.' do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
+
field :name, GraphQL::Types::String, null: false,
description: 'Human-readable name of the environment.'
@@ -67,6 +73,11 @@ module Types
description: 'Last deployment of the environment.',
resolver: Resolvers::Environments::LastDeploymentResolver
+ field :deploy_freezes,
+ [Types::Ci::FreezePeriodType],
+ null: true,
+ description: 'Deployment freeze periods of the environment.'
+
def tier
object.tier.to_sym
end
diff --git a/app/graphql/types/global_id_type.rb b/app/graphql/types/global_id_type.rb
index a71c2fb0e6c..7ebd98ff2e7 100644
--- a/app/graphql/types/global_id_type.rb
+++ b/app/graphql/types/global_id_type.rb
@@ -49,9 +49,7 @@ module Types
An example `#{graphql_name}` is: `"#{::Gitlab::GlobalId.build(model_name: model_name, id: 1)}"`.
#{
if deprecation = Gitlab::GlobalId::Deprecations.deprecation_by(model_name)
- 'The older format `"' +
- ::Gitlab::GlobalId.build(model_name: deprecation.old_name, id: 1).to_s +
- '"` was deprecated in ' + deprecation.milestone + '.'
+ "The older format `\"#{::Gitlab::GlobalId.build(model_name: deprecation.old_name, id: 1)}\"` was deprecated in #{deprecation.milestone}."
end}
MD
diff --git a/app/graphql/types/group_connection.rb b/app/graphql/types/group_connection.rb
new file mode 100644
index 00000000000..e4332e24302
--- /dev/null
+++ b/app/graphql/types/group_connection.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+# Normally this wouldn't be needed and we could use
+#
+# type Types::GroupType.connection_type, null: true
+#
+# in a resolver. However we can end up with cyclic definitions.
+# Running the spec locally can result in errors like
+#
+# NameError: uninitialized constant Types::GroupType
+#
+# or other errors. To fix this, we created this file and use
+#
+# type "Types::GroupConnection", null: true
+#
+# which gives a delayed resolution, and the proper connection type.
+#
+# See gitlab/app/graphql/types/ci/runner_type.rb
+# Reference: https://github.com/rmosolgo/graphql-ruby/issues/3974#issuecomment-1084444214
+# and https://docs.gitlab.com/ee/development/api_graphql_styleguide.html#testing-tips-and-tricks
+#
+Types::GroupConnection = Types::GroupType.connection_type
diff --git a/app/graphql/types/issue_type_enum.rb b/app/graphql/types/issue_type_enum.rb
index 78cd27f60c3..d7f587ff03d 100644
--- a/app/graphql/types/issue_type_enum.rb
+++ b/app/graphql/types/issue_type_enum.rb
@@ -16,5 +16,9 @@ module Types
value 'OBJECTIVE', value: 'objective',
description: 'Objective issue type. Available only when feature flag `okrs_mvc` is enabled.',
alpha: { milestone: '15.6' }
+
+ value 'KEY_RESULT', value: 'key_result',
+ description: 'Key Result issue type. Available only when feature flag `okrs_mvc` is enabled.',
+ alpha: { milestone: '15.7' }
end
end
diff --git a/app/graphql/types/key_type.rb b/app/graphql/types/key_type.rb
new file mode 100644
index 00000000000..30699793045
--- /dev/null
+++ b/app/graphql/types/key_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ class KeyType < Types::BaseObject # rubocop:disable Graphql/AuthorizeTypes
+ graphql_name 'Key'
+ description 'Represents an SSH key.'
+
+ field :created_at, Types::TimeType, null: false,
+ description: 'Timestamp of when the key was created.'
+ field :expires_at, Types::TimeType, null: false,
+ description: "Timestamp of when the key expires. It's null if it never expires."
+ field :id, GraphQL::Types::ID, null: false, description: 'ID of the key.'
+ field :key, GraphQL::Types::String, null: false, method: :publishable_key,
+ description: 'Public key of the key pair.'
+ field :title, GraphQL::Types::String, null: false, description: 'Title of the key.'
+ end
+end
diff --git a/app/graphql/types/merge_request_type.rb b/app/graphql/types/merge_request_type.rb
index 49bf7aa638c..abf7b3ad530 100644
--- a/app/graphql/types/merge_request_type.rb
+++ b/app/graphql/types/merge_request_type.rb
@@ -200,10 +200,10 @@ module Types
description: 'Array of available auto merge strategies.'
field :commits, Types::CommitType.connection_type, null: true,
calls_gitaly: true, description: 'Merge request commits.'
- field :committers, Types::UserType.connection_type, null: true, complexity: 5,
- calls_gitaly: true, description: 'Users who have added commits to the merge request.'
field :commits_without_merge_commits, Types::CommitType.connection_type, null: true,
calls_gitaly: true, description: 'Merge request commits excluding merge commits.'
+ field :committers, Types::UserType.connection_type, null: true, complexity: 5,
+ calls_gitaly: true, description: 'Users who have added commits to the merge request.'
field :has_ci, GraphQL::Types::Boolean, null: false, method: :has_ci?,
description: 'Indicates if the merge request has CI.'
field :merge_user, Types::UserType, null: true,
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index 1cbb2ede544..b342e57804b 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -63,6 +63,8 @@ module Types
mount_mutation Mutations::Issues::SetEscalationStatus
mount_mutation Mutations::Issues::Update
mount_mutation Mutations::Issues::Move
+ mount_mutation Mutations::Issues::LinkAlerts
+ mount_mutation Mutations::Issues::UnlinkAlert
mount_mutation Mutations::Labels::Create
mount_mutation Mutations::MergeRequests::Accept
mount_mutation Mutations::MergeRequests::Create
@@ -117,6 +119,8 @@ module Types
mount_mutation Mutations::Ci::Pipeline::Retry
mount_mutation Mutations::Ci::PipelineSchedule::Delete
mount_mutation Mutations::Ci::PipelineSchedule::TakeOwnership
+ mount_mutation Mutations::Ci::PipelineSchedule::Play
+ mount_mutation Mutations::Ci::PipelineSchedule::Create
mount_mutation Mutations::Ci::CiCdSettingsUpdate, deprecated: {
reason: :renamed,
replacement: 'ProjectCiCdSettingsUpdate',
diff --git a/app/graphql/types/nested_environment_type.rb b/app/graphql/types/nested_environment_type.rb
new file mode 100644
index 00000000000..b835af2bf45
--- /dev/null
+++ b/app/graphql/types/nested_environment_type.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Types
+ # rubocop: disable Graphql/AuthorizeTypes
+ class NestedEnvironmentType < BaseObject
+ graphql_name 'NestedEnvironment'
+ description 'Describes where code is deployed for a project organized by folder.'
+
+ field :name, GraphQL::Types::String,
+ null: false, description: 'Human-readable name of the environment.'
+
+ field :size, GraphQL::Types::Int,
+ null: false, description: 'Number of environments nested in the folder.'
+
+ field :environment,
+ Types::EnvironmentType,
+ null: true, description: 'Latest environment in the folder.'
+
+ def environment
+ BatchLoader::GraphQL.for(object.last_id).batch do |environment_ids, loader|
+ Environment.id_in(environment_ids).each do |environment|
+ loader.call(environment.id, environment)
+ end
+ end
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+end
diff --git a/app/graphql/types/notes/note_type.rb b/app/graphql/types/notes/note_type.rb
index eef5ce40bde..05629ea9223 100644
--- a/app/graphql/types/notes/note_type.rb
+++ b/app/graphql/types/notes/note_type.rb
@@ -77,6 +77,14 @@ module Types
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
+
+ # We now support also SyntheticNote notes as a NoteType, but SyntheticNote does not have a real note ID,
+ # as SyntheticNote is generated dynamically from a ResourceEvent instance.
+ def id
+ return super unless object.is_a?(SyntheticNote)
+
+ ::Gitlab::GlobalId.build(object, model_name: object.class.to_s, id: object.discussion_id)
+ end
end
end
end
diff --git a/app/graphql/types/packages/package_links_type.rb b/app/graphql/types/packages/package_links_type.rb
index f16937530b9..eb29fb655bd 100644
--- a/app/graphql/types/packages/package_links_type.rb
+++ b/app/graphql/types/packages/package_links_type.rb
@@ -12,6 +12,8 @@ module Types
field :web_path, GraphQL::Types::String, null: true, description: 'Path to the package details page.'
def web_path
+ return unless object.default?
+
package_path(object)
end
end
diff --git a/app/graphql/types/permission_types/base_permission_type.rb b/app/graphql/types/permission_types/base_permission_type.rb
index 07e6e7a55d6..0192af25d0f 100644
--- a/app/graphql/types/permission_types/base_permission_type.rb
+++ b/app/graphql/types/permission_types/base_permission_type.rb
@@ -11,20 +11,20 @@ module Types
abilities.each { |ability| ability_field(ability) }
end
- def self.ability_field(ability, **kword_args)
+ def self.ability_field(ability, **kword_args, &block)
define_field_resolver_method(ability) unless resolving_keywords?(kword_args)
- permission_field(ability, **kword_args)
+ permission_field(ability, **kword_args, &block)
end
- def self.permission_field(name, **kword_args)
+ def self.permission_field(name, **kword_args, &block)
kword_args = kword_args.reverse_merge(
name: name,
type: GraphQL::Types::Boolean,
description: "Indicates the user can perform `#{name}` on this resource",
null: false)
- field(**kword_args) # rubocop:disable Graphql/Descriptions
+ field(**kword_args, &block) # rubocop:disable Graphql/Descriptions
end
def self.define_field_resolver_method(ability)
diff --git a/app/graphql/types/permission_types/deployment.rb b/app/graphql/types/permission_types/deployment.rb
new file mode 100644
index 00000000000..fce376552b1
--- /dev/null
+++ b/app/graphql/types/permission_types/deployment.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Deployment < BasePermissionType
+ graphql_name 'DeploymentPermissions'
+
+ abilities :destroy_deployment
+ ability_field :update_deployment, calls_gitaly: true
+ end
+ end
+end
+
+Types::PermissionTypes::Deployment.prepend_mod_with('Types::PermissionTypes::Deployment')
diff --git a/app/graphql/types/permission_types/environment.rb b/app/graphql/types/permission_types/environment.rb
new file mode 100644
index 00000000000..59c9fce64e5
--- /dev/null
+++ b/app/graphql/types/permission_types/environment.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ class Environment < BasePermissionType
+ graphql_name 'EnvironmentPermissions'
+
+ abilities :update_environment, :destroy_environment, :stop_environment
+ end
+ end
+end
diff --git a/app/graphql/types/permission_types/project.rb b/app/graphql/types/permission_types/project.rb
index f6a5563d367..c833b512222 100644
--- a/app/graphql/types/permission_types/project.rb
+++ b/app/graphql/types/permission_types/project.rb
@@ -17,7 +17,8 @@ module Types
:admin_wiki, :admin_project, :update_pages,
:admin_remote_mirror, :create_label, :update_wiki, :destroy_wiki,
:create_pages, :destroy_pages, :read_pages_content, :admin_operations,
- :read_merge_request, :read_design, :create_design, :destroy_design
+ :read_merge_request, :read_design, :create_design, :destroy_design,
+ :read_environment
permission_field :create_snippet
diff --git a/app/graphql/types/project_statistics_type.rb b/app/graphql/types/project_statistics_type.rb
index c43baf1280b..a1d721856a9 100644
--- a/app/graphql/types/project_statistics_type.rb
+++ b/app/graphql/types/project_statistics_type.rb
@@ -11,6 +11,10 @@ module Types
field :build_artifacts_size, GraphQL::Types::Float, null: false,
description: 'Build artifacts size of the project in bytes.'
+ field :container_registry_size,
+ GraphQL::Types::Float,
+ null: true,
+ description: 'Container Registry size of the project in bytes.'
field :lfs_objects_size,
GraphQL::Types::Float,
null: false,
@@ -29,9 +33,5 @@ module Types
description: 'Uploads size of the project in bytes.'
field :wiki_size, GraphQL::Types::Float, null: true,
description: 'Wiki size of the project in bytes.'
- field :container_registry_size,
- GraphQL::Types::Float,
- null: true,
- description: 'Container Registry size of the project in bytes.'
end
end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index 771dad00fb3..fe13ee7ef3c 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -258,8 +258,11 @@ module Types
field :environments,
Types::EnvironmentType.connection_type,
null: true,
- description: 'Environments of the project.',
- resolver: Resolvers::EnvironmentsResolver
+ description: 'Environments of the project. ' \
+ 'This field can only be resolved for one project in any single request.',
+ resolver: Resolvers::EnvironmentsResolver do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
field :environment,
Types::EnvironmentType,
@@ -267,8 +270,18 @@ module Types
description: 'A single environment of the project.',
resolver: Resolvers::EnvironmentsResolver.single
+ field :nested_environments,
+ Types::NestedEnvironmentType.connection_type,
+ null: true,
+ calls_gitaly: true,
+ description: 'Environments for this project with nested folders, ' \
+ 'can only be resolved for one project in any single request',
+ resolver: Resolvers::Environments::NestedEnvironmentsResolver do
+ extension ::Gitlab::Graphql::Limit::FieldCallCount, limit: 1
+ end
+
field :deployment,
- Types::DeploymentDetailsType,
+ Types::DeploymentType,
null: true,
description: 'Details of the deployment of the project.',
resolver: Resolvers::DeploymentResolver.single
@@ -526,6 +539,13 @@ module Types
resolver: Resolvers::Projects::ForkTargetsResolver,
description: 'Namespaces in which the current user can fork the project into.'
+ field :fork_details, Types::Projects::ForkDetailsType,
+ calls_gitaly: true,
+ alpha: { milestone: '15.7' },
+ authorize: :read_code,
+ resolver: Resolvers::Projects::ForkDetailsResolver,
+ description: 'Details of the fork project compared to its upstream project.'
+
field :branch_rules,
Types::Projects::BranchRuleType.connection_type,
null: true,
@@ -537,6 +557,11 @@ module Types
description: "Programming languages used in the project.",
calls_gitaly: true
+ field :runners, Types::Ci::RunnerType.connection_type,
+ null: true,
+ resolver: ::Resolvers::Ci::ProjectRunnersResolver,
+ description: "Find runners visible to the current user."
+
def timelog_categories
object.project_namespace.timelog_categories if Feature.enabled?(:timelog_categories)
end
diff --git a/app/graphql/types/projects/fork_details_type.rb b/app/graphql/types/projects/fork_details_type.rb
new file mode 100644
index 00000000000..88c17d89620
--- /dev/null
+++ b/app/graphql/types/projects/fork_details_type.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module Projects
+ # rubocop: disable Graphql/AuthorizeTypes
+ class ForkDetailsType < BaseObject
+ graphql_name 'ForkDetails'
+ description 'Details of the fork project compared to its upstream project.'
+
+ field :ahead, GraphQL::Types::Int,
+ null: true,
+ description: 'Number of commits ahead of upstream.'
+
+ field :behind, GraphQL::Types::Int,
+ null: true,
+ description: 'Number of commits behind upstream.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index 21cb3f9e06c..7263f792bae 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -67,7 +67,7 @@ module Types
end
field :package,
- description: 'Find a package. This field can only be resolved for one query in any single request.',
+ 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
field :user, Types::UserType,
diff --git a/app/graphql/types/release_type.rb b/app/graphql/types/release_type.rb
index a20e53ad1bd..8516256b433 100644
--- a/app/graphql/types/release_type.rb
+++ b/app/graphql/types/release_type.rb
@@ -13,9 +13,6 @@ module Types
present_using ReleasePresenter
- field :id, ::Types::GlobalIDType[Release],
- null: false,
- description: 'Global ID of the release.'
field :assets, Types::ReleaseAssetsType, null: true, method: :itself,
description: 'Assets of the release.'
field :created_at, Types::TimeType, null: true,
@@ -26,6 +23,11 @@ module Types
description: 'Description (also known as "release notes") of the release.'
field :evidences, Types::EvidenceType.connection_type, null: true,
description: 'Evidence for the release.'
+ field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?,
+ description: 'Indicates the release is an historical release.'
+ field :id, ::Types::GlobalIDType[Release],
+ null: false,
+ description: 'Global ID of the release.'
field :links, Types::ReleaseLinksType, null: true, method: :itself,
description: 'Links of the release.'
field :milestones, Types::MilestoneType.connection_type, null: true,
@@ -42,8 +44,6 @@ module Types
authorize: :read_code
field :upcoming_release, GraphQL::Types::Boolean, null: true, method: :upcoming_release?,
description: 'Indicates the release is an upcoming release.'
- field :historical_release, GraphQL::Types::Boolean, null: true, method: :historical_release?,
- description: 'Indicates the release is an historical release.'
field :author, Types::UserType, null: true,
description: 'User that created the release.'
diff --git a/app/graphql/types/root_storage_statistics_type.rb b/app/graphql/types/root_storage_statistics_type.rb
index b1b712aab38..64aaf3e73a0 100644
--- a/app/graphql/types/root_storage_statistics_type.rb
+++ b/app/graphql/types/root_storage_statistics_type.rb
@@ -7,6 +7,7 @@ module Types
authorize :read_statistics
field :build_artifacts_size, GraphQL::Types::Float, null: false, description: 'CI artifacts size in bytes.'
+ field :container_registry_size, GraphQL::Types::Float, null: false, description: 'Container Registry size in bytes.'
field :dependency_proxy_size, GraphQL::Types::Float, null: false, description: 'Dependency Proxy sizes in bytes.'
field :lfs_objects_size, GraphQL::Types::Float, null: false, description: 'LFS objects size in bytes.'
field :packages_size, GraphQL::Types::Float, null: false, description: 'Packages size in bytes.'
@@ -16,6 +17,5 @@ module Types
field :storage_size, GraphQL::Types::Float, null: false, description: 'Total storage in bytes.'
field :uploads_size, GraphQL::Types::Float, null: false, description: 'Uploads size in bytes.'
field :wiki_size, GraphQL::Types::Float, null: false, description: 'Wiki size in bytes.'
- field :container_registry_size, GraphQL::Types::Float, null: false, description: 'Container Registry size in bytes.'
end
end
diff --git a/app/graphql/types/subscription_type.rb b/app/graphql/types/subscription_type.rb
index 9d5edec82b2..f7f26ba4c5a 100644
--- a/app/graphql/types/subscription_type.rb
+++ b/app/graphql/types/subscription_type.rb
@@ -34,6 +34,11 @@ module Types
subscription: Subscriptions::IssuableUpdated,
null: true,
description: 'Triggered when the merge status of a merge request is updated.'
+
+ field :merge_request_approval_state_updated,
+ subscription: Subscriptions::IssuableUpdated,
+ null: true,
+ description: 'Triggered when approval state of a merge request is updated.'
end
end
diff --git a/app/graphql/types/todo_action_enum.rb b/app/graphql/types/todo_action_enum.rb
index ef43b6eb464..33e1c4e98a4 100644
--- a/app/graphql/types/todo_action_enum.rb
+++ b/app/graphql/types/todo_action_enum.rb
@@ -5,11 +5,12 @@ module Types
value 'assigned', value: 1, description: 'User was assigned.'
value 'mentioned', value: 2, description: 'User was mentioned.'
value 'build_failed', value: 3, description: 'Build triggered by the user failed.'
- value 'marked', value: 4, description: 'User added a TODO.'
+ value 'marked', value: 4, description: 'User added a to-do item.'
value 'approval_required', value: 5, description: 'User was set as an approver.'
value 'unmergeable', value: 6, description: 'Merge request authored by the user could not be merged.'
value 'directly_addressed', value: 7, description: 'User was directly addressed.'
value 'merge_train_removed', value: 8, description: 'Merge request authored by the user was removed from the merge train.'
value 'review_requested', value: 9, description: 'Review was requested from the user.'
+ value 'member_access_requested', value: 10, description: 'Group access requested from the user.'
end
end
diff --git a/app/graphql/types/todo_type.rb b/app/graphql/types/todo_type.rb
index 0de6b1d6f8a..6e5ce35033b 100644
--- a/app/graphql/types/todo_type.rb
+++ b/app/graphql/types/todo_type.rb
@@ -15,13 +15,11 @@ module Types
field :project, Types::ProjectType,
description: 'Project this to-do item is associated with.',
- null: true,
- authorize: :read_project
+ null: true
field :group, 'Types::GroupType',
description: 'Group this to-do item is associated with.',
- null: true,
- authorize: :read_group
+ null: true
field :author, Types::UserType,
description: 'Author of this to-do item.',
diff --git a/app/graphql/types/user_interface.rb b/app/graphql/types/user_interface.rb
index f49b3eee4f5..51046d09f90 100644
--- a/app/graphql/types/user_interface.rb
+++ b/app/graphql/types/user_interface.rb
@@ -88,7 +88,10 @@ module Types
null: true,
description: 'Personal namespace of the user.'
- field :todos, resolver: Resolvers::TodosResolver, description: 'To-do items of the user.'
+ field :todos,
+ Types::TodoType.connection_type,
+ description: 'To-do items of the user.',
+ resolver: Resolvers::TodosResolver
# Merge request field: MRs can be authored, assigned, or assigned-for-review:
field :authored_merge_requests,
diff --git a/app/graphql/types/work_items/notes_filter_type_enum.rb b/app/graphql/types/work_items/notes_filter_type_enum.rb
new file mode 100644
index 00000000000..93fb4689f0b
--- /dev/null
+++ b/app/graphql/types/work_items/notes_filter_type_enum.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ class NotesFilterTypeEnum < BaseEnum
+ graphql_name 'NotesFilterType'
+ description 'Work item notes collection type.'
+
+ ::UserPreference::NOTES_FILTERS.each_pair do |key, value|
+ value key.upcase,
+ value: value,
+ description: UserPreference.notes_filters.invert[::UserPreference::NOTES_FILTERS[key]]
+ end
+
+ def self.default_value
+ ::UserPreference::NOTES_FILTERS[:all_notes]
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/work_items/widget_interface.rb b/app/graphql/types/work_items/widget_interface.rb
index b85d0a23535..672a78f12e1 100644
--- a/app/graphql/types/work_items/widget_interface.rb
+++ b/app/graphql/types/work_items/widget_interface.rb
@@ -17,7 +17,8 @@ module Types
::Types::WorkItems::Widgets::LabelsType,
::Types::WorkItems::Widgets::AssigneesType,
::Types::WorkItems::Widgets::StartAndDueDateType,
- ::Types::WorkItems::Widgets::MilestoneType
+ ::Types::WorkItems::Widgets::MilestoneType,
+ ::Types::WorkItems::Widgets::NotesType
].freeze
def self.ce_orphan_types
@@ -41,6 +42,8 @@ module Types
::Types::WorkItems::Widgets::StartAndDueDateType
when ::WorkItems::Widgets::Milestone
::Types::WorkItems::Widgets::MilestoneType
+ when ::WorkItems::Widgets::Notes
+ ::Types::WorkItems::Widgets::NotesType
else
raise "Unknown GraphQL type for widget #{object}"
end
diff --git a/app/graphql/types/work_items/widgets/hierarchy_type.rb b/app/graphql/types/work_items/widgets/hierarchy_type.rb
index 0ccd8af7dc8..4ec8ec84779 100644
--- a/app/graphql/types/work_items/widgets/hierarchy_type.rb
+++ b/app/graphql/types/work_items/widgets/hierarchy_type.rb
@@ -20,8 +20,29 @@ module Types
null: true, complexity: 5,
description: 'Child work items.'
+ field :has_children, GraphQL::Types::Boolean,
+ null: false, description: 'Indicates if the work item has children.'
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def has_children?
+ BatchLoader::GraphQL.for(object.work_item.id).batch(default_value: false) do |ids, loader|
+ links_for_parents = ::WorkItems::ParentLink.for_parents(ids)
+ .select(:work_item_parent_id)
+ .group(:work_item_parent_id)
+ .reorder(nil)
+
+ links_for_parents.each { |link| loader.call(link.work_item_parent_id, true) }
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ alias_method :has_children, :has_children?
+
def children
- object.children.inc_relations_for_permission_check
+ relation = object.children
+ relation = relation.inc_relations_for_permission_check unless object.children.loaded?
+
+ relation
end
end
# rubocop:enable Graphql/AuthorizeTypes
diff --git a/app/graphql/types/work_items/widgets/notes_type.rb b/app/graphql/types/work_items/widgets/notes_type.rb
new file mode 100644
index 00000000000..7da2777beee
--- /dev/null
+++ b/app/graphql/types/work_items/widgets/notes_type.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module Types
+ module WorkItems
+ module Widgets
+ # Disabling widget level authorization as it might be too granular
+ # and we already authorize the parent work item
+ # rubocop:disable Graphql/AuthorizeTypes
+ class NotesType < BaseObject
+ graphql_name 'WorkItemWidgetNotes'
+ description 'Represents a notes widget'
+
+ implements Types::WorkItems::WidgetInterface
+
+ # This field loads user comments, system notes and resource events as a discussion for an work item,
+ # raising the complexity considerably. In order to discourage fetching this field as part of fetching
+ # a list of issues we raise the complexity
+ field :discussions, Types::Notes::DiscussionType.connection_type,
+ null: true,
+ description: "Notes on this work item.",
+ resolver: Resolvers::WorkItems::WorkItemDiscussionsResolver
+ end
+ # rubocop:enable Graphql/AuthorizeTypes
+ end
+ end
+end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 340f3d45365..c78563a9a5f 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -124,7 +124,7 @@ module ApplicationHelper
end
def simple_sanitize(str)
- sanitize(str, tags: %w(a span))
+ sanitize(str, tags: %w[a span])
end
def body_data
@@ -187,14 +187,14 @@ module ApplicationHelper
css_classes << html_class unless html_class.blank?
content_tag :time, l(time, format: "%b %d, %Y"),
- class: css_classes.join(' '),
- title: l(time.to_time.in_time_zone, format: :timeago_tooltip),
- datetime: time.to_time.getutc.iso8601,
- data: {
- toggle: 'tooltip',
- placement: placement,
- container: 'body'
- }
+ class: css_classes.join(' '),
+ title: l(time.to_time.in_time_zone, format: :timeago_tooltip),
+ datetime: time.to_time.getutc.iso8601,
+ data: {
+ toggle: 'tooltip',
+ placement: placement,
+ container: 'body'
+ }
end
def edited_time_ago_with_tooltip(object, placement: 'top', html_class: 'time_ago', exclude_author: false)
@@ -234,11 +234,11 @@ module ApplicationHelper
end
def promo_url
- 'https://' + promo_host
+ "https://#{promo_host}"
end
def support_url
- Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || promo_url + '/getting-help/'
+ Gitlab::CurrentSettings.current_application_settings.help_page_support_url.presence || "#{promo_url}/getting-help/"
end
def instance_review_permitted?
@@ -279,7 +279,19 @@ module ApplicationHelper
end
def stylesheet_link_tag_defer(path)
- stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
+ if startup_css_enabled?
+ stylesheet_link_tag(path, media: "print", crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
+ else
+ stylesheet_link_tag(path, crossorigin: ActionController::Base.asset_host ? 'anonymous' : nil)
+ end
+ end
+
+ def startup_css_enabled?
+ !params.has_key?(:no_startup_css)
+ end
+
+ def use_new_fonts?
+ Feature.enabled?(:new_fonts, current_user) || request.params.has_key?(:new_fonts)
end
def outdated_browser?
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 7f13f609353..2b2ac262848 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -216,6 +216,7 @@ module ApplicationSettingsHelper
:default_branch_protection,
:default_ci_config_path,
:default_group_visibility,
+ :default_preferred_language,
:default_project_creation,
:default_project_visibility,
:default_projects_limit,
@@ -287,6 +288,7 @@ module ApplicationSettingsHelper
:max_import_size,
:max_pages_size,
:max_pages_custom_domains_per_project,
+ :max_terraform_state_size_bytes,
:max_yaml_size_bytes,
:max_yaml_depth,
:metrics_method_call_threshold,
@@ -318,7 +320,6 @@ module ApplicationSettingsHelper
:require_two_factor_authentication,
:restricted_visibility_levels,
:rsa_key_restriction,
- :send_user_confirmation_email,
:session_expire_delay,
:shared_runners_enabled,
:shared_runners_text,
@@ -445,7 +446,8 @@ module ApplicationSettingsHelper
:project_runner_token_expiration_interval,
:pipeline_limit_per_project_user_sha,
:invitation_flow_enforcement,
- :can_create_group
+ :can_create_group,
+ :bulk_import_enabled
].tap do |settings|
next if Gitlab.com?
@@ -545,7 +547,6 @@ module ApplicationSettingsHelper
settings_path: general_admin_application_settings_path(anchor: 'js-signup-settings'),
signup_enabled: @application_setting[:signup_enabled].to_s,
require_admin_approval_after_user_signup: @application_setting[:require_admin_approval_after_user_signup].to_s,
- send_user_confirmation_email: @application_setting[:send_user_confirmation_email].to_s,
email_confirmation_setting: @application_setting[:email_confirmation_setting].to_s,
minimum_password_length: @application_setting[:minimum_password_length],
minimum_password_length_min: ApplicationSetting::DEFAULT_MINIMUM_PASSWORD_LENGTH,
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 07152133402..c41b5923d13 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -70,11 +70,9 @@ module AuthHelper
end
def form_based_provider_with_highest_priority
- @form_based_provider_with_highest_priority ||= begin
- form_based_provider_priority.each do |provider_regexp|
- highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
- break highest_priority unless highest_priority.nil?
- end
+ @form_based_provider_with_highest_priority ||= form_based_provider_priority.each do |provider_regexp|
+ highest_priority = form_based_providers.find { |provider| provider.match?(provider_regexp) }
+ break highest_priority unless highest_priority.nil?
end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 798bb7b64a4..0fac2cb5fc5 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -91,7 +91,7 @@ module AvatarsHelper
title: user_name
}
- tag(:img, image_options)
+ tag.img(**image_options)
end
def user_avatar(options = {})
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index f08c1a2ff0a..281d5c923d0 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -113,7 +113,7 @@ module BlobHelper
end
def parent_dir_raw_path
- blob_raw_path.rpartition("/").first + "/"
+ "#{blob_raw_path.rpartition('/').first}/"
end
# SVGs can contain malicious JavaScript; only include whitelisted
@@ -295,7 +295,7 @@ module BlobHelper
end
def edit_link_tag(link_text, edit_path, common_classes)
- link_to link_text, edit_path, class: "#{common_classes}"
+ link_to link_text, edit_path, class: common_classes
end
def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 7b8290ac9ef..424e5920fed 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -39,7 +39,7 @@ module Ci
def job_statuses
statuses = Ci::HasStatus::AVAILABLE_STATUSES
- statuses.to_h { |status| [status, status.upcase] }
+ statuses.index_with(&:upcase)
end
end
end
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index 0de84c0d61f..8df30ee1f0d 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -96,8 +96,8 @@ module Ci
def toggle_shared_runners_settings_data(project)
{
- is_enabled: "#{project.shared_runners_enabled?}",
- is_disabled_and_unoverridable: "#{project.group&.shared_runners_setting == Namespace::SR_DISABLED_AND_UNOVERRIDABLE}",
+ is_enabled: project.shared_runners_enabled?.to_s,
+ is_disabled_and_unoverridable: (project.group&.shared_runners_setting == Namespace::SR_DISABLED_AND_UNOVERRIDABLE).to_s,
update_path: toggle_shared_runners_project_runners_path(project)
}
end
diff --git a/app/helpers/ci/secure_files_helper.rb b/app/helpers/ci/secure_files_helper.rb
index 30b2e12ac3b..fca89ddab1e 100644
--- a/app/helpers/ci/secure_files_helper.rb
+++ b/app/helpers/ci/secure_files_helper.rb
@@ -4,7 +4,7 @@ module Ci
def show_secure_files_setting(project, user)
return false if user.nil?
- Feature.enabled?(:ci_secure_files, project) && user.can?(:read_secure_files, project)
+ user.can?(:read_secure_files, project)
end
end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 4493bc2bc6d..53781364af7 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -124,7 +124,7 @@ module CommitsHelper
new_project_tag_path: new_project_tag_path(project, ref: commit),
email_patches_path: project_commit_path(project, commit, format: :patch),
plain_diff_path: project_commit_path(project, commit, format: :diff),
- can_revert: "#{can_collaborate && !commit.has_been_reverted?(current_user)}",
+ can_revert: (can_collaborate && !commit.has_been_reverted?(current_user)).to_s,
can_cherry_pick: can_collaborate.to_s,
can_tag: can?(current_user, :push_code, project).to_s,
can_email_patches: (commit.parents.length < 2).to_s
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index e05adc5cd0e..e0a1697cfa9 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -76,13 +76,11 @@ module DiffHelper
def diff_line_content(line)
if line.blank?
"&nbsp;".html_safe
- else
+ elsif line.start_with?('+', '-', ' ')
# `sub` and substring-ing would destroy HTML-safeness of `line`
- if line.start_with?('+', '-', ' ')
- line[1, line.length]
- else
- line
- end
+ line[1, line.length]
+ else
+ line
end
end
@@ -227,7 +225,7 @@ module DiffHelper
end
def conflicts(allow_tree_conflicts: false)
- return unless merge_request.cannot_be_merged?
+ return unless merge_request.cannot_be_merged? && merge_request.source_branch_exists? && merge_request.target_branch_exists?
conflicts_service = MergeRequests::Conflicts::ListService.new(merge_request, allow_tree_conflicts: allow_tree_conflicts) # rubocop:disable CodeReuse/ServiceClass
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 62e66b9a3ea..427cbe18fbf 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -17,8 +17,8 @@ module DropdownsHelper
end
content_tag_options = { class: "dropdown-menu dropdown-select #{options[:dropdown_class] if options.key?(:dropdown_class)}" }
- content_tag_options[:data] = options[:dropdown_qa_selector] ? { qa_selector: "#{options[:dropdown_qa_selector]}" } : {}
- content_tag_options[:data][:testid] = "#{options[:dropdown_testid]}" if options[:dropdown_testid]
+ content_tag_options[:data] = options[:dropdown_qa_selector] ? { qa_selector: (options[:dropdown_qa_selector]).to_s } : {}
+ content_tag_options[:data][:testid] = (options[:dropdown_testid]).to_s if options[:dropdown_testid]
dropdown_output << content_tag(:div, content_tag_options) do
output = []
@@ -86,7 +86,7 @@ module DropdownsHelper
title_output = []
if has_back
- title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back " + margin_class, aria: { label: "Go back" }, type: "button") do
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-back #{margin_class}", aria: { label: "Go back" }, type: "button") do
sprite_icon('arrow-left')
end
end
@@ -94,7 +94,7 @@ module DropdownsHelper
title_output << content_tag(:span, title, class: margin_class)
if has_close
- title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close " + margin_class, aria: { label: "Close" }, type: "button") do
+ title_output << content_tag(:button, class: "dropdown-title-button dropdown-menu-close #{margin_class}", aria: { label: "Close" }, type: "button") do
sprite_icon('close', size: 16, css_class: 'dropdown-menu-close-icon')
end
end
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 54733fa9101..cad39854c0e 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -139,7 +139,7 @@ module EmailsHelper
max_domain_length = list_id_max_length - Gitlab.config.gitlab.host.length - project.id.to_s.length - 2
if max_domain_length < 3
- return project.id.to_s + "..." + Gitlab.config.gitlab.host
+ return "#{project.id}...#{Gitlab.config.gitlab.host}"
end
if project_path_as_domain.length > max_domain_length
@@ -151,7 +151,7 @@ module EmailsHelper
project_path_as_domain = project_path_as_domain.slice(0, last_dot_index).concat("..")
end
- project.id.to_s + "." + project_path_as_domain + "." + Gitlab.config.gitlab.host
+ "#{project.id}.#{project_path_as_domain}.#{Gitlab.config.gitlab.host}"
end
def html_header_message
diff --git a/app/helpers/environment_helper.rb b/app/helpers/environment_helper.rb
index b6997b6fb70..0e64a98c9da 100644
--- a/app/helpers/environment_helper.rb
+++ b/app/helpers/environment_helper.rb
@@ -72,6 +72,7 @@ module EnvironmentHelper
{
name: environment.name,
id: environment.id,
+ project_full_path: project.full_path,
external_url: environment.external_url,
can_update_environment: can?(current_user, :update_environment, environment),
can_destroy_environment: can_destroy_environment?(environment),
diff --git a/app/helpers/environments_helper.rb b/app/helpers/environments_helper.rb
index 333237db6a4..5bf4fa2ffcc 100644
--- a/app/helpers/environments_helper.rb
+++ b/app/helpers/environments_helper.rb
@@ -67,7 +67,7 @@ module EnvironmentsHelper
'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)}",
+ 'custom_metrics_available' => custom_metrics_available?(project).to_s,
'dashboard_timezone' => project.metrics_setting_dashboard_timezone.to_s.upcase
}
end
@@ -78,8 +78,8 @@ module EnvironmentsHelper
{
'metrics_dashboard_base_path' => metrics_dashboard_base_path(environment, project),
'current_environment_name' => environment.name,
- 'has_metrics' => "#{environment.has_metrics?}",
- 'environment_state' => "#{environment.state}"
+ 'has_metrics' => environment.has_metrics?.to_s,
+ 'environment_state' => environment.state.to_s
}
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 087e4838ed9..bef2da495b0 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -92,7 +92,7 @@ module EventsHelper
content_tag :li, class: active do
link_to request.path, link_opts do
- content_tag(:span, ' ' + text)
+ content_tag(:span, " #{text}")
end
end
end
diff --git a/app/helpers/groups/observability_helper.rb b/app/helpers/groups/observability_helper.rb
index 6fb6acce386..26caac4ce7f 100644
--- a/app/helpers/groups/observability_helper.rb
+++ b/app/helpers/groups/observability_helper.rb
@@ -5,15 +5,15 @@ module Groups
ACTION_TO_PATH = {
'dashboards' => {
path: '/',
- title: -> { s_('Dashboards') }
+ title: -> { _('Dashboards') }
},
'manage' => {
path: '/dashboards',
- title: -> { s_('Manage Dashboards') }
+ title: -> { _('Manage Dashboards') }
},
'explore' => {
path: '/explore',
- title: -> { s_('Explore') }
+ title: -> { _('Explore') }
}
}.freeze
@@ -22,7 +22,7 @@ module Groups
# When running Observability UI in standalone mode (i.e. not backed by Observability Backend)
# the group-id is not required. This is mostly used for local dev
- base_url = ENV['STANDALONE_OBSERVABILITY_UI'] == 'true' ? observability_url : "#{observability_url}/#{group.id}"
+ base_url = ENV['STANDALONE_OBSERVABILITY_UI'] == 'true' ? observability_url : "#{observability_url}/-/#{group.id}"
sanitized_path = if params[:observability_path] && sanitize(params[:observability_path]) != ''
CGI.unescapeHTML(sanitize(params[:observability_path]))
diff --git a/app/helpers/groups/settings_helper.rb b/app/helpers/groups/settings_helper.rb
index 1b391680996..38300043dd7 100644
--- a/app/helpers/groups/settings_helper.rb
+++ b/app/helpers/groups/settings_helper.rb
@@ -9,7 +9,7 @@ module Groups
remove_form_id: remove_form_id,
button_text: _('Remove group'),
button_testid: 'remove-group-button',
- disabled: group.paid?.to_s,
+ disabled: group.prevent_delete?.to_s,
confirm_danger_message: remove_group_message(group),
phrase: group.full_path
}
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 921e30edbaa..63544e28a0e 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -10,7 +10,7 @@ module HooksHelper
def link_to_test_hook(hook, trigger)
path = test_hook_path(hook, trigger)
- trigger_human_name = trigger.to_s.tr('_', ' ').camelize
+ trigger_human_name = integration_webhook_event_human_name(trigger)
link_to path, rel: 'nofollow', method: :post do
content_tag(:span, trigger_human_name)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index c81041c2d9c..021b47ceab2 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -38,7 +38,7 @@ module IconsHelper
css_classes = []
css_classes << "s#{size}" if size
- css_classes << "#{css_class}" unless css_class.blank?
+ css_classes << css_class.to_s unless css_class.blank?
content_tag(
:svg,
diff --git a/app/helpers/ide_helper.rb b/app/helpers/ide_helper.rb
index 34f4749c42a..0e81cea8ac7 100644
--- a/app/helpers/ide_helper.rb
+++ b/app/helpers/ide_helper.rb
@@ -7,7 +7,10 @@ module IdeHelper
'use-new-web-ide' => use_new_web_ide?.to_s,
'new-web-ide-help-page-path' => help_page_path('user/project/web_ide/index.md', anchor: 'vscode-reimplementation'),
'user-preferences-path' => profile_preferences_path,
- 'branch-name' => @branch
+ 'branch-name' => @branch,
+ 'file-path' => @path,
+ 'fork-info' => @fork_info&.to_json,
+ 'merge-request' => @merge_request
}.merge(use_new_web_ide? ? new_ide_data : legacy_ide_data)
end
@@ -24,7 +27,9 @@ module IdeHelper
def new_ide_data
{
'project-path' => @project&.path_with_namespace,
- 'csp-nonce' => content_security_policy_nonce
+ 'csp-nonce' => content_security_policy_nonce,
+ # We will replace these placeholders in the FE
+ 'ide-remote-path' => ide_remote_path(remote_host: ':remote_host', remote_path: ':remote_path')
}
end
@@ -42,9 +47,6 @@ module IdeHelper
'render-whitespace-in-code': current_user.render_whitespace_in_code.to_s,
'codesandbox-bundler-url': Gitlab::CurrentSettings.web_ide_clientside_preview_bundler_url,
'default-branch' => @project && @project.default_branch,
- 'file-path' => @path,
- 'merge-request' => @merge_request,
- 'fork-info' => @fork_info&.to_json,
'project' => convert_to_project_entity_json(@project),
'enable-environments-guidance' => enable_environments_guidance?.to_s,
'preview-markdown-path' => @project && preview_markdown_path(@project),
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index abfa55cff24..0650af33e37 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -185,6 +185,29 @@ module IntegrationsHelper
target_type_i18n_map[target_type] || target_type
end
+ def integration_webhook_event_human_name(event)
+ event_i18n_map = {
+ repository_update_events: _('Repository update events'),
+ push_events: _('Push events'),
+ tag_push_events: s_('Webhooks|Tag push events'),
+ note_events: _('Comments'),
+ confidential_note_events: s_('Webhooks|Confidential comments'),
+ issues_events: s_('Webhooks|Issues events'),
+ confidential_issues_events: s_('Webhooks|Confidential issues events'),
+ subgroup_events: s_('Webhooks|Subgroup events'),
+ member_events: s_('Webhooks|Member events'),
+ merge_requests_events: s_('Webhooks|Merge request events'),
+ job_events: s_('Webhooks|Job events'),
+ pipeline_events: s_('Webhooks|Pipeline events'),
+ wiki_page_events: s_('Webhooks|Wiki page events'),
+ deployment_events: s_('Webhooks|Deployment events'),
+ feature_flag_events: s_('Webhooks|Feature flag events'),
+ releases_events: s_('Webhooks|Releases events')
+ }
+
+ event_i18n_map[event] || event.to_s.humanize
+ end
+
extend self
private
diff --git a/app/helpers/invite_members_helper.rb b/app/helpers/invite_members_helper.rb
index 5d537767eaf..6fad1346426 100644
--- a/app/helpers/invite_members_helper.rb
+++ b/app/helpers/invite_members_helper.rb
@@ -20,6 +20,7 @@ module InviteMembersHelper
end
end
+ # Overridden in EE
def common_invite_group_modal_data(source, member_class, is_project)
{
id: source.id,
@@ -29,7 +30,8 @@ module InviteMembersHelper
invalid_groups: source.related_group_ids,
help_link: help_page_url('user/permissions'),
is_project: is_project,
- access_levels: member_class.permissible_access_level_roles(current_user, source).to_json
+ access_levels: member_class.permissible_access_level_roles(current_user, source).to_json,
+ full_path: source.full_path
}.merge(group_select_data(source))
end
@@ -39,7 +41,8 @@ module InviteMembersHelper
id: source.id,
root_id: source.root_ancestor&.id,
name: source.name,
- default_access_level: Gitlab::Access::GUEST
+ default_access_level: Gitlab::Access::GUEST,
+ full_path: source.full_path
}
if show_invite_members_for_task?(source)
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index fd181109a94..2b21d8c51e6 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -138,15 +138,15 @@ module IssuablesHelper
def issuable_meta_author_status(author)
return "" unless author&.status&.customized? && status = user_status(author)
- "#{status}".html_safe
+ 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.work_item_type.base_type)
- output << content_tag(:span, sprite_icon("#{issuable.work_item_type.icon_name}", 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: issuable.issue_type.capitalize, created_at: time_ago_with_tooltip(issuable.created_at) }, class: 'gl-mr-2')
+ 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
@@ -207,7 +207,14 @@ module IssuablesHelper
def assigned_issuables_count(issuable_type)
case issuable_type
when :issues
- current_user.assigned_open_issues_count
+ if Feature.enabled?(:limit_assigned_issues_count)
+ ::Users::AssignedIssuesCountService.new(
+ current_user: current_user,
+ max_limit: User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT
+ ).count
+ else
+ current_user.assigned_open_issues_count
+ end
when :merge_requests
current_user.assigned_open_merge_requests_count
else
@@ -215,6 +222,16 @@ module IssuablesHelper
end
end
+ def assigned_open_issues_count_text
+ count = assigned_issuables_count(:issues)
+
+ if Feature.enabled?(:limit_assigned_issues_count) && count > User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT - 1
+ "#{count - 1}+"
+ else
+ count.to_s
+ end
+ end
+
def issuable_reference(issuable)
@show_full_reference ? issuable.to_reference(full: true) : issuable.to_reference(@group || @project)
end
@@ -348,12 +365,10 @@ module IssuablesHelper
else
[_("Closed"), "merge-request-close"]
end
+ elsif issuable.open?
+ [_("Open"), "issues"]
else
- if issuable.open?
- [_("Open"), "issues"]
- else
- [_("Closed"), "issue-closed"]
- end
+ [_("Closed"), "issue-closed"]
end
end
@@ -414,6 +429,7 @@ module IssuablesHelper
id: issuable[:id],
severity: issuable[:severity],
timeTrackingLimitToHours: Gitlab::CurrentSettings.time_tracking_limit_to_hours,
+ canCreateTimelogs: issuable.dig(:current_user, :can_create_timelogs),
createNoteEmail: issuable[:create_note_email],
issuableType: issuable[:type]
}
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 932a50d9451..1d68dccc741 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -256,6 +256,18 @@ module IssuesHelper
)
end
+ def dashboard_issues_list_data(current_user)
+ {
+ calendar_path: url_for(safe_params.merge(calendar_url_options)),
+ empty_state_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'),
+ initial_sort: current_user&.user_preference&.issues_sort,
+ is_public_visibility_restricted:
+ Gitlab::CurrentSettings.restricted_visibility_levels&.include?(Gitlab::VisibilityLevel::PUBLIC).to_s,
+ is_signed_in: current_user.present?.to_s,
+ rss_path: url_for(safe_params.merge(rss_url_options))
+ }
+ end
+
def issues_form_data(project)
{
new_issue_path: new_project_issue_path(project)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 0123eb68c9a..8c069bc828b 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -159,7 +159,7 @@ module LabelsHelper
end
def label_subscription_toggle_button_text(label, project = nil)
- label.subscribed?(current_user, project) ? 'Unsubscribe' : 'Subscribe'
+ label.subscribed?(current_user, project) ? _('Unsubscribe') : _('Subscribe')
end
def create_label_title(subject)
@@ -219,8 +219,8 @@ module LabelsHelper
}.merge(opts)
end
- def issuable_types
- ['issues', 'merge requests']
+ def labels_function_introduction
+ _('Labels can be applied to issues and merge requests. Group labels are available for any project within the group.')
end
def show_labels_full_path?(project, group)
diff --git a/app/helpers/listbox_helper.rb b/app/helpers/listbox_helper.rb
index 16caf862c7b..0aaeb39c82d 100644
--- a/app/helpers/listbox_helper.rb
+++ b/app/helpers/listbox_helper.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
module ListboxHelper
- DROPDOWN_CONTAINER_CLASSES = %w[dropdown b-dropdown gl-new-dropdown btn-group js-redirect-listbox].freeze
+ DROPDOWN_CONTAINER_CLASSES = %w[dropdown b-dropdown gl-dropdown btn-group js-redirect-listbox].freeze
DROPDOWN_BUTTON_CLASSES = %w[btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle].freeze
- DROPDOWN_INNER_CLASS = 'gl-new-dropdown-button-text'
+ DROPDOWN_INNER_CLASS = 'gl-dropdown-button-text'
DROPDOWN_ICON_CLASS = 'gl-button-icon dropdown-chevron gl-icon'
# Creates a listbox component with redirect behavior.
diff --git a/app/helpers/markup_helper.rb b/app/helpers/markup_helper.rb
index 9baea43b77d..ed9129ff78b 100644
--- a/app/helpers/markup_helper.rb
+++ b/app/helpers/markup_helper.rb
@@ -59,12 +59,20 @@ module MarkupHelper
# as Markdown. HTML tags in the parsed output are not counted toward the
# +max_chars+ limit. If the length limit falls within a tag's contents, then
# the tag contents are truncated without removing the closing tag.
- def first_line_in_markdown(object, attribute, max_chars = nil, options = {})
+ def first_line_in_markdown(object, attribute, max_chars = nil, is_todo: false, **options)
md = markdown_field(object, attribute, options.merge(post_process: false))
return unless md.present?
+ includes_code = false
+
tags = %w(a gl-emoji b strong i em pre code p span)
- tags << 'img' if options[:allow_images]
+
+ if is_todo
+ fragment = Nokogiri::HTML.fragment(md)
+ includes_code = fragment.css('code').any?
+
+ md = fragment
+ end
context = markdown_field_render_context(object, attribute, options)
context.reverse_merge!(truncate_visible_max_chars: max_chars || md.length)
@@ -77,12 +85,19 @@ module MarkupHelper
%w(
style data-src data-name data-unicode-version data-html
data-reference-type data-project-path data-iid data-mr-title
+ data-user
)
)
+ # Extra span with relative positioning relative due to system font being behind
+ # background color when username is first word of mention
+ if is_todo && !includes_code
+ text = "<span class=\"gl-relative\">\"</span>#{text}<span class=\"gl-relative\">\"</span>"
+ end
+
# since <img> tags are stripped, this can leave empty <a> tags hanging around
# (as our markdown wraps images in links)
- options[:allow_images] ? text : strip_empty_link_tags(text).html_safe
+ strip_empty_link_tags(text).html_safe
end
def markdown(text, context = {})
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index f1f5f941edd..29f94adcc78 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -14,19 +14,17 @@ module MembersHelper
else
"deny #{member.user.name}'s request to join"
end
+ elsif member.user
+ "remove #{member.user.name} from"
else
- if member.user
- "remove #{member.user.name} from"
- else
- e = RuntimeError.new("Data integrity error: no associated user for member ID #{member.id}")
- Gitlab::ErrorTracking.track_exception(e,
- member_id: member.id,
- invite_email: member.invite_email,
- invite_accepted_at: member.invite_accepted_at,
- source_id: member.source_id,
- source_type: member.source_type)
- "remove this orphaned member from"
- end
+ e = RuntimeError.new("Data integrity error: no associated user for member ID #{member.id}")
+ Gitlab::ErrorTracking.track_exception(e,
+ member_id: member.id,
+ invite_email: member.invite_email,
+ invite_accepted_at: member.invite_accepted_at,
+ source_id: member.source_id,
+ source_type: member.source_type)
+ "remove this orphaned member from"
end
"#{text} #{action} the #{member.source.human_name} #{source_text(member)}?"
diff --git a/app/helpers/nav/top_nav_helper.rb b/app/helpers/nav/top_nav_helper.rb
index 751900f4593..bd4d661ab49 100644
--- a/app/helpers/nav/top_nav_helper.rb
+++ b/app/helpers/nav/top_nav_helper.rb
@@ -101,8 +101,7 @@ module Nav
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'project' || active_nav_link?(path: %w[root#index projects#trending projects#starred dashboard/projects#index]),
- css_class: 'qa-projects-dropdown',
- data: { track_label: "projects_dropdown", track_action: "click_dropdown" },
+ data: { track_label: "projects_dropdown", track_action: "click_dropdown", qa_selector: "projects_dropdown" },
view: PROJECTS_VIEW,
shortcut_href: dashboard_projects_path,
**projects_menu_item_attrs
@@ -116,8 +115,7 @@ module Nav
builder.add_primary_menu_item_with_shortcut(
header: top_nav_localized_headers[:switch_to],
active: nav == 'group' || active_nav_link?(path: %w[dashboard/groups explore/groups]),
- css_class: 'qa-groups-dropdown',
- data: { track_label: "groups_dropdown", track_action: "click_dropdown" },
+ data: { track_label: "groups_dropdown", track_action: "click_dropdown", qa_selector: "groups_dropdown" },
view: GROUPS_VIEW,
shortcut_href: dashboard_groups_path,
**groups_menu_item_attrs
@@ -133,7 +131,7 @@ module Nav
href: dashboard_milestones_path,
active: active_nav_link?(controller: 'dashboard/milestones'),
icon: 'clock',
- data: { qa_selector: 'milestones_link', **menu_data_tracking_attrs('milestones') },
+ data: { **menu_data_tracking_attrs('milestones') },
shortcut_class: 'dashboard-shortcuts-milestones'
)
end
@@ -156,7 +154,7 @@ module Nav
href: activity_dashboard_path,
active: active_nav_link?(path: 'dashboard#activity'),
icon: 'history',
- data: { qa_selector: 'activity_link', **menu_data_tracking_attrs('activity') },
+ data: { **menu_data_tracking_attrs('activity') },
shortcut_class: 'dashboard-shortcuts-activity'
)
end
@@ -173,9 +171,8 @@ module Nav
title: title,
active: active_nav_link?(controller: 'admin/dashboard'),
icon: 'admin',
- css_class: 'qa-admin-area-link',
href: admin_root_path,
- data: { qa_selector: 'menu_item_link', qa_title: title, **menu_data_tracking_attrs(title) }
+ data: { qa_selector: 'admin_area_link', **menu_data_tracking_attrs(title) }
)
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 0cf2c5cea4c..bf3b132e33a 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -21,11 +21,11 @@ module NavHelper
def page_gutter_class
moved_sidebar_enabled = current_controller?('merge_requests') && moved_mr_sidebar_enabled?
- if page_has_markdown? && !current_controller?('conflicts')
+ if (page_has_markdown? || current_path?('projects/merge_requests#diffs')) && !current_controller?('conflicts')
if cookies[:collapsed_gutter] == 'true'
- ["page-gutter", "#{'right-sidebar-collapsed' unless moved_sidebar_enabled}"]
+ ["page-gutter", ('right-sidebar-collapsed' unless moved_sidebar_enabled).to_s]
else
- ["page-gutter", "#{'right-sidebar-expanded' unless moved_sidebar_enabled}"]
+ ["page-gutter", ('right-sidebar-expanded' unless moved_sidebar_enabled).to_s]
end
elsif current_path?('jobs#show')
%w[page-gutter build-sidebar right-sidebar-expanded]
diff --git a/app/helpers/numbers_helper.rb b/app/helpers/numbers_helper.rb
index 38d3f90dd55..7184a9c075c 100644
--- a/app/helpers/numbers_helper.rb
+++ b/app/helpers/numbers_helper.rb
@@ -6,7 +6,7 @@ module NumbersHelper
count = resource.page.total_count_with_limit(:all, limit: limit)
if count > limit
- number_with_delimiter(count - 1, options) + '+'
+ "#{number_with_delimiter(count - 1, options)}+"
else
number_with_delimiter(count, options)
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index c0665463706..6f7b2877100 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -80,8 +80,8 @@ module PageLayoutHelper
tags = []
page_card_attributes.each_with_index do |pair, i|
- tags << tag(:meta, property: "twitter:label#{i + 1}", content: pair[0])
- tags << tag(:meta, property: "twitter:data#{i + 1}", content: pair[1])
+ tags << tag.meta(property: "twitter:label#{i + 1}", content: pair[0])
+ tags << tag.meta(property: "twitter:data#{i + 1}", content: pair[1])
end
tags.join.html_safe
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 57afe0ed0be..f2b7c0064e4 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -100,11 +100,19 @@ module PreferencesHelper
def language_choices
options_for_select(
- selectable_locales_with_translation_level.sort,
+ selectable_locales_with_translation_level(Gitlab::I18n::MINIMUM_TRANSLATION_LEVEL).sort,
current_user.preferred_language
)
end
+ def default_preferred_language_choices
+ options_for_select(
+ selectable_locales_with_translation_level(
+ PreferredLanguageSwitcherHelper::SWITCHER_MINIMUM_TRANSLATION_LEVEL).sort,
+ Gitlab::CurrentSettings.default_preferred_language
+ )
+ end
+
def integration_views
[].tap do |views|
views << { name: 'gitpod', message: gitpod_enable_description, message_url: gitpod_url_placeholder, help_link: help_page_path('integration/gitpod.md') } if Gitlab::CurrentSettings.gitpod_enabled
@@ -136,8 +144,8 @@ module PreferencesHelper
first_day_of_week_choices.rassoc(Gitlab::CurrentSettings.first_day_of_week).first
end
- def selectable_locales_with_translation_level
- Gitlab::I18n.selectable_locales.map do |code, language|
+ def selectable_locales_with_translation_level(minimum_level)
+ Gitlab::I18n.selectable_locales(minimum_level).map do |code, language|
[
s_("i18n|%{language} (%{percent_translated}%% translated)") % {
language: language,
diff --git a/app/helpers/preferred_language_switcher_helper.rb b/app/helpers/preferred_language_switcher_helper.rb
new file mode 100644
index 00000000000..1cad4f842ec
--- /dev/null
+++ b/app/helpers/preferred_language_switcher_helper.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+module PreferredLanguageSwitcherHelper
+ SWITCHER_MINIMUM_TRANSLATION_LEVEL = 90
+
+ def ordered_selectable_locales
+ highly_translated_languages = Gitlab::I18n.selectable_locales(SWITCHER_MINIMUM_TRANSLATION_LEVEL)
+ # see https://docs.gitlab.com/ee/development/i18n/externalization.html#adding-a-new-language
+ # for translation standards
+ locale_list = highly_translated_languages.filter_map do |code, language|
+ percentage = Gitlab::I18n.percentage_translated_for(code)
+ {
+ value: code,
+ percentage: percentage,
+ text: language.split('-').last.strip
+ }
+ end
+
+ locale_list.sort_by { |item| item[:percentage] }.reverse
+ end
+end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index bfe39bbc211..979b979fba7 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -46,6 +46,14 @@ module ProfilesHelper
end
end
+ def ssh_key_usage_types
+ {
+ s_('SSHKey|Authentication & Signing') => 'auth_and_signing',
+ s_('SSHKey|Authentication') => 'auth',
+ s_('SSHKey|Signing') => 'signing'
+ }
+ end
+
# Overridden in EE::ProfilesHelper#ssh_key_expiration_tooltip
def ssh_key_expiration_tooltip(key)
return key.errors.full_messages.join(', ') if key.errors.full_messages.any?
diff --git a/app/helpers/programming_languages_helper.rb b/app/helpers/programming_languages_helper.rb
new file mode 100644
index 00000000000..c50872aec6f
--- /dev/null
+++ b/app/helpers/programming_languages_helper.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module ProgrammingLanguagesHelper
+ def search_language_placeholder
+ placeholder = _('Language')
+
+ return placeholder unless params[:language].present?
+
+ programming_languages.find { |language| language.id.to_s == params[:language] }&.name ||
+ placeholder
+ end
+
+ def programming_languages
+ @programming_languages ||= ProgrammingLanguage.most_popular
+ end
+
+ def language_state_class(language)
+ params[:language] == language.id.to_s ? 'is-active' : ''
+ end
+end
diff --git a/app/helpers/projects/ml/experiments_helper.rb b/app/helpers/projects/ml/experiments_helper.rb
index 29bd879859e..a67484e3d2f 100644
--- a/app/helpers/projects/ml/experiments_helper.rb
+++ b/app/helpers/projects/ml/experiments_helper.rb
@@ -9,7 +9,9 @@ module Projects
items = candidates.map do |candidate|
{
**candidate.params.to_h { |p| [p.name, p.value] },
- **candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] }
+ **candidate.latest_metrics.to_h { |m| [m.name, number_with_precision(m.value, precision: 4)] },
+ artifact: link_to_artifact(candidate),
+ details: link_to_details(candidate)
}
end
@@ -19,6 +21,42 @@ module Projects
def unique_logged_names(candidates, &selector)
Gitlab::Json.generate(candidates.flat_map(&selector).map(&:name).uniq)
end
+
+ def candidate_as_data(candidate)
+ data = {
+ params: candidate.params,
+ metrics: candidate.latest_metrics,
+ info: {
+ iid: candidate.iid,
+ path_to_artifact: link_to_artifact(candidate),
+ experiment_name: candidate.experiment.name,
+ path_to_experiment: link_to_experiment(candidate),
+ status: candidate.status
+ }
+ }
+
+ Gitlab::Json.generate(data)
+ end
+
+ private
+
+ def link_to_artifact(candidate)
+ artifact = candidate.artifact
+
+ return unless artifact.present?
+
+ project_package_path(candidate.experiment.project, artifact)
+ end
+
+ def link_to_details(candidate)
+ project_ml_candidate_path(candidate.experiment.project, candidate.iid)
+ end
+
+ def link_to_experiment(candidate)
+ experiment = candidate.experiment
+
+ project_ml_experiment_path(experiment.project, experiment.iid)
+ end
end
end
end
diff --git a/app/helpers/projects/pipeline_helper.rb b/app/helpers/projects/pipeline_helper.rb
index edbdb9d4adf..5c62920cd89 100644
--- a/app/helpers/projects/pipeline_helper.rb
+++ b/app/helpers/projects/pipeline_helper.rb
@@ -12,6 +12,7 @@ module Projects
graphql_resource_etag: graphql_etag_pipeline_path(pipeline),
metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: project.namespace, project_id: project, format: :json),
pipeline_iid: pipeline.iid,
+ pipeline_path: pipeline_path(pipeline),
pipeline_project_path: project.full_path,
total_job_count: pipeline.total_size,
summary_endpoint: summary_project_pipeline_tests_path(project, pipeline, format: :json),
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index e41a3fa5091..682febe9dc9 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -142,6 +142,8 @@ module ProjectsHelper
end
def project_search_tabs?(tab)
+ return false unless @project.present?
+
abilities = Array(search_tab_ability_map[tab])
abilities.any? { |ability| can?(current_user, ability, @project) }
@@ -254,11 +256,8 @@ module ProjectsHelper
end
end
- # TODO: Remove this method when removing the feature flag
- # https://gitlab.com/gitlab-org/gitlab/merge_requests/11209#note_162234863
- # make sure to remove from the EE specific controller as well: ee/app/controllers/ee/dashboard/projects_controller.rb
def show_projects?(projects, params)
- Feature.enabled?(:project_list_filter_bar) || !!(params[:personal] || params[:name] || any_projects?(projects))
+ !!(params[:personal] || params[:name] || params[:language] || any_projects?(projects))
end
def push_to_create_project_command(user = current_user)
@@ -465,9 +464,9 @@ module ProjectsHelper
def project_coverage_chart_data_attributes(daily_coverage_options, ref)
{
graph_endpoint: "#{daily_coverage_options[:graph_api_path]}?#{daily_coverage_options[:base_params].to_query}",
- graph_start_date: "#{daily_coverage_options[:base_params][:start_date].strftime('%b %d')}",
- graph_end_date: "#{daily_coverage_options[:base_params][:end_date].strftime('%b %d')}",
- graph_ref: "#{ref}",
+ graph_start_date: daily_coverage_options[:base_params][:start_date].strftime('%b %d'),
+ graph_end_date: daily_coverage_options[:base_params][:end_date].strftime('%b %d'),
+ graph_ref: ref.to_s,
graph_csv_path: "#{daily_coverage_options[:download_path]}?#{daily_coverage_options[:base_params].to_query}"
}
end
@@ -480,6 +479,32 @@ module ProjectsHelper
format_cached_count(1000, number)
end
+ def fork_divergence_message(counts)
+ messages = []
+
+ if counts[:behind].nil? || counts[:ahead].nil?
+ return s_('ForksDivergence|Fork has diverged from upstream repository')
+ end
+
+ if counts[:behind] > 0
+ messages << s_("ForksDivergence|%{behind} %{commit_word} behind") % {
+ behind: counts[:behind], commit_word: n_('commit', 'commits', counts[:behind])
+ }
+ end
+
+ if counts[:ahead] > 0
+ messages << s_("ForksDivergence|%{ahead} %{commit_word} ahead of") % {
+ ahead: counts[:ahead], commit_word: n_('commit', 'commits', counts[:ahead])
+ }
+ end
+
+ if messages.blank?
+ s_('ForksDivergence|Up to date with upstream repository')
+ else
+ s_("ForksDivergence|%{messages} upstream repository") % { messages: messages.join(', ') }
+ end
+ end
+
private
def localized_access_names
@@ -531,10 +556,10 @@ module ProjectsHelper
def search_tab_ability_map
@search_tab_ability_map ||= tab_ability_map.merge(
- blobs: :download_code,
- commits: :download_code,
+ blobs: :read_code,
+ commits: :read_code,
merge_requests: :read_merge_request,
- notes: [:read_merge_request, :download_code, :read_issue, :read_snippet],
+ notes: [:read_merge_request, :read_code, :read_issue, :read_snippet],
members: :read_project_member
)
end
@@ -658,7 +683,6 @@ module ProjectsHelper
lfsEnabled: !!project.lfs_enabled,
emailsDisabled: project.emails_disabled?,
metricsDashboardAccessLevel: feature.metrics_dashboard_access_level,
- operationsAccessLevel: feature.operations_access_level,
monitorAccessLevel: feature.monitor_access_level,
showDefaultAwardEmojis: project.show_default_award_emojis?,
warnAboutPotentiallyUnwantedCharacters: project.warn_about_potentially_unwanted_characters?,
@@ -680,7 +704,7 @@ module ProjectsHelper
def find_file_path
return unless @project && !@project.empty_repo?
- return unless can?(current_user, :download_code, @project)
+ return unless can?(current_user, :read_code, @project)
ref = @ref || @project.repository.root_ref
diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb
index dce0517690d..63e2b377fef 100644
--- a/app/helpers/routing/pseudonymization_helper.rb
+++ b/app/helpers/routing/pseudonymization_helper.rb
@@ -12,6 +12,7 @@ module Routing
tab
glm_source
glm_content
+ _gl
].freeze
def initialize(request_object, group, project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index b8ac2afa7d6..e03365ad5f1 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -40,6 +40,7 @@ module SearchHelper
[
groups_autocomplete(term),
projects_autocomplete(term),
+ users_autocomplete(term),
issue_autocomplete(term)
].flatten
end
@@ -308,8 +309,8 @@ module SearchHelper
{
category: "Groups",
id: group.id,
- value: "#{search_result_sanitize(group.name)}",
- label: "#{search_result_sanitize(group.full_name)}",
+ value: search_result_sanitize(group.name),
+ label: search_result_sanitize(group.full_name),
url: group_path(group),
avatar_url: group.avatar_url || ''
}
@@ -343,14 +344,32 @@ module SearchHelper
{
category: "Projects",
id: p.id,
- value: "#{search_result_sanitize(p.name)}",
- label: "#{search_result_sanitize(p.full_name)}",
+ value: search_result_sanitize(p.name),
+ label: search_result_sanitize(p.full_name),
url: project_path(p),
avatar_url: p.avatar_url || ''
}
end
end
+ def users_autocomplete(term, limit = 5)
+ return [] unless current_user && Ability.allowed?(current_user, :read_users_list)
+
+ SearchService
+ .new(current_user, { scope: 'users', per_page: limit, search: term })
+ .search_objects
+ .map do |user|
+ {
+ category: "Users",
+ id: user.id,
+ value: search_result_sanitize(user.name),
+ label: search_result_sanitize(user.username),
+ url: user_path(user),
+ avatar_url: user.avatar_url || ''
+ }
+ end
+ end
+
def recent_merge_requests_autocomplete(term)
return [] unless current_user
@@ -427,20 +446,38 @@ module SearchHelper
result
end
+ def code_tab_condition
+ return true if project_search_tabs?(:blobs)
+
+ @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_code_tab)
+ end
+
+ def wiki_tab_condition
+ return true if project_search_tabs?(:wiki)
+
+ @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_wiki_tab)
+ end
+
+ def commits_tab_condition
+ return true if project_search_tabs?(:commits)
+
+ @project.nil? && search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)
+ end
+
# search page scope navigation
def search_navigation
{
projects: { sort: 1, label: _("Projects"), data: { qa_selector: 'projects_tab' }, condition: @project.nil? },
- blobs: { sort: 2, label: _("Code"), data: { qa_selector: 'code_tab' }, condition: project_search_tabs?(:blobs) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_code_tab)) },
+ blobs: { sort: 2, label: _("Code"), data: { qa_selector: 'code_tab' }, condition: code_tab_condition },
# sort: 3 is reserved for EE items
issues: { sort: 4, label: _("Issues"), condition: project_search_tabs?(:issues) || feature_flag_tab_enabled?(:global_search_issues_tab) },
merge_requests: { sort: 5, label: _("Merge requests"), condition: project_search_tabs?(:merge_requests) || feature_flag_tab_enabled?(:global_search_merge_requests_tab) },
- wiki_blobs: { sort: 6, label: _("Wiki"), condition: project_search_tabs?(:wiki) || search_service.show_elasticsearch_tabs? },
- commits: { sort: 7, label: _("Commits"), condition: project_search_tabs?(:commits) || (search_service.show_elasticsearch_tabs? && feature_flag_tab_enabled?(:global_search_commits_tab)) },
+ wiki_blobs: { sort: 6, label: _("Wiki"), condition: wiki_tab_condition },
+ commits: { sort: 7, label: _("Commits"), condition: commits_tab_condition },
notes: { sort: 8, label: _("Comments"), condition: project_search_tabs?(:notes) || search_service.show_elasticsearch_tabs? },
milestones: { sort: 9, label: _("Milestones"), condition: project_search_tabs?(:milestones) || @project.nil? },
- users: { sort: 10, label: _("Users"), condition: show_user_search_tab? },
- snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: @show_snippets.present? && @project.nil? }
+ users: { sort: 10, label: _("Users"), condition: show_user_search_tab? },
+ snippet_titles: { sort: 11, label: _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }, condition: search_service.show_snippets? && @project.nil? }
}
end
@@ -545,12 +582,10 @@ module SearchHelper
else
:success
end
+ elsif issuable.closed?
+ :info
else
- if issuable.closed?
- :info
- else
- :success
- end
+ :success
end
end
@@ -566,7 +601,7 @@ module SearchHelper
end
def feature_flag_tab_enabled?(flag)
- @group || Feature.enabled?(flag, current_user, type: :ops)
+ @group.present? || Feature.enabled?(flag, current_user, type: :ops)
end
def sanitized_search_params
diff --git a/app/helpers/sidebars_helper.rb b/app/helpers/sidebars_helper.rb
index 9002fdda128..cbee02a28c0 100644
--- a/app/helpers/sidebars_helper.rb
+++ b/app/helpers/sidebars_helper.rb
@@ -20,9 +20,8 @@ module SidebarsHelper
end
end
- def project_sidebar_context(project, user, current_ref)
- context_data = project_sidebar_context_data(project, user, current_ref)
-
+ def project_sidebar_context(project, user, current_ref, ref_type: nil)
+ context_data = project_sidebar_context_data(project, user, current_ref, ref_type: ref_type)
Sidebars::Projects::Context.new(**context_data)
end
@@ -83,12 +82,13 @@ module SidebarsHelper
tracking_attrs('user_side_navigation', 'render', 'user_side_navigation')
end
- def project_sidebar_context_data(project, user, current_ref)
+ def project_sidebar_context_data(project, user, current_ref, ref_type: nil)
{
current_user: user,
container: project,
learn_gitlab_enabled: learn_gitlab_enabled?(project),
current_ref: current_ref,
+ ref_type: ref_type,
jira_issues_integration: project_jira_issues_integration?,
can_view_pipeline_editor: can_view_pipeline_editor?(project),
show_cluster_hint: show_gke_cluster_integration_callout?(project)
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index a711f36fe05..4a9596a1347 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -44,25 +44,18 @@ module SortingHelper
# rubocop: enable Metrics/AbcSize
def projects_sort_options_hash
- use_old_sorting = Feature.disabled?(:project_list_filter_bar) || current_controller?('admin/projects')
-
options = {
sort_value_latest_activity => sort_title_latest_activity,
sort_value_recently_created => sort_title_created_date,
sort_value_name => sort_title_name,
sort_value_name_desc => sort_title_name_desc,
- sort_value_stars_desc => sort_title_stars
+ sort_value_stars_desc => sort_title_stars,
+ sort_value_oldest_activity => sort_title_oldest_activity,
+ sort_value_oldest_created => sort_title_oldest_created,
+ sort_value_recently_created => sort_title_recently_created,
+ sort_value_stars_desc => sort_title_most_stars
}
- if use_old_sorting
- options = options.merge({
- sort_value_oldest_activity => sort_title_oldest_activity,
- sort_value_oldest_created => sort_title_oldest_created,
- sort_value_recently_created => sort_title_recently_created,
- sort_value_stars_desc => sort_title_most_stars
- })
- end
-
if current_controller?('admin/projects')
options[sort_value_largest_repo] = sort_title_largest_repo
end
@@ -79,29 +72,6 @@ module SortingHelper
}
end
- def projects_sort_option_titles
- # Only used for the project filter search bar
- projects_sort_options_hash.merge({
- sort_value_oldest_activity => sort_title_latest_activity,
- sort_value_oldest_created => sort_title_created_date,
- sort_value_name_desc => sort_title_name,
- sort_value_stars_asc => sort_title_stars
- })
- end
-
- def projects_reverse_sort_options_hash
- {
- sort_value_latest_activity => sort_value_oldest_activity,
- sort_value_recently_created => sort_value_oldest_created,
- sort_value_name => sort_value_name_desc,
- sort_value_stars_desc => sort_value_stars_asc,
- sort_value_oldest_activity => sort_value_latest_activity,
- sort_value_oldest_created => sort_value_recently_created,
- sort_value_name_desc => sort_value_name,
- sort_value_stars_asc => sort_value_stars_desc
- }
- end
-
def forks_reverse_sort_options_hash
{
sort_value_recently_created => sort_value_oldest_created,
@@ -188,13 +158,6 @@ module SortingHelper
}
end
- def runners_sort_options_hash
- {
- sort_value_created_date => sort_title_created_date,
- sort_value_contacted_date => sort_title_contacted_date
- }
- end
-
def starrers_sort_options_hash
{
sort_value_name => sort_title_name,
@@ -308,7 +271,7 @@ module SortingHelper
end
def sort_direction_button(reverse_url, reverse_sort, sort_value)
- link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort'
+ link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
icon = sort_direction_icon(sort_value)
url = reverse_url
@@ -329,13 +292,6 @@ module SortingHelper
sort_direction_button(url, reverse_sort, sort_value)
end
- def project_sort_direction_button(sort_value)
- reverse_sort = projects_reverse_sort_options_hash[sort_value]
- url = filter_projects_path(sort: reverse_sort)
-
- sort_direction_button(url, reverse_sort, sort_value)
- end
-
def packages_sort_options_hash
{
sort_value_recently_created => sort_title_created_date,
diff --git a/app/helpers/ssh_keys_helper.rb b/app/helpers/ssh_keys_helper.rb
index 17a9fd6146d..4cd40836335 100644
--- a/app/helpers/ssh_keys_helper.rb
+++ b/app/helpers/ssh_keys_helper.rb
@@ -2,10 +2,14 @@
module SshKeysHelper
def ssh_key_delete_modal_data(key, path)
+ title = _('Delete Key')
+
{
path: path,
method: 'delete',
qa_selector: 'delete_ssh_key_button',
+ title: title,
+ aria_label: title,
modal_attributes: {
'data-qa-selector': 'ssh_key_delete_modal',
title: _('Are you sure you want to delete this SSH key?'),
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 2942765a108..e3e2f423da3 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -75,7 +75,7 @@ module SubmoduleHelper
return true if url_no_dotgit == [Gitlab.config.gitlab.url, '/', namespace, '/',
project].join('')
- url_with_dotgit = url_no_dotgit + '.git'
+ url_with_dotgit = "#{url_no_dotgit}.git"
url_with_dotgit == Gitlab::RepositoryUrlBuilder.build([namespace, '/', project].join(''))
end
@@ -108,7 +108,7 @@ module SubmoduleHelper
def relative_self_links(relative_path, commit, old_commit, project)
relative_path = relative_path.rstrip
- absolute_project_path = "/" + project.full_path
+ absolute_project_path = "/#{project.full_path}"
# Resolve `relative_path` to target path
# Assuming `absolute_project_path` is `/g1/p1`:
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index e0e6229bc6d..307f03e0d64 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -36,7 +36,7 @@ module TimeboxesHelper
end
end
- def milestones_browse_issuables_path(milestone, state: nil, type:)
+ def milestones_browse_issuables_path(milestone, type:, state: nil)
opts = { milestone_title: milestone.title, state: state }
if @project
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index be63d28600f..d7c4540544b 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -16,17 +16,20 @@ module TodosHelper
def todo_action_name(todo)
case todo.action
when Todo::ASSIGNED then todo.self_added? ? _('assigned') : _('assigned you')
- when Todo::REVIEW_REQUESTED then s_('Todos|requested a review of')
+ when Todo::REVIEW_REQUESTED then s_('Todos|requested a review')
when Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED then format(
- s_("Todos|mentioned %{who} on"), who: todo_action_subject(todo)
+ s_("Todos|mentioned %{who}"), who: todo_action_subject(todo)
)
- when Todo::BUILD_FAILED then s_('Todos|The pipeline failed in')
- when Todo::MARKED then s_('Todos|added a todo for')
+ when Todo::BUILD_FAILED then s_('Todos|The pipeline failed')
+ when Todo::MARKED then s_('Todos|added a to-do item')
when Todo::APPROVAL_REQUIRED then format(
- s_("Todos|set %{who} as an approver for"), who: todo_action_subject(todo)
+ s_("Todos|set %{who} as an approver"), who: todo_action_subject(todo)
)
when Todo::UNMERGEABLE then s_('Todos|Could not merge')
- when Todo::MERGE_TRAIN_REMOVED then s_("Todos|Removed from Merge Train:")
+ when Todo::MERGE_TRAIN_REMOVED then s_("Todos|Removed from Merge Train")
+ when Todo::MEMBER_ACCESS_REQUESTED then format(
+ s_("Todos|has requested access to group %{which}"), which: _(todo.target.name)
+ )
end
end
@@ -37,45 +40,48 @@ module TodosHelper
end
end
- def todo_target_link(todo)
- text = raw(todo_target_type_name(todo) + ' ') +
- if todo.for_commit?
- content_tag(:span, todo.target_reference, class: 'commit-sha')
- else
- todo.target_reference
- end
+ def todo_target_name(todo)
+ return todo.target_reference unless todo.for_commit?
- link_to text, todo_target_path(todo)
+ content_tag(:span, todo.target_reference, class: 'commit-sha')
end
def todo_target_title(todo)
- # Design To Dos' filenames are displayed in `#todo_target_link` (see `Design#to_reference`),
+ # Design To Dos' filenames are displayed in `#todo_target_name` (see `Design#to_reference`),
# so to avoid displaying duplicate filenames in the To Do list for designs,
# we return an empty string here.
- return "" if todo.target.blank? || todo.for_design?
+ return "" if todo.target.blank? || todo.for_design? || todo.member_access_requested?
- "\"#{todo.target.title}\""
+ todo.target.title.to_s
end
def todo_parent_path(todo)
if todo.resource_parent.is_a?(Group)
- link_to todo.resource_parent.name, group_path(todo.resource_parent)
+ todo.resource_parent.name
else
- link_to_project(todo.project)
+ title = content_tag(:span, todo.project.name, class: 'project-name')
+ namespace = content_tag(:span, "#{todo.project.namespace.human_name} / ", class: 'namespace-name')
+
+ title.prepend(namespace) if todo.project.namespace
+
+ title
end
end
- def todo_target_type_name(todo)
- return _('design') if todo.for_design?
- return _('alert') if todo.for_alert?
-
- target_type = if todo.for_issue_or_work_item?
+ def todo_target_aria_label(todo)
+ target_type = if todo.for_design?
+ _('Design')
+ elsif todo.for_alert?
+ _('Alert')
+ elsif todo.member_access_requested?
+ _('Group')
+ elsif todo.for_issue_or_work_item?
IntegrationsHelper.integration_issue_type(todo.target.issue_type)
else
IntegrationsHelper.integration_todo_target_type(todo.target_type)
end
- target_type.titleize.downcase
+ "#{target_type} #{todo_target_name(todo)}"
end
def todo_target_path(todo)
@@ -92,6 +98,8 @@ module TodosHelper
elsif todo.for_issue_or_work_item?
path_options[:only_path] = true
Gitlab::UrlBuilder.build(todo.target, **path_options)
+ elsif todo.member_access_requested?
+ todo.access_request_url
else
path = [todo.resource_parent, todo.target]
@@ -123,18 +131,18 @@ module TodosHelper
when MergeRequest
case state
when 'closed'
- background_class = 'gl-bg-red-500'
+ variant = 'danger'
when 'merged'
- background_class = 'gl-bg-blue-500'
+ variant = 'info'
end
when Issue
- background_class = 'gl-bg-blue-500' if state == 'closed'
+ variant = 'info' if state == 'closed'
when AlertManagement::Alert
- background_class = 'gl-bg-blue-500' if state == 'resolved'
+ variant = 'info' if state == 'resolved'
end
- tag.span class: "gl-my-0 gl-px-2 status-box #{background_class}" do
- raw_state_to_i18n[state] || state.capitalize
+ content_tag(:span, class: 'todo-target-state') do
+ gl_badge_tag(raw_state_to_i18n[state] || state.capitalize, { variant: variant, size: 'sm' })
end
end
@@ -183,7 +191,8 @@ module TodosHelper
{ id: Todo::REVIEW_REQUESTED, text: s_('Todos|Review requested') },
{ id: Todo::MENTIONED, text: s_('Todos|Mentioned') },
{ id: Todo::MARKED, text: s_('Todos|Added') },
- { id: Todo::BUILD_FAILED, text: s_('Todos|Pipelines') }
+ { id: Todo::BUILD_FAILED, text: s_('Todos|Pipelines') },
+ { id: Todo::MEMBER_ACCESS_REQUESTED, text: s_('Todos|Member access requested') }
]
end
@@ -222,10 +231,15 @@ module TodosHelper
end
content = content_tag(:span, class: css_class) do
- "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
+ format(s_("Todos|Due %{due_date}"), due_date: if is_due_today
+ _("today")
+ else
+ l(todo.target.due_date,
+ format: Date::DATE_FORMATS[:medium])
+ end)
end
- "&middot; #{content}".html_safe
+ "#{content} &middot;".html_safe
end
def todo_author_display?(todo)
diff --git a/app/helpers/tooling/visual_review_helper.rb b/app/helpers/tooling/visual_review_helper.rb
index da6eb3ec434..cd3b8be5aac 100644
--- a/app/helpers/tooling/visual_review_helper.rb
+++ b/app/helpers/tooling/visual_review_helper.rb
@@ -14,10 +14,10 @@ module Tooling
GITLAB_ORG_GITLAB_PROJECT_PATH = 'gitlab-org/gitlab'
def visual_review_toolbar_options
- { 'data-merge-request-id': "#{ENV['REVIEW_APPS_MERGE_REQUEST_IID']}",
- 'data-mr-url': "#{GITLAB_INSTANCE_URL}",
- 'data-project-id': "#{GITLAB_ORG_GITLAB_PROJECT_ID}",
- 'data-project-path': "#{GITLAB_ORG_GITLAB_PROJECT_PATH}",
+ { 'data-merge-request-id': ENV['REVIEW_APPS_MERGE_REQUEST_IID'].to_s,
+ 'data-mr-url': GITLAB_INSTANCE_URL,
+ 'data-project-id': GITLAB_ORG_GITLAB_PROJECT_ID,
+ 'data-project-path': GITLAB_ORG_GITLAB_PROJECT_PATH,
'data-require-auth': false,
'id': 'review-app-toolbar-script',
'src': 'https://gitlab.com/assets/webpack/visual_review_toolbar.js' }
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index 48548ae9e6a..0bb92dfd118 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,6 +1,10 @@
# frozen_string_literal: true
module VersionCheckHelper
+ include Gitlab::Utils::StrongMemoize
+
+ SECURITY_ALERT_SEVERITY = 'danger'
+
def show_version_check?
return false unless Gitlab::CurrentSettings.version_check_enabled
return false if User.single_user&.requires_usage_stats_consent?
@@ -8,6 +12,17 @@ module VersionCheckHelper
current_user&.can_read_all_resources?
end
+ def gitlab_version_check
+ VersionCheck.new.response
+ end
+ strong_memoize_attr :gitlab_version_check
+
+ def show_security_patch_upgrade_alert?
+ return false unless show_version_check? && gitlab_version_check
+
+ gitlab_version_check['severity'] === SECURITY_ALERT_SEVERITY
+ end
+
def link_to_version
if Gitlab.pre_release?
commit_link = link_to(Gitlab.revision, source_host_url + namespace_project_commits_path(source_code_group, source_code_project, Gitlab.revision))
diff --git a/app/helpers/web_hooks/web_hooks_helper.rb b/app/helpers/web_hooks/web_hooks_helper.rb
index e95b90c69ef..bda9bf58fb7 100644
--- a/app/helpers/web_hooks/web_hooks_helper.rb
+++ b/app/helpers/web_hooks/web_hooks_helper.rb
@@ -7,8 +7,6 @@ module WebHooks
def show_project_hook_failed_callout?(project:)
return false if project_hook_page?
return false unless current_user
- return false unless Feature.enabled?(:webhooks_failed_callout, project)
- return false unless Feature.enabled?(:web_hooks_disable_failed, project)
return false unless Ability.allowed?(current_user, :read_web_hooks, project)
# Assumes include of Users::CalloutsHelper
diff --git a/app/helpers/wiki_helper.rb b/app/helpers/wiki_helper.rb
index 017a1861905..b2b8ca2a120 100644
--- a/app/helpers/wiki_helper.rb
+++ b/app/helpers/wiki_helper.rb
@@ -60,7 +60,7 @@ module WikiHelper
end
def wiki_sort_controls(wiki, direction)
- link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn qa-reverse-sort rspec-reverse-sort'
+ link_class = 'gl-button btn btn-default btn-icon has-tooltip reverse-sort-btn rspec-reverse-sort'
reversed_direction = direction == 'desc' ? 'asc' : 'desc'
icon_class = direction == 'desc' ? 'highest' : 'lowest'
title = direction == 'desc' ? _('Sort direction: Descending') : _('Sort direction: Ascending')
diff --git a/app/helpers/x509_helper.rb b/app/helpers/x509_helper.rb
index 1a9dbefceef..599be0b91f7 100644
--- a/app/helpers/x509_helper.rb
+++ b/app/helpers/x509_helper.rb
@@ -16,8 +16,4 @@ module X509Helper
rescue StandardError
{}
end
-
- def x509_signature?(sig)
- sig.is_a?(CommitSignatures::X509CommitSignature) || sig.is_a?(Gitlab::X509::Signature)
- end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 65ea90d0b5d..ede6007e0e2 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -94,12 +94,13 @@ module Emails
end
end
- def access_token_revoked_email(user, token_name)
+ def access_token_revoked_email(user, token_name, source = nil)
return unless user&.active?
@user = user
@token_name = token_name
@target_url = profile_personal_access_tokens_url
+ @source = source
Gitlab::I18n.with_locale(@user.preferred_language) do
mail_with_locale(to: @user.notification_email_or_default, subject: subject(_("A personal access token has been revoked")))
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index 7cfebf0473f..f1f22d94061 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -14,7 +14,7 @@ class AbuseReport < ApplicationRecord
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
- scope :by_user, -> (user) { where(user_id: user) }
+ scope :by_user, ->(user) { where(user_id: user) }
scope :with_users, -> { includes(:reporter, :user) }
# For CacheMarkdownField
diff --git a/app/models/achievements/achievement.rb b/app/models/achievements/achievement.rb
new file mode 100644
index 00000000000..904961491b5
--- /dev/null
+++ b/app/models/achievements/achievement.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module Achievements
+ class Achievement < ApplicationRecord
+ include Avatarable
+ include StripAttribute
+
+ belongs_to :namespace, inverse_of: :achievements, optional: false
+
+ strip_attributes! :name, :description
+
+ validates :name,
+ presence: true,
+ length: { maximum: 255 },
+ uniqueness: { case_sensitive: false, scope: [:namespace_id] }
+ validates :description, length: { maximum: 1024 }
+ end
+end
diff --git a/app/models/alert_management/alert.rb b/app/models/alert_management/alert.rb
index 9f05c87018d..a5a539eae75 100644
--- a/app/models/alert_management/alert.rb
+++ b/app/models/alert_management/alert.rb
@@ -53,7 +53,7 @@ module AlertManagement
validates :fingerprint, allow_blank: true, uniqueness: {
scope: :project,
conditions: -> { not_resolved },
- message: -> (object, data) { _('Cannot have multiple unresolved alerts') }
+ message: ->(object, data) { _('Cannot have multiple unresolved alerts') }
}, unless: :resolved?
validate :hosts_format
@@ -74,23 +74,23 @@ module AlertManagement
delegate :iid, to: :issue, prefix: true, allow_nil: true
delegate :details_url, to: :present
- scope :for_iid, -> (iid) { where(iid: iid) }
- scope :for_fingerprint, -> (project, fingerprint) { where(project: project, fingerprint: fingerprint) }
- scope :for_environment, -> (environment) { where(environment: environment) }
- scope :for_assignee_username, -> (assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
- scope :search, -> (query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
+ scope :for_iid, ->(iid) { where(iid: iid) }
+ scope :for_fingerprint, ->(project, fingerprint) { where(project: project, fingerprint: fingerprint) }
+ scope :for_environment, ->(environment) { where(environment: environment) }
+ scope :for_assignee_username, ->(assignee_username) { joins(:assignees).merge(User.by_username(assignee_username)) }
+ scope :search, ->(query) { fuzzy_search(query, [:title, :description, :monitoring_tool, :service]) }
scope :not_resolved, -> { without_status(:resolved) }
scope :with_prometheus_alert, -> { includes(:prometheus_alert) }
scope :with_operations_alerts, -> { where(domain: :operations) }
- scope :order_start_time, -> (sort_order) { order(started_at: sort_order) }
- scope :order_end_time, -> (sort_order) { order(ended_at: sort_order) }
- scope :order_event_count, -> (sort_order) { order(events: sort_order) }
+ scope :order_start_time, ->(sort_order) { order(started_at: sort_order) }
+ scope :order_end_time, ->(sort_order) { order(ended_at: sort_order) }
+ scope :order_event_count, ->(sort_order) { order(events: sort_order) }
# Ascending sort order sorts severity from less critical to more critical.
# Descending sort order sorts severity from more critical to less critical.
# https://gitlab.com/gitlab-org/gitlab/-/issues/221242#what-is-the-expected-correct-behavior
- scope :order_severity, -> (sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
+ scope :order_severity, ->(sort_order) { order(severity: sort_order == :asc ? :desc : :asc) }
scope :order_severity_with_open_prometheus_alert, -> { open.with_prometheus_alert.order(severity: :asc, started_at: :desc) }
scope :counts_by_project_id, -> { group(:project_id).count }
diff --git a/app/models/alert_management/http_integration.rb b/app/models/alert_management/http_integration.rb
index b2686924363..906855d6dfc 100644
--- a/app/models/alert_management/http_integration.rb
+++ b/app/models/alert_management/http_integration.rb
@@ -28,7 +28,7 @@ module AlertManagement
before_validation :ensure_token
before_validation :ensure_payload_example_not_nil
- scope :for_endpoint_identifier, -> (endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) }
+ scope :for_endpoint_identifier, ->(endpoint_identifier) { where(endpoint_identifier: endpoint_identifier) }
scope :active, -> { where(active: true) }
scope :ordered_by_id, -> { order(:id) }
diff --git a/app/models/analytics/cycle_analytics/aggregation.rb b/app/models/analytics/cycle_analytics/aggregation.rb
index 2e58d64ae95..a888422a6b4 100644
--- a/app/models/analytics/cycle_analytics/aggregation.rb
+++ b/app/models/analytics/cycle_analytics/aggregation.rb
@@ -1,24 +1,15 @@
# frozen_string_literal: true
class Analytics::CycleAnalytics::Aggregation < ApplicationRecord
- include IgnorableColumns
include FromUnion
belongs_to :group, optional: false
validates :incremental_runtimes_in_seconds, :incremental_processed_records, :full_runtimes_in_seconds, :full_processed_records, presence: true, length: { maximum: 10 }, allow_blank: true
- scope :priority_order, -> (column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) }
+ scope :priority_order, ->(column_to_sort = :last_incremental_run_at) { order(arel_table[column_to_sort].asc.nulls_first) }
scope :enabled, -> { where('enabled IS TRUE') }
- # These columns were added with wrong naming convention, the columns were never used.
- ignore_column :last_full_run_processed_records, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_runtimes_in_seconds, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_issues_updated_at, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_mrs_updated_at, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_issues_id, remove_with: '15.1', remove_after: '2022-05-22'
- ignore_column :last_full_run_merge_requests_id, remove_with: '15.1', remove_after: '2022-05-22'
-
def cursor_for(mode, model)
{
updated_at: self["last_#{mode}_#{model.table_name}_updated_at"],
diff --git a/app/models/analytics/usage_trends/measurement.rb b/app/models/analytics/usage_trends/measurement.rb
index 02e239ca0ef..c1245d8dce7 100644
--- a/app/models/analytics/usage_trends/measurement.rb
+++ b/app/models/analytics/usage_trends/measurement.rb
@@ -23,9 +23,9 @@ module Analytics
validates :recorded_at, uniqueness: { scope: :identifier }
scope :order_by_latest, -> { order(recorded_at: :desc) }
- scope :with_identifier, -> (identifier) { where(identifier: identifier) }
- scope :recorded_after, -> (date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? }
- scope :recorded_before, -> (date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? }
+ scope :with_identifier, ->(identifier) { where(identifier: identifier) }
+ scope :recorded_after, ->(date) { where(self.model.arel_table[:recorded_at].gteq(date)) if date.present? }
+ scope :recorded_before, ->(date) { where(self.model.arel_table[:recorded_at].lteq(date)) if date.present? }
def self.identifier_query_mapping
{
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index bd948c2c32a..4a046b3ab20 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -3,10 +3,10 @@
class Appearance < ApplicationRecord
include CacheableAttributes
include CacheMarkdownField
- include ObjectStorage::BackgroundMove
include WithUploads
attribute :title, default: ''
+ attribute :short_title, default: ''
attribute :description, default: ''
attribute :new_project_guidelines, default: ''
attribute :profile_image_guidelines, default: ''
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index adbbddd635c..3fb1f58f3e0 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -11,6 +11,7 @@ class ApplicationSetting < ApplicationRecord
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
ignore_columns %i[static_objects_external_storage_auth_token], remove_with: '14.9', remove_after: '2022-03-22'
ignore_column :user_email_lookup_limit, remove_with: '15.0', remove_after: '2022-04-18'
+ ignore_column :send_user_confirmation_email, remove_with: '15.8', remove_after: '2022-12-18'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -20,7 +21,7 @@ class ApplicationSetting < ApplicationRecord
'Admin Area > Settings > General > Kroki'
enum whats_new_variant: { all_tiers: 0, current_tier: 1, disabled: 2 }, _prefix: true
- enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }
+ enum email_confirmation_setting: { off: 0, soft: 1, hard: 2 }, _prefix: true
add_authentication_token_field :runners_registration_token, encrypted: -> { Feature.enabled?(:application_settings_tokens_optional_encryption) ? :optional : :required }
add_authentication_token_field :health_check_access_token
@@ -87,7 +88,7 @@ class ApplicationSetting < ApplicationRecord
validates :grafana_url,
system_hook_url: {
- blocked_message: "is blocked: %{exception_message}. " + GRAFANA_URL_ERROR_MESSAGE
+ blocked_message: "is blocked: %{exception_message}. #{GRAFANA_URL_ERROR_MESSAGE}"
},
if: :grafana_url_absolute?
@@ -226,6 +227,10 @@ class ApplicationSetting < ApplicationRecord
presence: true,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :max_terraform_state_size_bytes,
+ presence: true,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :default_artifacts_expire_in, presence: true, duration: true
validates :container_expiration_policies_enable_historic_entries,
@@ -412,12 +417,10 @@ class ApplicationSetting < ApplicationRecord
allow_nil: false,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
- # rubocop:disable Cop/StaticTranslationDefinition
validates :deactivate_dormant_users_period,
presence: true,
- numericality: { only_integer: true, greater_than_or_equal_to: 90, message: _("'%{value}' days of inactivity must be greater than or equal to 90") },
+ numericality: { only_integer: true, greater_than_or_equal_to: 90, message: N_("'%{value}' days of inactivity must be greater than or equal to 90") },
if: :deactivate_dormant_users?
- # rubocop:enable Cop/StaticTranslationDefinition
Gitlab::SSHPublicKey.supported_types.each do |type|
validates :"#{type}_key_restriction", presence: true, key_restriction: { type: type }
@@ -466,7 +469,7 @@ class ApplicationSetting < ApplicationRecord
validates :external_auth_client_key,
presence: true,
- if: -> (setting) { setting.external_auth_client_cert.present? }
+ if: ->(setting) { setting.external_auth_client_cert.present? }
validates :lets_encrypt_notification_email,
devise_email: true,
@@ -488,17 +491,17 @@ class ApplicationSetting < ApplicationRecord
validates :eks_access_key_id,
length: { in: 16..128 },
- if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
+ if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
validates :eks_secret_access_key,
presence: true,
- if: -> (setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
+ if: ->(setting) { setting.eks_integration_enabled? && setting.eks_access_key_id.present? }
validates_with X509CertificateCredentialsValidator,
certificate: :external_auth_client_cert,
pkey: :external_auth_client_key,
pass: :external_auth_client_key_pass,
- if: -> (setting) { setting.external_auth_client_cert.present? }
+ if: ->(setting) { setting.external_auth_client_cert.present? }
validates :default_ci_config_path,
format: { without: %r{(\.{2}|\A/)},
@@ -687,6 +690,10 @@ class ApplicationSetting < ApplicationRecord
validates :disable_admin_oauth_scopes,
inclusion: { in: [true, false], message: N_('must be a boolean value') }
+ validates :bulk_import_enabled,
+ allow_nil: false,
+ inclusion: { in: [true, false], message: N_('must be a boolean value') }
+
before_validation :ensure_uuid!
before_validation :coerce_repository_storages_weighted, if: :repository_storages_weighted_changed?
before_validation :normalize_default_branch_name
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 308c05d638c..229c4e68d79 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -76,6 +76,7 @@ module ApplicationSettingImplementation
eks_account_id: nil,
eks_integration_enabled: false,
eks_secret_access_key: nil,
+ email_confirmation_setting: 'off',
email_restrictions_enabled: false,
email_restrictions: nil,
external_pipeline_validation_service_timeout: nil,
@@ -113,6 +114,7 @@ module ApplicationSettingImplementation
max_attachment_size: Settings.gitlab['max_attachment_size'],
max_export_size: 0,
max_import_size: 0,
+ max_terraform_state_size_bytes: 0,
max_yaml_size_bytes: 1.megabyte,
max_yaml_depth: 100,
minimum_password_length: DEFAULT_MINIMUM_PASSWORD_LENGTH,
@@ -146,7 +148,6 @@ module ApplicationSettingImplementation
require_two_factor_authentication: false,
restricted_visibility_levels: Settings.gitlab['restricted_visibility_levels'],
rsa_key_restriction: default_min_key_size(:rsa),
- send_user_confirmation_email: false,
session_expire_delay: Settings.gitlab['session_expire_delay'],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
shared_runners_text: nil,
@@ -243,7 +244,8 @@ module ApplicationSettingImplementation
search_rate_limit_unauthenticated: 10,
users_get_by_id_limit: 300,
users_get_by_id_limit_allowlist: [],
- can_create_group: true
+ can_create_group: true,
+ bulk_import_enabled: false
}
end
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 0ad17cd8869..5cc87be388f 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -28,11 +28,11 @@ class AuditEvent < ApplicationRecord
validates :entity_type, presence: true
validates :ip_address, ip_address: true
- scope :by_entity_type, -> (entity_type) { where(entity_type: entity_type) }
- scope :by_entity_id, -> (entity_id) { where(entity_id: entity_id) }
- scope :by_author_id, -> (author_id) { where(author_id: author_id) }
- scope :by_entity_username, -> (username) { where(entity_id: find_user_id(username)) }
- scope :by_author_username, -> (username) { where(author_id: find_user_id(username)) }
+ scope :by_entity_type, ->(entity_type) { where(entity_type: entity_type) }
+ scope :by_entity_id, ->(entity_id) { where(entity_id: entity_id) }
+ scope :by_author_id, ->(author_id) { where(author_id: author_id) }
+ scope :by_entity_username, ->(username) { where(entity_id: find_user_id(username)) }
+ scope :by_author_username, ->(username) { where(author_id: find_user_id(username)) }
after_initialize :initialize_details
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index e9530a80d9f..f41f0a8be84 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -23,11 +23,11 @@ class AwardEmoji < ApplicationRecord
scope :downvotes, -> { named(DOWNVOTE_NAME) }
scope :upvotes, -> { named(UPVOTE_NAME) }
- scope :named, -> (names) { where(name: names) }
- scope :awarded_by, -> (users) { where(user: users) }
+ scope :named, ->(names) { where(name: names) }
+ scope :awarded_by, ->(users) { where(user: users) }
- after_save :expire_cache
after_destroy :expire_cache
+ after_save :expire_cache
class << self
def votes_for_collection(ids, type)
diff --git a/app/models/badge.rb b/app/models/badge.rb
index 4339d419b48..0676de10d02 100644
--- a/app/models/badge.rb
+++ b/app/models/badge.rb
@@ -8,6 +8,8 @@ class Badge < ApplicationRecord
# the placeholder is found.
PLACEHOLDERS = {
'project_path' => :full_path,
+ 'project_title' => :title,
+ 'project_name' => :path,
'project_id' => :id,
'default_branch' => :default_branch,
'commit_sha' => ->(project) { project.commit&.sha }
diff --git a/app/models/blob_viewer/metrics_dashboard_yml.rb b/app/models/blob_viewer/metrics_dashboard_yml.rb
index cac6b2192d0..4b7a178566c 100644
--- a/app/models/blob_viewer/metrics_dashboard_yml.rb
+++ b/app/models/blob_viewer/metrics_dashboard_yml.rb
@@ -25,11 +25,7 @@ module BlobViewer
private
def parse_blob_data
- if Feature.enabled?(:metrics_dashboard_exhaustive_validations, project)
- exhaustive_metrics_dashboard_validation
- else
- old_metrics_dashboard_validation
- end
+ old_metrics_dashboard_validation
end
def old_metrics_dashboard_validation
@@ -41,14 +37,5 @@ module BlobViewer
rescue ActiveModel::ValidationError => e
e.model.errors.messages.map { |messages| messages.join(': ') }
end
-
- def exhaustive_metrics_dashboard_validation
- yaml = ::Gitlab::Config::Loader::Yaml.new(blob.data).load_raw!
- Gitlab::Metrics::Dashboard::Validator
- .errors(yaml, dashboard_path: blob.path, project: project)
- .map(&:message)
- rescue Gitlab::Config::Loader::FormatError => e
- [e.message]
- end
end
end
diff --git a/app/models/board_group_recent_visit.rb b/app/models/board_group_recent_visit.rb
index dc273e256a8..65299d6dd12 100644
--- a/app/models/board_group_recent_visit.rb
+++ b/app/models/board_group_recent_visit.rb
@@ -12,7 +12,7 @@ class BoardGroupRecentVisit < ApplicationRecord
validates :group, presence: true
validates :board, presence: true
- scope :by_user_parent, -> (user, group) { where(user: user, group: group) }
+ scope :by_user_parent, ->(user, group) { where(user: user, group: group) }
def self.board_parent_relation
:group
diff --git a/app/models/board_project_recent_visit.rb b/app/models/board_project_recent_visit.rb
index 723afd6feab..c5122392b91 100644
--- a/app/models/board_project_recent_visit.rb
+++ b/app/models/board_project_recent_visit.rb
@@ -12,7 +12,7 @@ class BoardProjectRecentVisit < ApplicationRecord
validates :project, presence: true
validates :board, presence: true
- scope :by_user_parent, -> (user, project) { where(user: user, project: project) }
+ scope :by_user_parent, ->(user, project) { where(user: user, project: project) }
def self.board_parent_relation
:project
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index 2200a66b3c2..2565ad5f2b8 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -17,7 +17,7 @@ class BulkImport < ApplicationRecord
enum source_type: { gitlab: 0 }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
- scope :order_by_created_at, -> (direction) { order(created_at: direction) }
+ scope :order_by_created_at, ->(direction) { order(created_at: direction) }
state_machine :status, initial: :created do
state :created, value: 0
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index a2542e669e1..e49c4e09a50 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -53,7 +53,7 @@ class BulkImports::Entity < ApplicationRecord
scope :by_user_id, ->(user_id) { joins(:bulk_import).where(bulk_imports: { user_id: user_id }) }
scope :stale, -> { where('created_at < ?', 8.hours.ago).where(status: [0, 1]) }
scope :by_bulk_import_id, ->(bulk_import_id) { where(bulk_import_id: bulk_import_id) }
- scope :order_by_created_at, -> (direction) { order(created_at: direction) }
+ scope :order_by_created_at, ->(direction) { order(created_at: direction) }
alias_attribute :destination_slug, :destination_name
diff --git a/app/models/bulk_imports/export_upload.rb b/app/models/bulk_imports/export_upload.rb
index a9cba5119af..4304032b28c 100644
--- a/app/models/bulk_imports/export_upload.rb
+++ b/app/models/bulk_imports/export_upload.rb
@@ -3,7 +3,6 @@
module BulkImports
class ExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
self.table_name = 'bulk_import_export_uploads'
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index 357f4629078..b04ef1cb7ae 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -26,7 +26,7 @@ class BulkImports::Tracker < ApplicationRecord
entity_scope = where(bulk_import_entity_id: entity_id)
next_stage_scope = entity_scope.with_status(:created).select('MIN(stage)')
- entity_scope.where(stage: next_stage_scope)
+ entity_scope.where(stage: next_stage_scope).with_status(:created)
}
def self.stage_running?(entity_id, stage)
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index d6051d70503..662fb3cffa8 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -18,8 +18,11 @@ module Ci
belongs_to :project
belongs_to :trigger_request
+
+ # To be removed upon :ci_bridge_remove_sourced_pipelines feature flag removal
has_many :sourced_pipelines, class_name: "::Ci::Sources::Pipeline",
- foreign_key: :source_job_id
+ foreign_key: :source_job_id,
+ inverse_of: :source_bridge
has_one :downstream_pipeline, through: :sourced_pipeline, source: :pipeline
@@ -86,8 +89,20 @@ module Ci
end
end
+ def sourced_pipelines
+ if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
+ raise 'Ci::Bridge does not have sourced_pipelines association'
+ end
+
+ super
+ end
+
def has_downstream_pipeline?
- sourced_pipelines.exists?
+ if Feature.enabled?(:ci_bridge_remove_sourced_pipelines, project)
+ sourced_pipeline.present?
+ else
+ sourced_pipelines.exists?
+ end
end
def downstream_pipeline_params
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index f44ba124fe2..7f42b21bc87 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -7,7 +7,6 @@ module Ci
include Ci::Contextable
include TokenAuthenticatable
include AfterCommitQueue
- include ObjectStorage::BackgroundMove
include Presentable
include Importable
include Ci::HasRef
@@ -47,7 +46,7 @@ module Ci
# DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`.
# Details: https://gitlab.com/gitlab-org/gitlab/-/issues/24644#note_689472685
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
- has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
+ has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id, inverse_of: :job
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
has_many :pages_deployments, inverse_of: :ci_build
@@ -71,6 +70,7 @@ module Ci
delegate :harbor_integration, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
delegate :ensure_persistent_ref, to: :pipeline
+ delegate :enable_debug_trace!, to: :metadata
serialize :options # rubocop:disable Cop/ActiveRecordSerialize
serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiveRecordSerialize
@@ -90,7 +90,7 @@ module Ci
scope :with_downloadable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
- .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id")
.where(file_type: Ci::JobArtifact::DOWNLOADABLE_TYPES)
)
end
@@ -98,7 +98,7 @@ module Ci
scope :with_erasable_artifacts, -> do
where('EXISTS (?)',
Ci::JobArtifact.select(1)
- .where('ci_builds.id = ci_job_artifacts.job_id')
+ .where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id")
.where(file_type: Ci::JobArtifact.erasable_file_types)
)
end
@@ -108,11 +108,11 @@ module Ci
end
scope :with_existing_job_artifacts, ->(query) do
- where('EXISTS (?)', ::Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').merge(query))
+ where('EXISTS (?)', ::Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").merge(query))
end
scope :without_archived_trace, -> do
- where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where("#{Ci::Build.quoted_table_name}.id = #{Ci::JobArtifact.quoted_table_name}.job_id").trace)
end
scope :with_artifacts, ->(artifact_scope) do
@@ -155,7 +155,7 @@ module Ci
scope :manual_actions, -> { where(when: :manual, status: COMPLETED_STATUSES + %i[manual]) }
scope :scheduled_actions, -> { where(when: :delayed, status: COMPLETED_STATUSES + %i[scheduled]) }
scope :ref_protected, -> { where(protected: true) }
- scope :with_live_trace, -> { where('EXISTS (?)', Ci::BuildTraceChunk.where('ci_builds.id = ci_build_trace_chunks.build_id').select(1)) }
+ 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
@@ -172,8 +172,6 @@ module Ci
add_authentication_token_field :token, encrypted: :required
- before_save :ensure_token, unless: :assign_token_on_scheduling?
-
after_save :stick_build_if_status_changed
after_create unless: :importing? do |build|
@@ -247,11 +245,8 @@ module Ci
!build.waiting_for_deployment_approval? # If false is returned, it stops the transition
end
- before_transition any => [:pending] do |build, transition|
- if build.assign_token_on_scheduling?
- build.ensure_token
- end
-
+ before_transition any => [:pending] do |build|
+ build.ensure_token
true
end
@@ -419,12 +414,12 @@ module Ci
end
def waiting_for_deployment_approval?
- manual? && starts_environment? && deployment&.blocked?
+ manual? && deployment_job? && deployment&.blocked?
end
def outdated_deployment?
strong_memoize(:outdated_deployment) do
- starts_environment? &&
+ deployment_job? &&
incomplete? &&
project.ci_forward_deployment_enabled? &&
deployment&.older_than_last_successful_deployment?
@@ -528,7 +523,7 @@ module Ci
environment.present?
end
- def starts_environment?
+ def deployment_job?
has_environment_keyword? && self.environment_action == 'start'
end
@@ -722,7 +717,7 @@ module Ci
end
def ensure_trace_metadata!
- Ci::BuildTraceMetadata.find_or_upsert_for!(id)
+ Ci::BuildTraceMetadata.find_or_upsert_for!(id, partition_id)
end
def artifacts_expose_as
@@ -866,6 +861,10 @@ module Ci
Gitlab::Ci::Build::Step.from_after_script(self)].compact
end
+ def runtime_hooks
+ Gitlab::Ci::Build::Hook.from_hooks(self)
+ end
+
def image
Gitlab::Ci::Build::Image.from_image(self)
end
@@ -995,7 +994,7 @@ module Ci
# Virtual deployment status depending on the environment status.
def deployment_status
- return unless starts_environment?
+ return unless deployment_job?
if success?
return successful_deployment_status
@@ -1136,8 +1135,15 @@ module Ci
end
end
- def assign_token_on_scheduling?
- ::Feature.enabled?(:ci_assign_job_token_on_scheduling, project)
+ def partition_id_token_prefix
+ partition_id.to_s(16) if Feature.enabled?(:ci_build_partition_id_token_prefix, project)
+ end
+
+ override :format_token
+ def format_token(token)
+ return token if partition_id_token_prefix.nil?
+
+ "#{partition_id_token_prefix}_#{token}"
end
protected
@@ -1208,11 +1214,11 @@ module Ci
if project.ci_cd_settings.opt_in_jwt?
id_tokens_variables
else
- legacy_jwt_variables.concat(id_tokens_variables)
+ predefined_jwt_variables.concat(id_tokens_variables)
end
end
- def legacy_jwt_variables
+ def predefined_jwt_variables
Gitlab::Ci::Variables::Collection.new.tap do |variables|
jwt = Gitlab::Ci::Jwt.for_build(self)
jwt_v2 = Gitlab::Ci::JwtV2.for_build(self)
@@ -1229,7 +1235,7 @@ module Ci
Gitlab::Ci::Variables::Collection.new.tap do |variables|
id_tokens.each do |var_name, token_data|
- token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['id_token']['aud'])
+ token = Gitlab::Ci::JwtV2.for_build(self, aud: token_data['aud'])
variables.append(key: var_name, value: token, public: false, masked: true)
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 2f28509f812..9b4794abb2e 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -5,21 +5,16 @@ module Ci
# Data that should be persisted forever, should be stored with Ci::Build model.
class BuildMetadata < Ci::ApplicationRecord
BuildTimeout = Struct.new(:value, :source)
- ROUTING_FEATURE_FLAG = :ci_partitioning_use_ci_builds_metadata_routing_table
include Ci::Partitionable
include Presentable
include ChronicDurationAttribute
include Gitlab::Utils::StrongMemoize
- self.table_name = 'ci_builds_metadata'
+ self.table_name = 'p_ci_builds_metadata'
self.primary_key = 'id'
- self.sequence_name = 'ci_builds_metadata_id_seq'
- partitionable scope: :build, through: {
- table: :p_ci_builds_metadata,
- flag: ROUTING_FEATURE_FLAG
- }
+ partitionable scope: :build
belongs_to :build, class_name: 'CommitStatus'
belongs_to :project
@@ -63,6 +58,12 @@ module Ci
runtime_runner_features[:cancel_gracefully] == true
end
+ def enable_debug_trace!
+ self.debug_trace_enabled = true
+ save! if changes.any?
+ true
+ end
+
private
def set_build_project
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index d4cbbfac4ab..3fa17d6d286 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -2,15 +2,18 @@
module Ci
class BuildNeed < Ci::ApplicationRecord
+ include Ci::Partitionable
include BulkInsertSafe
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 :optional, inclusion: { in: [true, false] }
- scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
+ scope :scoped_build, -> { where("#{Ci::Build.quoted_table_name}.id = #{quoted_table_name}.build_id") }
scope :artifacts, -> { where(artifacts: true) }
end
end
diff --git a/app/models/ci/build_pending_state.rb b/app/models/ci/build_pending_state.rb
index 53cf0697e2e..3684dac06c7 100644
--- a/app/models/ci/build_pending_state.rb
+++ b/app/models/ci/build_pending_state.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class Ci::BuildPendingState < Ci::ApplicationRecord
+ include Ci::Partitionable
+
belongs_to :build, class_name: 'Ci::Build', foreign_key: :build_id
+ partitionable scope: :build
+
enum state: Ci::Stage.statuses
enum failure_reason: CommitStatus.failure_reasons
diff --git a/app/models/ci/build_report_result.rb b/app/models/ci/build_report_result.rb
index b674c1b1a0e..b2d99fab295 100644
--- a/app/models/ci/build_report_result.rb
+++ b/app/models/ci/build_report_result.rb
@@ -2,11 +2,15 @@
module Ci
class BuildReportResult < Ci::ApplicationRecord
+ include Ci::Partitionable
+
self.primary_key = :build_id
belongs_to :build, class_name: "Ci::Build", inverse_of: :report_results
belongs_to :project, class_name: "Project", inverse_of: :build_report_results
+ partitionable scope: :build
+
validates :build, :project, presence: true
validates :data, json_schema: { filename: "build_report_result_data" }
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 0f37ce70964..20c0b04e228 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -4,6 +4,8 @@ module Ci
# The purpose of this class is to store Build related runner session.
# Data will be removed after transitioning from running to any state.
class BuildRunnerSession < Ci::ApplicationRecord
+ include Ci::Partitionable
+
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
DEFAULT_PORT_NAME = 'default_port'
@@ -12,6 +14,8 @@ module Ci
belongs_to :build, class_name: 'Ci::Build', inverse_of: :runner_session
+ partitionable scope: :build
+
validates :build, presence: true
validates :url, public_url: { schemes: %w(https) }
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7baa98b59f9..57d8b9ba368 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -2,6 +2,7 @@
module Ci
class BuildTraceChunk < Ci::ApplicationRecord
+ include Ci::Partitionable
include ::Comparable
include ::FastDestroyAll
include ::Checksummable
@@ -10,6 +11,8 @@ module Ci
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ partitionable scope: :build
+
attribute :data_store, default: :redis_trace_chunks
after_create { metrics.increment_trace_operation(operation: :chunked) }
@@ -28,8 +31,8 @@ module Ci
redis_trace_chunks: 4
}.freeze
- STORE_TYPES = DATA_STORES.keys.to_h do |store|
- [store, "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize]
+ STORE_TYPES = DATA_STORES.keys.index_with do |store|
+ "Ci::BuildTraceChunks::#{store.to_s.camelize}".constantize
end.freeze
LIVE_STORES = %i[redis redis_trace_chunks].freeze
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index 86de90983ff..00cf1531483 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -2,6 +2,8 @@
module Ci
class BuildTraceMetadata < Ci::ApplicationRecord
+ include Ci::Partitionable
+
MAX_ATTEMPTS = 5
self.table_name = 'ci_build_trace_metadata'
self.primary_key = :build_id
@@ -9,15 +11,17 @@ module Ci
belongs_to :build, class_name: 'Ci::Build'
belongs_to :trace_artifact, class_name: 'Ci::JobArtifact'
+ partitionable scope: :build
+
validates :build, presence: true
validates :archival_attempts, presence: true
- def self.find_or_upsert_for!(build_id)
- record = find_by(build_id: build_id)
+ def self.find_or_upsert_for!(build_id, partition_id)
+ record = find_by(build_id: build_id, partition_id: partition_id)
return record if record
- upsert({ build_id: build_id }, unique_by: :build_id)
- find_by!(build_id: build_id)
+ upsert({ build_id: build_id, partition_id: partition_id }, unique_by: :build_id)
+ find_by!(build_id: build_id, partition_id: partition_id)
end
# The job is retried around 5 times during the 7 days retention period for
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index da0bbbacddd..1bf32e04a15 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -4,6 +4,10 @@ module Ci
class FreezePeriod < Ci::ApplicationRecord
include StripAttribute
include Ci::NamespacedModelName
+ include Gitlab::Utils::StrongMemoize
+
+ STATUS_ACTIVE = :active
+ STATUS_INACTIVE = :inactive
default_scope { order(created_at: :asc) } # rubocop:disable Cop/DefaultScope
@@ -14,5 +18,60 @@ module Ci
validates :freeze_start, cron: true, presence: true
validates :freeze_end, cron: true, presence: true
validates :cron_timezone, cron_freeze_period_timezone: true, presence: true
+
+ def active?
+ status == STATUS_ACTIVE
+ end
+
+ def status
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:status") do
+ within_freeze_period? ? STATUS_ACTIVE : STATUS_INACTIVE
+ end
+ end
+
+ def time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_start") do
+ freeze_start_parsed_cron.previous_time_from(time_zone_now)
+ end
+ end
+
+ def next_time_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:next_time_start") do
+ freeze_start_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_now
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_now") do
+ freeze_end_parsed_cron.next_time_from(time_zone_now)
+ end
+ end
+
+ def time_end_from_start
+ Gitlab::SafeRequestStore.fetch("ci:freeze_period:#{id}:time_end_from_start") do
+ freeze_end_parsed_cron.next_time_from(time_start)
+ end
+ end
+
+ private
+
+ def within_freeze_period?
+ time_start <= time_zone_now && time_zone_now <= time_end_from_start
+ end
+
+ def freeze_start_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_start, cron_timezone)
+ end
+ strong_memoize_attr :freeze_start_parsed_cron
+
+ def freeze_end_parsed_cron
+ Gitlab::Ci::CronParser.new(freeze_end, cron_timezone)
+ end
+ strong_memoize_attr :freeze_end_parsed_cron
+
+ def time_zone_now
+ Time.zone.now
+ end
+ strong_memoize_attr :time_zone_now
end
end
diff --git a/app/models/ci/freeze_period_status.rb b/app/models/ci/freeze_period_status.rb
deleted file mode 100644
index e810bb3f229..00000000000
--- a/app/models/ci/freeze_period_status.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- class FreezePeriodStatus
- attr_reader :project
-
- def initialize(project:)
- @project = project
- end
-
- def execute
- project.freeze_periods.any? { |period| within_freeze_period?(period) }
- end
-
- def within_freeze_period?(period)
- start_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_start, period.cron_timezone)
- end_freeze_cron = Gitlab::Ci::CronParser.new(period.freeze_end, period.cron_timezone)
-
- start_freeze = start_freeze_cron.previous_time_from(time_zone_now)
- end_freeze = end_freeze_cron.next_time_from(start_freeze)
-
- start_freeze <= time_zone_now && time_zone_now <= end_freeze
- end
-
- private
-
- def time_zone_now
- @time_zone_now ||= Time.zone.now
- end
- end
-end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index 922806a21c3..53c358f4eba 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -5,7 +5,6 @@ module Ci
include Ci::Partitionable
include IgnorableColumns
include AfterCommitQueue
- include ObjectStorage::BackgroundMove
include UpdateProjectStatistics
include UsageStatistics
include Sortable
@@ -52,7 +51,8 @@ module Ci
cobertura: 'cobertura-coverage.xml',
terraform: 'tfplan.json',
cluster_applications: 'gl-cluster-applications.json', # DEPRECATED: https://gitlab.com/gitlab-org/gitlab/-/issues/361094
- requirements: 'requirements.json',
+ requirements: 'requirements.json', # Will be DEPRECATED soon: https://gitlab.com/groups/gitlab-org/-/epics/9203
+ requirements_v2: 'requirements_v2.json',
coverage_fuzzing: 'gl-coverage-fuzzing.json',
api_fuzzing: 'gl-api-fuzzing-report.json',
cyclonedx: 'gl-sbom.cdx.json'
@@ -95,6 +95,7 @@ module Ci
load_performance: :raw,
terraform: :raw,
requirements: :raw,
+ requirements_v2: :raw,
coverage_fuzzing: :raw,
api_fuzzing: :raw
}.freeze
@@ -119,6 +120,7 @@ module Ci
sast
secret_detection
requirements
+ requirements_v2
cluster_image_scanning
cyclonedx
].freeze
@@ -209,7 +211,8 @@ module Ci
load_performance: 25, ## EE-specific
api_fuzzing: 26, ## EE-specific
cluster_image_scanning: 27, ## EE-specific
- cyclonedx: 28 ## EE-specific
+ cyclonedx: 28, ## EE-specific
+ requirements_v2: 29 ## EE-specific
}
# `file_location` indicates where actual files are stored.
diff --git a/app/models/ci/job_token/allowlist.rb b/app/models/ci/job_token/allowlist.rb
new file mode 100644
index 00000000000..9e9a0a68ebd
--- /dev/null
+++ b/app/models/ci/job_token/allowlist.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+module Ci
+ module JobToken
+ class Allowlist
+ def initialize(source_project, direction:)
+ @source_project = source_project
+ @direction = direction
+ end
+
+ def includes?(target_project)
+ source_links
+ .with_target(target_project)
+ .exists?
+ end
+
+ def projects
+ Project.from_union(target_projects, remove_duplicates: false)
+ end
+
+ private
+
+ def source_links
+ Ci::JobToken::ProjectScopeLink
+ .with_source(@source_project)
+ .where(direction: @direction)
+ end
+
+ def target_project_ids
+ source_links
+ # pluck needed to avoid ci and main db join
+ .pluck(:target_project_id)
+ end
+
+ def target_projects
+ [
+ Project.id_in(@source_project),
+ Project.id_in(target_project_ids)
+ ]
+ end
+ end
+ end
+end
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
index 3fdf07123e6..b784f93651a 100644
--- a/app/models/ci/job_token/project_scope_link.rb
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -12,8 +12,8 @@ module Ci
belongs_to :target_project, class_name: 'Project'
belongs_to :added_by, class_name: 'User'
- scope :from_project, ->(project) { where(source_project: project) }
- scope :to_project, ->(project) { where(target_project: project) }
+ scope :with_source, ->(project) { where(source_project: project) }
+ scope :with_target, ->(project) { where(target_project: project) }
validates :source_project, presence: true
validates :target_project, presence: true
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 1aa49b95201..e320c0f92d1 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -1,49 +1,58 @@
# frozen_string_literal: true
-# This model represents the surface where a CI_JOB_TOKEN can be used.
-# A Scope is initialized with the project that the job token belongs to,
-# and indicates what are all the other projects that the token could access.
+# This model represents the scope of access for a CI_JOB_TOKEN.
#
-# By default a job token can only access its own project, which is the same
-# project that defines the scope.
-# By adding ScopeLinks to the scope we can allow other projects to be accessed
-# by the job token. This works as an allowlist of projects for a job token.
+# A scope is initialized with a project.
+#
+# Projects can be added to the scope by adding ScopeLinks to
+# create an allowlist of projects in either access direction (inbound, outbound).
+#
+# Currently, projects in the outbound allowlist can be accessed via the token
+# in the source project.
+#
+# TODO(Issue #346298) Projects in the inbound allowlist can use their token to access
+# the source project.
+#
+# CI_JOB_TOKEN should be considered untrusted without these features enabled.
#
-# If a project is not included in the scope we should not allow the job user
-# to access it since operations using CI_JOB_TOKEN should be considered untrusted.
module Ci
module JobToken
class Scope
- attr_reader :source_project
+ attr_reader :current_project
- def initialize(project)
- @source_project = project
+ def initialize(current_project)
+ @current_project = current_project
end
- def includes?(target_project)
- # if the setting is disabled any project is considered to be in scope.
- return true unless source_project.ci_outbound_job_token_scope_enabled?
+ def allows?(accessed_project)
+ self_referential?(accessed_project) || outbound_allows?(accessed_project)
+ end
- target_project.id == source_project.id ||
- Ci::JobToken::ProjectScopeLink.from_project(source_project).to_project(target_project).exists?
+ def outbound_projects
+ outbound_allowlist.projects
end
+ # Deprecated: use outbound_projects, TODO(Issue #346298) remove references to all_project
def all_projects
- Project.from_union(target_projects, remove_duplicates: false)
+ outbound_projects
end
private
- def target_project_ids
- Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
+ def outbound_allows?(accessed_project)
+ # if the setting is disabled any project is considered to be in scope.
+ return true unless @current_project.ci_outbound_job_token_scope_enabled?
+
+ outbound_allowlist.includes?(accessed_project)
+ end
+
+ def outbound_allowlist
+ Ci::JobToken::Allowlist.new(@current_project, direction: :outbound)
end
- def target_projects
- [
- Project.id_in(source_project),
- Project.id_in(target_project_ids)
- ]
+ def self_referential?(accessed_project)
+ @current_project.id == accessed_project.id
end
end
end
diff --git a/app/models/ci/job_variable.rb b/app/models/ci/job_variable.rb
index 332a78b66ae..998f0647ad5 100644
--- a/app/models/ci/job_variable.rb
+++ b/app/models/ci/job_variable.rb
@@ -2,12 +2,15 @@
module Ci
class JobVariable < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::NewHasVariable
include Ci::RawVariable
include BulkInsertSafe
belongs_to :job, class_name: "Ci::Build", foreign_key: :job_id
+ partitionable scope: :job
+
alias_attribute :secret_value, :value
validates :key, uniqueness: { scope: :job_id }, unless: :dotenv_source?
diff --git a/app/models/ci/pending_build.rb b/app/models/ci/pending_build.rb
index 0fa6a234a3d..2b1eb67d4f2 100644
--- a/app/models/ci/pending_build.rb
+++ b/app/models/ci/pending_build.rb
@@ -3,11 +3,14 @@
module Ci
class PendingBuild < Ci::ApplicationRecord
include EachBatch
+ include Ci::Partitionable
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :namespace, inverse_of: :pending_builds, class_name: 'Namespace'
+ partitionable scope: :build
+
validates :namespace, presence: true
scope :ref_protected, -> { where(protected: true) }
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 020f5cf9d8e..05207fb1ca0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -350,9 +350,13 @@ module Ci
scope :for_sha_or_source_sha, -> (sha) { for_sha(sha).or(for_source_sha(sha)) }
scope :for_ref, -> (ref) { where(ref: ref) }
scope :for_branch, -> (branch) { for_ref(branch).where(tag: false) }
- scope :for_id, -> (id) { where(id: id) }
scope :for_iid, -> (iid) { where(iid: iid) }
scope :for_project, -> (project_id) { where(project_id: project_id) }
+ scope :for_name, -> (name) do
+ name_column = Ci::PipelineMetadata.arel_table[:name]
+
+ joins(:pipeline_metadata).where(name_column.lower.eq(name.downcase))
+ end
scope :created_after, -> (time) { where(arel_table[:created_at].gt(time)) }
scope :created_before_id, -> (id) { where(arel_table[:id].lt(id)) }
scope :before_pipeline, -> (pipeline) { created_before_id(pipeline.id).outside_pipeline_family(pipeline) }
@@ -721,7 +725,7 @@ module Ci
def freeze_period?
strong_memoize(:freeze_period) do
- Ci::FreezePeriodStatus.new(project: project).execute
+ project.freeze_periods.any?(&:active?)
end
end
@@ -1341,13 +1345,14 @@ module Ci
persistent_ref.create
end
+ # For dependent bridge jobs we reset the upstream bridge recursively
+ # to reflect that a downstream pipeline is running again
def reset_source_bridge!(current_user)
# break recursion when no source_pipeline bridge (first upstream pipeline)
return unless bridge_waiting?
return unless current_user.can?(:update_pipeline, source_bridge.pipeline)
- source_bridge.pending!
- Ci::AfterRequeueJobService.new(project, current_user).execute(source_bridge) # rubocop:disable CodeReuse/ServiceClass
+ Ci::EnqueueJobService.new(source_bridge, current_user: current_user).execute(&:pending!) # rubocop:disable CodeReuse/ServiceClass
end
# EE-only
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index 96e5567e85e..20ff07e88ba 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -16,7 +16,7 @@ module Ci
belongs_to :owner, class_name: 'User'
has_one :last_pipeline, -> { order(id: :desc) }, class_name: 'Ci::Pipeline'
has_many :pipelines
- has_many :variables, class_name: 'Ci::PipelineScheduleVariable', validate: false
+ has_many :variables, class_name: 'Ci::PipelineScheduleVariable'
validates :cron, unless: :importing?, cron: true, presence: { unless: :importing? }
validates :cron_timezone, cron_timezone: true, presence: { unless: :importing? }
@@ -78,8 +78,6 @@ module Ci
ref.start_with? 'refs/tags/'
end
- private
-
def worker_cron_expression
Settings.cron_jobs['pipeline_schedule_worker']['cron']
end
diff --git a/app/models/ci/pipeline_schedule_variable.rb b/app/models/ci/pipeline_schedule_variable.rb
index 718ed14edeb..00251ea06fd 100644
--- a/app/models/ci/pipeline_schedule_variable.rb
+++ b/app/models/ci/pipeline_schedule_variable.rb
@@ -9,6 +9,6 @@ module Ci
alias_attribute :secret_value, :value
- validates :key, uniqueness: { scope: :pipeline_schedule_id }
+ validates :key, presence: true, uniqueness: { scope: :pipeline_schedule_id }
end
end
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index eb805ffae0a..37c82c125aa 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -104,8 +104,8 @@ module Ci
to: :pipeline
def clone(current_user:, new_job_variables_attributes: [])
- new_attributes = self.class.clone_accessors.to_h do |attribute|
- [attribute, public_send(attribute)] # rubocop:disable GitlabSecurity/PublicSend
+ new_attributes = self.class.clone_accessors.index_with do |attribute|
+ public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
end
if persisted_environment.present?
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index 6d25f747a9d..b788e4f58c1 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -24,11 +24,18 @@ module Ci
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
def assign_resource_to(processable)
- resources.free.limit(1).update_all(build_id: processable.id) > 0
+ attrs = {
+ build_id: processable.id,
+ partition_id: processable.partition_id
+ }
+
+ resources.free.limit(1).update_all(attrs) > 0
end
def release_resource_from(processable)
- resources.retained_by(processable).update_all(build_id: nil) > 0
+ attrs = { build_id: nil, partition_id: nil }
+
+ resources.retained_by(processable).update_all(attrs) > 0
end
def upcoming_processables
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 3be627989b1..a7f3ff938c3 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -89,6 +89,9 @@ module Ci
scope :ordered, -> { order(id: :desc) }
scope :with_recent_runner_queue, -> { where('contacted_at > ?', recent_queue_deadline) }
+ scope :with_running_builds, -> do
+ where('EXISTS(?)', ::Ci::Build.running.select(1).where('ci_builds.runner_id = ci_runners.id'))
+ end
# BACKWARD COMPATIBILITY: There are needed to maintain compatibility with `AVAILABLE_SCOPES` used by `lib/api/runners.rb`
scope :deprecated_shared, -> { instance_type }
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index 82390ccc538..502ceae3675 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -15,6 +15,8 @@ module Ci
validates :runner_id, uniqueness: { scope: :namespace_id }
validate :group_runner_type
+ scope :for_runner, ->(runner_id) { where(runner_id: runner_id) }
+
def recent_runners
::Ci::Runner.belonging_to_group(namespace_id).recent
end
diff --git a/app/models/ci/running_build.rb b/app/models/ci/running_build.rb
index ae38d54862d..43214b0c336 100644
--- a/app/models/ci/running_build.rb
+++ b/app/models/ci/running_build.rb
@@ -1,7 +1,18 @@
# frozen_string_literal: true
module Ci
+ # This model represents metadata for a running build.
+ # Despite the generic RunningBuild name, in this first iteration it applies only to shared runners
+ # (see Ci::RunningBuild.upsert_shared_runner_build!).
+ # The decision to insert all of the running builds here was deferred to avoid the pressure on the database as
+ # at this time that was not necessary.
+ # We can reconsider the decision to limit this only to shared runners when there is more evidence that inserting all
+ # of the running builds there is worth the additional pressure.
class RunningBuild < Ci::ApplicationRecord
+ include Ci::Partitionable
+
+ partitionable scope: :build
+
belongs_to :project
belongs_to :build, class_name: 'Ci::Build'
belongs_to :runner, class_name: 'Ci::Runner'
diff --git a/app/models/ci/secure_file.rb b/app/models/ci/secure_file.rb
index df38398e5a9..1e6c48bbef5 100644
--- a/app/models/ci/secure_file.rb
+++ b/app/models/ci/secure_file.rb
@@ -17,20 +17,19 @@ module Ci
validates :file, presence: true, file_size: { maximum: FILE_SIZE_LIMIT }
validates :checksum, :file_store, :name, :project_id, presence: true
validates :name, uniqueness: { scope: :project }
+
+ attribute :metadata, :ind_jsonb
validates :metadata, json_schema: { filename: "ci_secure_file_metadata" }, allow_nil: true
+ attribute :file_store, default: -> { Ci::SecureFileUploader.default_store }
+ mount_file_store_uploader Ci::SecureFileUploader
+
after_initialize :generate_key_data
before_validation :assign_checksum
scope :order_by_created_at, -> { order(created_at: :desc) }
scope :project_id_in, ->(ids) { where(project_id: ids) }
- serialize :metadata, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
-
- attribute :file_store, default: -> { Ci::SecureFileUploader.default_store }
-
- mount_file_store_uploader Ci::SecureFileUploader
-
def checksum_algorithm
CHECKSUM_ALGORITHM
end
diff --git a/app/models/ci/sources/pipeline.rb b/app/models/ci/sources/pipeline.rb
index 2df504cd3de..855e68d1db1 100644
--- a/app/models/ci/sources/pipeline.rb
+++ b/app/models/ci/sources/pipeline.rb
@@ -3,6 +3,7 @@
module Ci
module Sources
class Pipeline < Ci::ApplicationRecord
+ include Ci::Partitionable
include Ci::NamespacedModelName
self.table_name = "ci_sources_pipelines"
@@ -15,6 +16,11 @@ module Ci
belongs_to :source_bridge, class_name: "Ci::Bridge", foreign_key: :source_job_id
belongs_to :source_pipeline, class_name: "Ci::Pipeline", foreign_key: :source_pipeline_id
+ partitionable scope: :pipeline
+
+ before_validation :set_source_partition_id, on: :create
+ validates :source_partition_id, presence: true
+
validates :project, presence: true
validates :pipeline, presence: true
@@ -23,6 +29,15 @@ module Ci
validates :source_pipeline, presence: true
scope :same_project, -> { where(arel_table[:source_project_id].eq(arel_table[:project_id])) }
+
+ private
+
+ def set_source_partition_id
+ return if source_partition_id_changed? && source_partition_id.present?
+ return unless source_job
+
+ self.source_partition_id = source_job.partition_id
+ end
end
end
end
diff --git a/app/models/ci/unit_test_failure.rb b/app/models/ci/unit_test_failure.rb
index a5aa3b70e37..cfef1249164 100644
--- a/app/models/ci/unit_test_failure.rb
+++ b/app/models/ci/unit_test_failure.rb
@@ -2,6 +2,8 @@
module Ci
class UnitTestFailure < Ci::ApplicationRecord
+ include Ci::Partitionable
+
REPORT_WINDOW = 14.days
validates :unit_test, :build, :failed_at, presence: true
@@ -9,6 +11,8 @@ module Ci
belongs_to :unit_test, class_name: "Ci::UnitTest", foreign_key: :unit_test_id
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
+ partitionable scope: :build
+
scope :deletable, -> { where('failed_at < ?', REPORT_WINDOW.ago) }
def self.recent_failures_count(project:, unit_test_keys:, date_range: REPORT_WINDOW.ago..Time.current)
diff --git a/app/models/clusters/agent_token.rb b/app/models/clusters/agent_token.rb
index 1607d0b6d19..e2dcff13a69 100644
--- a/app/models/clusters/agent_token.rb
+++ b/app/models/clusters/agent_token.rb
@@ -25,5 +25,9 @@ module Clusters
active: 0,
revoked: 1
}
+
+ def to_ability_name
+ :cluster
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 54de45ebba7..5175842e5de 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -359,6 +359,10 @@ class Commit
end
def has_signature?
+ if signature_type == :SSH && !ssh_signatures_enabled?
+ return false
+ end
+
signature_type && signature_type != :NONE
end
@@ -378,6 +382,10 @@ class Commit
@signature_type ||= raw_signature_type || :NONE
end
+ def ssh_signatures_enabled?
+ Feature.enabled?(:ssh_commit_signatures, project)
+ end
+
def signature
strong_memoize(:signature) do
case signature_type
@@ -385,6 +393,8 @@ class Commit
gpg_commit.signature
when :X509
Gitlab::X509::Commit.new(self).signature
+ when :SSH
+ Gitlab::Ssh::Commit.new(self).signature if ssh_signatures_enabled?
else
nil
end
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index e2f0de52bc9..87029cb2033 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -148,7 +148,7 @@ class CommitRange
def sha_start
return unless sha_from
- exclude_start? ? sha_from + '^' : sha_from
+ exclude_start? ? "#{sha_from}^" : sha_from
end
def commit_start
diff --git a/app/models/commit_signatures/gpg_signature.rb b/app/models/commit_signatures/gpg_signature.rb
index 2ae59853520..a9e8ca2dd33 100644
--- a/app/models/commit_signatures/gpg_signature.rb
+++ b/app/models/commit_signatures/gpg_signature.rb
@@ -2,6 +2,7 @@
module CommitSignatures
class GpgSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
sha_attribute :gpg_key_primary_keyid
@@ -10,6 +11,14 @@ module CommitSignatures
validates :gpg_key_primary_keyid, presence: true
+ def signed_by_user
+ gpg_key&.user
+ end
+
+ def type
+ :gpg
+ end
+
def self.with_key_and_subkeys(gpg_key)
subkey_ids = gpg_key.subkeys.pluck(:id)
diff --git a/app/models/commit_signatures/ssh_signature.rb b/app/models/commit_signatures/ssh_signature.rb
index 7a8d0653fcd..1e64e2b2978 100644
--- a/app/models/commit_signatures/ssh_signature.rb
+++ b/app/models/commit_signatures/ssh_signature.rb
@@ -3,7 +3,16 @@
module CommitSignatures
class SshSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :key, optional: true
+
+ def type
+ :ssh
+ end
+
+ def signed_by_user
+ key&.user
+ end
end
end
diff --git a/app/models/commit_signatures/x509_commit_signature.rb b/app/models/commit_signatures/x509_commit_signature.rb
index 2cbb331dd7e..4edbc147502 100644
--- a/app/models/commit_signatures/x509_commit_signature.rb
+++ b/app/models/commit_signatures/x509_commit_signature.rb
@@ -2,15 +2,24 @@
module CommitSignatures
class X509CommitSignature < ApplicationRecord
include CommitSignature
+ include SignatureType
belongs_to :x509_certificate, class_name: 'X509Certificate', foreign_key: 'x509_certificate_id', optional: false
validates :x509_certificate_id, presence: true
+ def type
+ :x509
+ end
+
def x509_commit
return unless commit
Gitlab::X509::Commit.new(commit)
end
+
+ def signed_by_user
+ commit&.committer
+ end
end
end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index b32502c3ee2..f419fa8518e 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -16,7 +16,6 @@ module Avatarable
included do
prepend ShadowMethods
- include ObjectStorage::BackgroundMove
include Gitlab::Utils::StrongMemoize
include ApplicationHelper
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index ec0cf36d875..6a855198697 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -40,7 +40,7 @@ module CacheMarkdownField
# Banzai is less strict about authors, so don't always have an author key
context[:author] = self.author if self.respond_to?(:author)
- context[:markdown_engine] = :common_mark
+ context[:markdown_engine] = Banzai::Filter::MarkdownFilter::DEFAULT_ENGINE
if Feature.enabled?(:personal_snippet_reference_filters, context[:author])
context[:user] = self.parent_user
diff --git a/app/models/concerns/cached_commit.rb b/app/models/concerns/cached_commit.rb
index 183d5728743..0fb72552dd5 100644
--- a/app/models/concerns/cached_commit.rb
+++ b/app/models/concerns/cached_commit.rb
@@ -4,8 +4,8 @@ module CachedCommit
extend ActiveSupport::Concern
def to_hash
- Gitlab::Git::Commit::SERIALIZE_KEYS.each_with_object({}) do |key, hash|
- hash[key] = public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ Gitlab::Git::Commit::SERIALIZE_KEYS.index_with do |key|
+ public_send(key) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/models/concerns/ci/partitionable.rb b/app/models/concerns/ci/partitionable.rb
index 68a6714c892..d6ba0f4488f 100644
--- a/app/models/concerns/ci/partitionable.rb
+++ b/app/models/concerns/ci/partitionable.rb
@@ -25,10 +25,21 @@ module Ci
PARTITIONABLE_MODELS = %w[
CommitStatus
Ci::BuildMetadata
- Ci::Stage
+ Ci::BuildNeed
+ Ci::BuildReportResult
+ Ci::BuildRunnerSession
+ Ci::BuildTraceChunk
+ Ci::BuildTraceMetadata
+ Ci::BuildPendingState
Ci::JobArtifact
- Ci::PipelineVariable
+ Ci::JobVariable
Ci::Pipeline
+ Ci::PendingBuild
+ Ci::RunningBuild
+ Ci::PipelineVariable
+ Ci::Sources::Pipeline
+ Ci::Stage
+ Ci::UnitTestFailure
].freeze
def self.check_inclusion(klass)
@@ -57,14 +68,31 @@ module Ci
end
class_methods do
- def partitionable(scope:, through: nil)
- if through
- define_singleton_method(:routing_table_name) { through[:table] }
- define_singleton_method(:routing_table_name_flag) { through[:flag] }
+ def partitionable(scope:, through: nil, partitioned: false)
+ handle_partitionable_through(through)
+ handle_partitionable_dml(partitioned)
+ handle_partitionable_scope(scope)
+ end
- include Partitionable::Switch
- end
+ private
+
+ def handle_partitionable_through(options)
+ return unless options
+
+ define_singleton_method(:routing_table_name) { options[:table] }
+ define_singleton_method(:routing_table_name_flag) { options[:flag] }
+
+ include Partitionable::Switch
+ end
+
+ def handle_partitionable_dml(partitioned)
+ define_singleton_method(:partitioned?) { partitioned }
+ return unless partitioned
+
+ include Partitionable::PartitionedFilter
+ end
+ def handle_partitionable_scope(scope)
define_method(:partition_scope_value) do
strong_memoize(:partition_scope_value) do
next Ci::Pipeline.current_partition_value if respond_to?(:importing?) && importing?
diff --git a/app/models/concerns/ci/partitionable/partitioned_filter.rb b/app/models/concerns/ci/partitionable/partitioned_filter.rb
new file mode 100644
index 00000000000..4adae3be26a
--- /dev/null
+++ b/app/models/concerns/ci/partitionable/partitioned_filter.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Ci
+ module Partitionable
+ # Used to patch the save, update, delete, destroy methods to use the
+ # partition_id attributes for their SQL queries.
+ module PartitionedFilter
+ extend ActiveSupport::Concern
+
+ if Rails::VERSION::MAJOR >= 7
+ # These methods are updated in Rails 7 to use `_primary_key_constraints_hash`
+ # by default, so this patch will no longer be required.
+ #
+ # rubocop:disable Gitlab/NoCodeCoverageComment
+ # :nocov:
+ raise "`#{__FILE__}` should be double checked" if Rails.env.test?
+
+ warn "Update `#{__FILE__}`. Patches Rails internals for partitioning"
+ # :nocov:
+ # rubocop:enable Gitlab/NoCodeCoverageComment
+ else
+ def _update_row(attribute_names, attempted_action = "update")
+ self.class._update_record(
+ attributes_with_values(attribute_names),
+ _primary_key_constraints_hash
+ )
+ end
+
+ def _delete_row
+ self.class._delete_record(_primary_key_constraints_hash)
+ end
+ end
+
+ # Introduced in Rails 7, but updated to include `partition_id` filter.
+ # https://github.com/rails/rails/blob/a4dbb153fd390ac31bb9808809e7ac4d3a2c5116/activerecord/lib/active_record/persistence.rb#L1031-L1033
+ def _primary_key_constraints_hash
+ { @primary_key => id_in_database, partition_id: partition_id } # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/commit_signature.rb b/app/models/concerns/commit_signature.rb
index 5bdfa9a2966..7f1fbbefd94 100644
--- a/app/models/concerns/commit_signature.rb
+++ b/app/models/concerns/commit_signature.rb
@@ -44,7 +44,7 @@ module CommitSignature
project.commit(commit_sha)
end
- def user
- commit.committer
+ def signed_by_user
+ raise NoMethodError, 'must implement `signed_by_user` method'
end
end
diff --git a/app/models/concerns/counter_attribute.rb b/app/models/concerns/counter_attribute.rb
index 03e062a9855..f1efbba67e1 100644
--- a/app/models/concerns/counter_attribute.rb
+++ b/app/models/concerns/counter_attribute.rb
@@ -17,14 +17,29 @@
# counter_attribute :storage_size
# end
#
+# It's possible to define a conditional counter attribute. You need to pass a proc
+# that must accept a single argument, the object instance on which this concern is
+# included.
+#
+# @example:
+#
+# class ProjectStatistics
+# include CounterAttribute
+#
+# counter_attribute :conditional_one, if: -> { |object| object.use_counter_attribute? }
+# end
+#
# To increment the counter we can use the method:
-# delayed_increment_counter(:commit_count, 3)
+# increment_counter(:commit_count, 3)
+#
+# This method would determine whether it would increment the counter using Redis,
+# or fallback to legacy increment on ActiveRecord counters.
#
# It is possible to register callbacks to be executed after increments have
# been flushed to the database. Callbacks are not executed if there are no increments
# to flush.
#
-# counter_attribute_after_flush do |statistic|
+# counter_attribute_after_commit do |statistic|
# Namespaces::ScheduleAggregationWorker.perform_async(statistic.namespace_id)
# end
#
@@ -32,99 +47,51 @@ module CounterAttribute
extend ActiveSupport::Concern
extend AfterCommitQueue
include Gitlab::ExclusiveLeaseHelpers
-
- LUA_STEAL_INCREMENT_SCRIPT = <<~EOS
- local increment_key, flushed_key = KEYS[1], KEYS[2]
- local increment_value = redis.call("get", increment_key) or 0
- local flushed_value = redis.call("incrby", flushed_key, increment_value)
- if flushed_value == 0 then
- redis.call("del", increment_key, flushed_key)
- else
- redis.call("del", increment_key)
- end
- return flushed_value
- EOS
-
- WORKER_DELAY = 10.minutes
- WORKER_LOCK_TTL = 10.minutes
+ include Gitlab::Utils::StrongMemoize
class_methods do
- def counter_attribute(attribute)
- counter_attributes << attribute
+ def counter_attribute(attribute, if: nil)
+ counter_attributes << {
+ attribute: attribute,
+ if_proc: binding.local_variable_get(:if) # can't read `if` directly
+ }
end
def counter_attributes
- @counter_attributes ||= Set.new
+ @counter_attributes ||= []
end
- def after_flush_callbacks
- @after_flush_callbacks ||= []
+ def after_commit_callbacks
+ @after_commit_callbacks ||= []
end
- # perform registered callbacks after increments have been flushed to the database
- def counter_attribute_after_flush(&callback)
- after_flush_callbacks << callback
- end
-
- def counter_attribute_enabled?(attribute)
- counter_attributes.include?(attribute)
+ # perform registered callbacks after increments have been committed to the database
+ def counter_attribute_after_commit(&callback)
+ after_commit_callbacks << callback
end
end
- # This method must only be called by FlushCounterIncrementsWorker
- # because it should run asynchronously and with exclusive lease.
- # This will
- # 1. temporarily move the pending increment for a given attribute
- # to a relative "flushed" Redis key, delete the increment key and return
- # the value. If new increments are performed at this point, the increment
- # key is recreated as part of `delayed_increment_counter`.
- # The "flushed" key is used to ensure that we can keep incrementing
- # counters in Redis while flushing existing values.
- # 2. then the value is used to update the counter in the database.
- # 3. finally the "flushed" key is deleted.
- def flush_increments_to_database!(attribute)
- lock_key = counter_lock_key(attribute)
-
- with_exclusive_lease(lock_key) do
- previous_db_value = read_attribute(attribute)
- increment_key = counter_key(attribute)
- flushed_key = counter_flushed_key(attribute)
- increment_value = steal_increments(increment_key, flushed_key)
- new_db_value = nil
-
- next if increment_value == 0
-
- transaction do
- update_counters_with_lease({ attribute => increment_value })
- redis_state { |redis| redis.del(flushed_key) }
- new_db_value = reset.read_attribute(attribute)
- end
+ def counter_attribute_enabled?(attribute)
+ counter_attribute = self.class.counter_attributes.find { |registered| registered[:attribute] == attribute }
+ return false unless counter_attribute
+ return true unless counter_attribute[:if_proc]
- execute_after_flush_callbacks
+ counter_attribute[:if_proc].call(self)
+ end
- log_flush_counter(attribute, increment_value, previous_db_value, new_db_value)
+ def counter(attribute)
+ strong_memoize_with(:counter, attribute) do
+ # This needs #to_sym because attribute could come from a Sidekiq param,
+ # which would be a string.
+ build_counter_for(attribute.to_sym)
end
end
- def delayed_increment_counter(attribute, increment)
- raise ArgumentError, "#{attribute} is not a counter attribute" unless counter_attribute_enabled?(attribute)
-
+ def increment_counter(attribute, increment)
return if increment == 0
run_after_commit_or_now do
- increment_counter(attribute, increment)
-
- FlushCounterIncrementsWorker.perform_in(WORKER_DELAY, self.class.name, self.id, attribute)
- end
-
- true
- end
-
- def increment_counter(attribute, increment)
- if counter_attribute_enabled?(attribute)
- new_value = redis_state do |redis|
- redis.incrby(counter_key(attribute), increment)
- end
+ new_value = counter(attribute).increment(increment)
log_increment_counter(attribute, increment, new_value)
end
@@ -137,74 +104,33 @@ module CounterAttribute
end
def reset_counter!(attribute)
- if counter_attribute_enabled?(attribute)
- detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
- update!(attribute => 0)
- clear_counter!(attribute)
- end
-
- log_clear_counter(attribute)
+ detect_race_on_record(log_fields: { caller: __method__, attributes: attribute }) do
+ counter(attribute).reset!
end
- end
- def get_counter_value(attribute)
- if counter_attribute_enabled?(attribute)
- redis_state do |redis|
- redis.get(counter_key(attribute)).to_i
- end
- end
+ log_clear_counter(attribute)
end
- def counter_key(attribute)
- "project:{#{project_id}}:counters:#{self.class}:#{id}:#{attribute}"
- end
-
- def counter_flushed_key(attribute)
- counter_key(attribute) + ':flushed'
- end
-
- def counter_lock_key(attribute)
- counter_key(attribute) + ':lock'
- end
-
- def counter_attribute_enabled?(attribute)
- self.class.counter_attribute_enabled?(attribute)
+ def execute_after_commit_callbacks
+ self.class.after_commit_callbacks.each do |callback|
+ callback.call(self.reset)
+ end
end
private
- def database_lock_key
- "project:{#{project_id}}:#{self.class}:#{id}"
- end
-
- def steal_increments(increment_key, flushed_key)
- redis_state do |redis|
- redis.eval(LUA_STEAL_INCREMENT_SCRIPT, keys: [increment_key, flushed_key])
- end
- end
+ def build_counter_for(attribute)
+ raise ArgumentError, %(attribute "#{attribute}" does not exist) unless has_attribute?(attribute)
- def clear_counter!(attribute)
- redis_state do |redis|
- redis.del(counter_key(attribute))
- end
- end
-
- def execute_after_flush_callbacks
- self.class.after_flush_callbacks.each do |callback|
- callback.call(self)
+ if counter_attribute_enabled?(attribute)
+ Gitlab::Counters::BufferedCounter.new(self, attribute)
+ else
+ Gitlab::Counters::LegacyCounter.new(self, attribute)
end
end
- def redis_state(&block)
- Gitlab::Redis::SharedState.with(&block)
- end
-
- def with_exclusive_lease(lock_key)
- in_lock(lock_key, ttl: WORKER_LOCK_TTL) do
- yield
- end
- rescue Gitlab::ExclusiveLeaseHelpers::FailedToObtainLockError
- # a worker is already updating the counters
+ def database_lock_key
+ "project:{#{project_id}}:#{self.class}:#{id}"
end
# detect_race_on_record uses a lease to monitor access
@@ -258,19 +184,6 @@ module CounterAttribute
Gitlab::AppLogger.info(payload)
end
- def log_flush_counter(attribute, increment, previous_db_value, new_db_value)
- payload = Gitlab::ApplicationContext.current.merge(
- message: 'Flush counter attribute to database',
- attribute: attribute,
- project_id: project_id,
- increment: increment,
- previous_db_value: previous_db_value,
- new_db_value: new_db_value
- )
-
- Gitlab::AppLogger.info(payload)
- end
-
def log_clear_counter(attribute)
payload = Gitlab::ApplicationContext.current.merge(
message: 'Clear counter attribute',
diff --git a/app/models/concerns/has_user_type.rb b/app/models/concerns/has_user_type.rb
index ad070090dd5..1af655277b8 100644
--- a/app/models/concerns/has_user_type.rb
+++ b/app/models/concerns/has_user_type.rb
@@ -13,10 +13,11 @@ module HasUserType
project_bot: 6,
migration_bot: 7,
security_bot: 8,
- automation_bot: 9
+ automation_bot: 9,
+ admin_bot: 11
}.with_indifferent_access.freeze
- BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot].freeze
+ BOT_USER_TYPES = %w[alert_bot project_bot support_bot visual_review_bot migration_bot security_bot automation_bot admin_bot].freeze
NON_INTERNAL_USER_TYPES = %w[human project_bot service_user].freeze
INTERNAL_USER_TYPES = (USER_TYPES.keys - NON_INTERNAL_USER_TYPES).freeze
@@ -24,7 +25,6 @@ module HasUserType
scope :humans, -> { where(user_type: :human) }
scope :bots, -> { where(user_type: BOT_USER_TYPES) }
scope :without_bots, -> { humans.or(where.not(user_type: BOT_USER_TYPES)) }
- scope :bots_without_project_bot, -> { where(user_type: BOT_USER_TYPES - ['project_bot']) }
scope :non_internal, -> { humans.or(where(user_type: NON_INTERNAL_USER_TYPES)) }
scope :without_ghosts, -> { humans.or(where.not(user_type: :ghost)) }
scope :without_project_bot, -> { humans.or(where.not(user_type: :project_bot)) }
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 31b2a8d7cc1..9f0cd96a8f8 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -366,7 +366,7 @@ module Issuable
select(issuable_columns)
.select(extra_select_columns)
- .from("#{table_name}")
+ .from(table_name.to_s)
.joins("JOIN LATERAL(#{highest_priority}) as highest_priorities ON TRUE")
.group(group_columns)
.reorder(highest_priority_arel_with_direction.nulls_last)
diff --git a/app/models/concerns/milestoneable.rb b/app/models/concerns/milestoneable.rb
index a95bed7ad42..e95a8a42aa6 100644
--- a/app/models/concerns/milestoneable.rb
+++ b/app/models/concerns/milestoneable.rb
@@ -9,6 +9,12 @@
module Milestoneable
extend ActiveSupport::Concern
+ class_methods do
+ def milestone_releases_subquery
+ Milestone.joins(:releases).where("#{table_name}.milestone_id = milestones.id")
+ end
+ end
+
included do
belongs_to :milestone
@@ -17,9 +23,15 @@ module Milestoneable
scope :any_milestone, -> { where.not(milestone_id: nil) }
scope :with_milestone, ->(title) { left_joins_milestones.where(milestones: { title: title }) }
scope :without_particular_milestones, ->(titles) { left_outer_joins(:milestone).where("milestones.title NOT IN (?) OR milestone_id IS NULL", titles) }
- scope :any_release, -> { joins_milestone_releases }
- scope :with_release, -> (tag, project_id) { joins_milestone_releases.where(milestones: { releases: { tag: tag, project_id: project_id } }) }
- scope :without_particular_release, -> (tag, project_id) { joins_milestone_releases.where.not(milestones: { releases: { tag: tag, project_id: project_id } }) }
+ scope :any_release, -> do
+ where("EXISTS (?)", milestone_releases_subquery)
+ end
+ scope :with_release, -> (tag, project_id) do
+ where("EXISTS (?)", milestone_releases_subquery.where(releases: { tag: tag, project_id: project_id }))
+ end
+ scope :without_particular_release, -> (tag, project_id) do
+ where("EXISTS (?)", milestone_releases_subquery.where.not(releases: { tag: tag, project_id: project_id }))
+ end
scope :left_joins_milestones, -> { joins("LEFT OUTER JOIN milestones ON #{table_name}.milestone_id = milestones.id") }
scope :order_milestone_due_desc, -> { left_joins_milestones.reorder(Arel.sql('milestones.due_date IS NULL, milestones.id IS NULL, milestones.due_date DESC')) }
@@ -30,11 +42,6 @@ module Milestoneable
.where(milestone_releases: { release_id: nil })
end
- scope :joins_milestone_releases, -> do
- joins("JOIN milestone_releases ON #{table_name}.milestone_id = milestone_releases.milestone_id
- JOIN releases ON milestone_releases.release_id = releases.id").distinct
- end
-
private
def milestone_is_valid
diff --git a/app/models/concerns/sensitive_serializable_hash.rb b/app/models/concerns/sensitive_serializable_hash.rb
index 4ad8d16fcb9..794748483e4 100644
--- a/app/models/concerns/sensitive_serializable_hash.rb
+++ b/app/models/concerns/sensitive_serializable_hash.rb
@@ -19,8 +19,6 @@ module SensitiveSerializableHash
# In general, prefer NOT to use serializable_hash / to_json / as_json in favor
# of serializers / entities instead which has an allowlist of attributes
def serializable_hash(options = nil)
- return super if options && options[:unsafe_serialization_hash]
-
options = options.try(:dup) || {}
options[:except] = Array(options[:except]).dup
diff --git a/app/models/concerns/signature_type.rb b/app/models/concerns/signature_type.rb
new file mode 100644
index 00000000000..804f42b6f72
--- /dev/null
+++ b/app/models/concerns/signature_type.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module SignatureType
+ TYPES = %i[gpg ssh x509].freeze
+
+ def type
+ raise NoMethodError, 'must implement `type` method'
+ end
+
+ TYPES.each do |type|
+ define_method("#{type}?") { self.type == type }
+ end
+end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index eccb004b503..6532a18d1b8 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -72,7 +72,7 @@ module Sortable
private
- def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
+ def highest_label_priority(target_column:, project_column:, target_type_column: nil, target_type: nil, excluded_labels: [])
query = Label.select(LabelPriority.arel_table[:priority].minimum.as('label_priority'))
.left_join_priorities
.joins(:label_links)
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index ee5774d4868..05addcf83d2 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -63,14 +63,15 @@ module Taskable
def task_status(short: false)
return '' if description.blank?
- prep, completed = if short
- ['/', '']
- else
- [' of ', ' completed']
- end
-
sum = tasks.summary
- "#{sum.complete_count}#{prep}#{sum.item_count} #{'checklist item'.pluralize(sum.item_count)}#{completed}"
+ checklist_item_noun = n_('checklist item', 'checklist items', sum.item_count)
+ if short
+ format(s_('Tasks|%{complete_count}/%{total_count} %{checklist_item_noun}'),
+checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ else
+ format(s_('Tasks|%{complete_count} of %{total_count} %{checklist_item_noun} completed'),
+checklist_item_noun: checklist_item_noun, complete_count: sum.complete_count, total_count: sum.item_count)
+ end
end
# Return a short string that describes the current state of this Taskable's
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 54fe9eac2bc..2b7447dc700 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -15,12 +15,13 @@ module TimeTrackable
alias_method :time_spent?, :time_spent
- default_value_for :time_estimate, value: 0, allows_nil: false
+ attribute :time_estimate, default: 0
validates :time_estimate, numericality: { message: 'has an invalid format' }, allow_nil: false
validate :check_negative_time_spent
has_many :timelogs, dependent: :destroy, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ after_initialize :set_time_estimate_default_value
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -67,6 +68,13 @@ module TimeTrackable
val.is_a?(Integer) ? super([val, Gitlab::Database::MAX_INT_VALUE].min) : super(val)
end
+ def set_time_estimate_default_value
+ return if new_record?
+ return unless has_attribute?(:time_estimate)
+
+ self.time_estimate ||= self.class.column_defaults['time_estimate']
+ end
+
private
def reset_spent_time
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index 7da4e31b472..db0fcd915b3 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -98,6 +98,8 @@ class ContainerRepository < ApplicationRecord
)
end
+ before_update :set_status_updated_at_to_now, if: :status_changed?
+
state_machine :migration_state, initial: :default, use_transactions: false do
state :pre_importing do
validates :migration_pre_import_started_at, presence: true
@@ -521,11 +523,20 @@ class ContainerRepository < ApplicationRecord
end
def set_delete_ongoing_status
- update_columns(status: :delete_ongoing, delete_started_at: Time.zone.now)
+ now = Time.zone.now
+ update_columns(
+ status: :delete_ongoing,
+ delete_started_at: now,
+ status_updated_at: now
+ )
end
def set_delete_scheduled_status
- update_columns(status: :delete_scheduled, delete_started_at: nil)
+ update_columns(
+ status: :delete_scheduled,
+ delete_started_at: nil,
+ status_updated_at: Time.zone.now
+ )
end
def migration_in_active_state?
@@ -623,6 +634,10 @@ class ContainerRepository < ApplicationRecord
tag
end
end
+
+ def set_status_updated_at_to_now
+ self.status_updated_at = Time.zone.now
+ end
end
ContainerRepository.prepend_mod_with('ContainerRepository')
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index 5eda9b4bf15..91656d4f846 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -85,8 +85,8 @@ class CustomerRelations::Organization < ApplicationRecord
private
def self.default_state_counts
- states.keys.each_with_object({}) do |key, memo|
- memo[key] = 0
+ states.keys.index_with do |key|
+ 0
end
end
diff --git a/app/models/dependency_proxy/group_setting.rb b/app/models/dependency_proxy/group_setting.rb
index 3a7ae66a263..b39ea36644a 100644
--- a/app/models/dependency_proxy/group_setting.rb
+++ b/app/models/dependency_proxy/group_setting.rb
@@ -3,7 +3,5 @@
class DependencyProxy::GroupSetting < ApplicationRecord
belongs_to :group
- attribute :enabled, default: true
-
validates :group, presence: true
end
diff --git a/app/models/deploy_token.rb b/app/models/deploy_token.rb
index 66d1ce01814..498ca9c4f30 100644
--- a/app/models/deploy_token.rb
+++ b/app/models/deploy_token.rb
@@ -37,6 +37,7 @@ class DeployToken < ApplicationRecord
message: "can contain only letters, digits, '_', '-', '+', and '.'"
}
+ validates :expires_at, iso8601_date: true, on: :create
validates :deploy_token_type, presence: true
enum deploy_token_type: {
group_type: 1,
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index ea92b978d3a..1254ce1c90a 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -363,6 +363,10 @@ class Deployment < ApplicationRecord
deployable&.user || user
end
+ def triggered_by?(user)
+ deployed_by == user
+ end
+
def link_merge_requests(relation)
# NOTE: relation.select will perform column deduplication,
# when id == environment_id it will outputs 2 columns instead of 3
@@ -441,9 +445,10 @@ class Deployment < ApplicationRecord
# default tag limit is 100, 0 means no limit
# when refs_by_oid is passed an SHA, returns refs for that commit
def tags(limit: 100)
- project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
+ strong_memoize_with(:tag, limit) do
+ project.repository.refs_by_oid(oid: sha, limit: limit, ref_patterns: [Gitlab::Git::TAG_REF_PREFIX]) || []
+ end
end
- strong_memoize_attr :tags
private
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 2d3f342953f..f1edfb3a34b 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -6,6 +6,7 @@ class Environment < ApplicationRecord
include FastDestroyAll::Helpers
include Presentable
include NullifyIfBlank
+ include FromUnion
self.reactive_cache_refresh_interval = 1.minute
self.reactive_cache_lifetime = 55.seconds
@@ -27,27 +28,29 @@ class Environment < ApplicationRecord
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.
- has_one :last_deployment, -> { success.ordered }, class_name: 'Deployment', inverse_of: :environment
- has_one :last_visible_deployment, -> { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
+ # NOTE:
+ # 1) no-op arguments is to prevent accidental legacy preloading. See: https://gitlab.com/gitlab-org/gitlab/-/issues/369240
+ # 2) If you preload multiple last deployments of environments, use Preloaders::Environments::DeploymentPreloader.
+ has_one :last_deployment, -> (_env) { success.ordered }, class_name: 'Deployment', inverse_of: :environment
+ has_one :last_visible_deployment, -> (_env) { visible.order(id: :desc) }, inverse_of: :environment, class_name: 'Deployment'
+ has_one :upcoming_deployment, -> (_env) { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
Deployment::FINISHED_STATUSES.each do |status|
- has_one :"last_#{status}_deployment", -> { where(status: status).ordered },
+ has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered },
class_name: 'Deployment', inverse_of: :environment
end
Deployment::UPCOMING_STATUSES.each do |status|
- has_one :"last_#{status}_deployment", -> { where(status: status).ordered_as_upcoming },
+ has_one :"last_#{status}_deployment", -> (_env) { where(status: status).ordered_as_upcoming },
class_name: 'Deployment', inverse_of: :environment
end
- has_one :upcoming_deployment, -> { upcoming.order(id: :desc) }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
before_validation :generate_slug, if: ->(env) { env.slug.blank? }
+ before_validation :ensure_environment_tier
before_save :set_environment_type
- before_save :ensure_environment_tier
after_save :clear_reactive_cache!
validates :name,
@@ -68,6 +71,10 @@ class Environment < ApplicationRecord
length: { maximum: 255 },
allow_nil: true
+ # Currently, the tier presence is validaed for newly created environments.
+ # After the `BackfillEnvironmentTiers` background migration has been completed, we should remove `on: :create`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/385253.
+ validates :tier, presence: true, on: :create
validate :safe_external_url
validate :merge_request_not_changed
@@ -87,7 +94,6 @@ class Environment < ApplicationRecord
scope :in_review_folder, -> { where(environment_type: "review") }
scope :for_name, -> (name) { where(name: name) }
- scope :preload_cluster, -> { preload(last_deployment: :cluster) }
scope :preload_project, -> { preload(:project) }
scope :auto_stoppable, -> (limit) { available.where('auto_stop_at < ?', Time.zone.now).limit(limit) }
scope :auto_deletable, -> (limit) { stopped.where('auto_delete_at < ?', Time.zone.now).limit(limit) }
@@ -96,7 +102,16 @@ class Environment < ApplicationRecord
# Search environments which have names like the given query.
# Do not set a large limit unless you've confirmed that it works on gitlab.com scale.
scope :for_name_like, -> (query, limit: 5) do
- where('LOWER(environments.name) LIKE LOWER(?) || \'%\'', sanitize_sql_like(query)).limit(limit)
+ top_level = 'LOWER(environments.name) LIKE LOWER(?) || \'%\''
+
+ where(top_level, sanitize_sql_like(query)).limit(limit)
+ end
+
+ scope :for_name_like_within_folder, -> (query, limit: 5) do
+ within_folder = 'LOWER(ltrim(environments.name, environments.environment_type'\
+ ' || \'/\')) LIKE LOWER(?) || \'%\''
+
+ where(within_folder, sanitize_sql_like(query)).limit(limit)
end
scope :for_project, -> (project) { where(project_id: project) }
@@ -106,7 +121,6 @@ class Environment < ApplicationRecord
scope :with_rank, -> do
select('environments.*, rank() OVER (PARTITION BY project_id ORDER BY id DESC)')
end
- scope :for_id, -> (id) { where(id: id) }
scope :with_deployment, -> (sha, status: nil) do
deployments = Deployment.select(1).where('deployments.environment_id = environments.id').where(sha: sha)
@@ -197,12 +211,19 @@ class Environment < ApplicationRecord
update_all(auto_delete_at: at_time)
end
+ def self.nested
+ group('COALESCE(environment_type, id::text)', 'COALESCE(environment_type, name)')
+ .select('COALESCE(environment_type, id::text), COALESCE(environment_type, name) AS name',
+ 'COUNT(*) AS size', 'MAX(id) AS last_id')
+ .order('name ASC')
+ end
+
class << self
def count_by_state
environments_count_by_state = group(:state).count
- valid_states.each_with_object({}) do |state, count_hash|
- count_hash[state] = environments_count_by_state[state.to_s] || 0
+ valid_states.index_with do |state|
+ environments_count_by_state[state.to_s] || 0
end
end
end
@@ -490,6 +511,12 @@ class Environment < ApplicationRecord
environment_type.nil?
end
+ def deploy_freezes
+ Gitlab::SafeRequestStore.fetch("project:#{project_id}:freeze_periods_for_environments") do
+ project.freeze_periods
+ end
+ end
+
private
# We deliberately avoid using AddressableUrlValidator to allow users to update their environments even if they have
diff --git a/app/models/event.rb b/app/models/event.rb
index a1417db3410..ed65b367b8a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -132,7 +132,7 @@ class Event < ApplicationRecord
where(
'action IN (?) OR (target_type IN (?) AND action IN (?))',
[actions[:pushed], actions[:commented]],
- %w(MergeRequest Issue), [actions[:created], actions[:closed], actions[:merged]]
+ %w(MergeRequest Issue WorkItem), [actions[:created], actions[:closed], actions[:merged]]
)
end
@@ -380,13 +380,11 @@ class Event < ApplicationRecord
protected
def capability
- @capability ||= begin
- capabilities.flat_map do |ability, syms|
- if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
- [ability]
- else
- []
- end
+ @capability ||= capabilities.flat_map do |ability, syms|
+ if syms.any? { |sym| send(sym) } # rubocop: disable GitlabSecurity/PublicSend
+ [ability]
+ else
+ []
end
end
end
diff --git a/app/models/generic_commit_status.rb b/app/models/generic_commit_status.rb
index 6c8bfc35334..b02074849a1 100644
--- a/app/models/generic_commit_status.rb
+++ b/app/models/generic_commit_status.rb
@@ -3,8 +3,6 @@
class GenericCommitStatus < CommitStatus
EXTERNAL_STAGE_IDX = 1_000_000
- before_validation :set_default_values
-
validates :target_url, addressable_url: true,
length: { maximum: 255 },
allow_nil: true
@@ -13,12 +11,6 @@ class GenericCommitStatus < CommitStatus
# GitHub compatible API
alias_attribute :context, :name
- def set_default_values
- self.context ||= 'default'
- self.stage ||= 'external'
- self.stage_idx ||= EXTERNAL_STAGE_IDX
- end
-
def tags
[:external]
end
diff --git a/app/models/gpg_key.rb b/app/models/gpg_key.rb
index 2db074e733e..1bf35179393 100644
--- a/app/models/gpg_key.rb
+++ b/app/models/gpg_key.rb
@@ -40,8 +40,8 @@ class GpgKey < ApplicationRecord
unless: -> { errors.has_key?(:key) }
before_validation :extract_fingerprint, :extract_primary_keyid
- after_commit :update_invalid_gpg_signatures, on: :create
after_create :generate_subkeys
+ after_commit :update_invalid_gpg_signatures, on: :create
def primary_keyid
super&.upcase
diff --git a/app/models/group.rb b/app/models/group.rb
index 098116ed800..0cdd7dd8596 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -20,6 +20,7 @@ class Group < Namespace
include BulkUsersByEmailLoad
include ChronicDurationAttribute
include RunnerTokenExpirationInterval
+ include Todoable
extend ::Gitlab::Utils::Override
@@ -119,7 +120,7 @@ class Group < Namespace
has_many :group_callouts, class_name: 'Users::GroupCallout', foreign_key: :group_id
- has_many :protected_branches, inverse_of: :group
+ has_many :protected_branches, inverse_of: :group, foreign_key: :namespace_id
has_one :group_feature, inverse_of: :group, class_name: 'Groups::FeatureSetting'
@@ -154,10 +155,10 @@ class Group < Namespace
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
after_create :post_create_hook
+ after_create -> { create_or_load_association(:group_feature) }
+ after_update :path_changed_hook, if: :saved_change_to_path?
after_destroy :post_destroy_hook
after_commit :update_two_factor_requirement
- after_update :path_changed_hook, if: :saved_change_to_path?
- after_create -> { create_or_load_association(:group_feature) }
scope :with_users, -> { includes(:users) }
@@ -165,7 +166,16 @@ class Group < Namespace
scope :by_id, ->(groups) { where(id: groups) }
- scope :by_ids_or_paths, -> (ids, paths) { by_id(ids).or(where(path: paths)) }
+ scope :by_ids_or_paths, -> (ids, paths) do
+ return by_id(ids) unless paths.present?
+
+ ids_by_full_path = Route
+ .for_routable_type(Namespace.name)
+ .where('LOWER(routes.path) IN (?)', paths.map(&:downcase))
+ .select(:namespace_id)
+
+ Group.from_union([by_id(ids), by_id(ids_by_full_path), where('LOWER(path) IN (?)', paths.map(&:downcase))])
+ end
scope :for_authorized_group_members, -> (user_ids) do
joins(:group_members)
@@ -550,6 +560,11 @@ class Group < Namespace
members_with_parents.pluck(Arel.sql('DISTINCT members.user_id'))
end
+ def self_and_hierarchy_intersecting_with_user_groups(user)
+ user_groups = GroupsFinder.new(user).execute.unscope(:order)
+ self_and_hierarchy.unscope(:order).where(id: user_groups)
+ end
+
def self_and_ancestors_ids
strong_memoize(:self_and_ancestors_ids) do
self_and_ancestors.pluck(:id)
@@ -831,6 +846,7 @@ class Group < Namespace
def has_project_with_service_desk_enabled?
Gitlab::ServiceDesk.supported? && all_projects.service_desk_enabled.exists?
end
+ strong_memoize_attr :has_project_with_service_desk_enabled?, :has_project_with_service_desk_enabled
def activity_path
Gitlab::Routing.url_helpers.activity_group_path(self)
@@ -887,6 +903,10 @@ class Group < Namespace
feature_flag_enabled_for_self_or_ancestor?(:work_items)
end
+ def work_items_mvc_feature_flag_enabled?
+ feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc)
+ end
+
def work_items_mvc_2_feature_flag_enabled?
feature_flag_enabled_for_self_or_ancestor?(:work_items_mvc_2)
end
diff --git a/app/models/group_deploy_key.rb b/app/models/group_deploy_key.rb
index c65b00a6de0..9495df7ab6d 100644
--- a/app/models/group_deploy_key.rb
+++ b/app/models/group_deploy_key.rb
@@ -12,6 +12,11 @@ class GroupDeployKey < Key
joins(:group_deploy_keys_groups).where(group_deploy_keys_groups: { group_id: group_ids }).uniq
end
+ # Remove usage_type because it defined in Key class but doesn't have a column in group_deploy_keys table
+ def self.defined_enums
+ super.without('usage_type')
+ end
+
def type
'DeployKey'
end
diff --git a/app/models/hooks/active_hook_filter.rb b/app/models/hooks/active_hook_filter.rb
index cdcfd3f3ff5..4599ebf8717 100644
--- a/app/models/hooks/active_hook_filter.rb
+++ b/app/models/hooks/active_hook_filter.rb
@@ -18,10 +18,6 @@ class ActiveHookFilter
branch_name = Gitlab::Git.branch_name(data[:ref])
- if Feature.disabled?(:enhanced_webhook_support_regex)
- return RefMatcher.new(@hook.push_events_branch_filter).matches?(branch_name)
- end
-
case @hook.branch_filter_strategy
when 'all_branches'
true
diff --git a/app/models/hooks/service_hook.rb b/app/models/hooks/service_hook.rb
index 27119d3a95a..94ced96bbde 100644
--- a/app/models/hooks/service_hook.rb
+++ b/app/models/hooks/service_hook.rb
@@ -13,4 +13,9 @@ class ServiceHook < WebHook
override :parent
delegate :parent, to: :integration
+
+ override :executable?
+ def executable?
+ true
+ end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 946cdda2e75..189291a38ec 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -41,12 +41,9 @@ class WebHook < ApplicationRecord
after_initialize :initialize_url_variables
before_validation :reset_token
- before_validation :set_branch_filter_nil, \
- if: -> { branch_filter_strategy_all_branches? && enhanced_webhook_support_regex? }
- validates :push_events_branch_filter, \
- untrusted_regexp: true, if: -> { branch_filter_strategy_regex? && enhanced_webhook_support_regex? }
- validates :push_events_branch_filter, \
- "web_hooks/wildcard_branch_filter": true, if: -> { branch_filter_strategy_wildcard? }
+ before_validation :set_branch_filter_nil, if: :branch_filter_strategy_all_branches?
+ validates :push_events_branch_filter, untrusted_regexp: true, if: :branch_filter_strategy_regex?
+ validates :push_events_branch_filter, "web_hooks/wildcard_branch_filter": true, if: :branch_filter_strategy_wildcard?
validates :url_variables, json_schema: { filename: 'web_hooks_url_variables' }
validate :no_missing_url_variables
@@ -59,8 +56,6 @@ class WebHook < ApplicationRecord
}, _prefix: true
scope :executable, -> do
- next all unless Feature.enabled?(:web_hooks_disable_failed)
-
where('recent_failures <= ? AND (disabled_until IS NULL OR disabled_until < ?)', FAILURE_THRESHOLD, Time.current)
end
@@ -69,23 +64,17 @@ class WebHook < ApplicationRecord
where('recent_failures > ? OR disabled_until >= ?', FAILURE_THRESHOLD, Time.current)
end
- def self.web_hooks_disable_failed?(hook)
- Feature.enabled?(:web_hooks_disable_failed, hook.parent)
- end
-
def executable?
!temporarily_disabled? && !permanently_disabled?
end
def temporarily_disabled?
- return false unless web_hooks_disable_failed?
return false if recent_failures <= FAILURE_THRESHOLD
disabled_until.present? && disabled_until >= Time.current
end
def permanently_disabled?
- return false unless web_hooks_disable_failed?
return false if disabled_until.present?
recent_failures > FAILURE_THRESHOLD
@@ -197,7 +186,7 @@ class WebHook < ApplicationRecord
end
# See app/validators/json_schemas/web_hooks_url_variables.json
- VARIABLE_REFERENCE_RE = /\{([A-Za-z_][A-Za-z0-9_]+)\}/.freeze
+ VARIABLE_REFERENCE_RE = /\{([A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*)\}/.freeze
def interpolated_url
return url unless url.include?('{')
@@ -232,10 +221,6 @@ class WebHook < ApplicationRecord
backoff_count.succ.clamp(1, MAX_FAILURES)
end
- def web_hooks_disable_failed?
- self.class.web_hooks_disable_failed?(self)
- end
-
def initialize_url_variables
self.url_variables = {} if encrypted_url_variables.nil?
end
@@ -257,10 +242,6 @@ class WebHook < ApplicationRecord
errors.add(:url, "Invalid URL template. Missing keys: #{missing}")
end
- def enhanced_webhook_support_regex?
- Feature.enabled?(:enhanced_webhook_support_regex)
- end
-
def set_branch_filter_nil
self.push_events_branch_filter = nil
end
diff --git a/app/models/import_export_upload.rb b/app/models/import_export_upload.rb
index bc363cce8dd..bdb53653637 100644
--- a/app/models/import_export_upload.rb
+++ b/app/models/import_export_upload.rb
@@ -2,7 +2,6 @@
class ImportExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
belongs_to :project
belongs_to :group
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 41278dce22d..a630a6dee11 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -19,7 +19,7 @@ class Integration < ApplicationRecord
INTEGRATION_NAMES = %w[
asana assembla bamboo bugzilla buildkite campfire confluence custom_issue_tracker datadog discord
- drone_ci emails_on_push ewm external_wiki flowdock hangouts_chat harbor irker jira
+ drone_ci emails_on_push ewm external_wiki hangouts_chat harbor irker jira
mattermost mattermost_slash_commands microsoft_teams packagist pipelines_email
pivotaltracker prometheus pumble pushover redmine slack slack_slash_commands teamcity unify_circuit webex_teams youtrack zentao
].freeze
@@ -41,7 +41,9 @@ class Integration < ApplicationRecord
Integrations::BaseCi
Integrations::BaseIssueTracker
Integrations::BaseMonitoring
+ Integrations::BaseSlackNotification
Integrations::BaseSlashCommands
+ Integrations::BaseThirdPartyWiki
].freeze
SECTION_TYPE_CONFIGURATION = 'configuration'
diff --git a/app/models/integrations/asana.rb b/app/models/integrations/asana.rb
index 2cfd71c9eb2..b8cfd718007 100644
--- a/app/models/integrations/asana.rb
+++ b/app/models/integrations/asana.rb
@@ -42,10 +42,8 @@ module Integrations
end
def client
- @_client ||= begin
- ::Asana::Client.new do |c|
- c.authentication :access_token, api_key
- end
+ @_client ||= ::Asana::Client.new do |c|
+ c.authentication :access_token, api_key
end
end
diff --git a/app/models/integrations/bamboo.rb b/app/models/integrations/bamboo.rb
index b4e97f0871e..fc5e6a88c2d 100644
--- a/app/models/integrations/bamboo.rb
+++ b/app/models/integrations/bamboo.rb
@@ -16,7 +16,7 @@ module Integrations
help: -> { s_('BambooService|Bamboo build plan key.') },
non_empty_password_title: -> { s_('BambooService|Enter new build key') },
non_empty_password_help: -> { s_('BambooService|Leave blank to use your current build key.') },
- placeholder: -> { s_('KEY') },
+ placeholder: -> { _('KEY') },
required: true
field :username,
diff --git a/app/models/integrations/base_chat_notification.rb b/app/models/integrations/base_chat_notification.rb
index 750aa60b185..f2a707c2214 100644
--- a/app/models/integrations/base_chat_notification.rb
+++ b/app/models/integrations/base_chat_notification.rb
@@ -33,7 +33,10 @@ module Integrations
boolean_accessor :notify_only_broken_pipelines, :notify_only_default_branch
- validates :webhook, presence: true, public_url: true, if: :activated?
+ validates :webhook,
+ presence: true,
+ public_url: true,
+ if: -> (integration) { integration.activated? && integration.requires_webhook? }
validates :labels_to_be_notified_behavior, inclusion: { in: LABEL_NOTIFICATION_BEHAVIOURS }, allow_blank: true
def initialize_properties
@@ -73,8 +76,6 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', help: "#{webhook_help}", required: true }.freeze,
- { type: 'text', name: 'username', placeholder: 'GitLab-integration' }.freeze,
{ type: 'checkbox', name: 'notify_only_broken_pipelines', help: 'Do not send notifications for successful pipelines.' }.freeze,
{
type: 'select',
@@ -96,19 +97,24 @@ module Integrations
['Match all of the labels', MATCH_ALL_LABELS]
]
}.freeze
- ].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
end
def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- return unless webhook.present?
-
object_kind = data[:object_kind]
+ return false unless should_execute?(object_kind)
+
data = custom_data(data)
- return unless notify_label?(data)
+ return false unless notify_label?(data)
# WebHook events often have an 'update' event that follows a 'open' or
# 'close' action. Ignore update events for now to prevent duplicate
@@ -168,8 +174,17 @@ module Integrations
self.public_send(field_name) # rubocop:disable GitlabSecurity/PublicSend
end
+ def requires_webhook?
+ true
+ end
+
private
+ def should_execute?(object_kind)
+ supported_events.include?(object_kind) &&
+ (!requires_webhook? || webhook.present?)
+ end
+
def log_usage(_, _)
# Implement in child class
end
diff --git a/app/models/integrations/base_slack_notification.rb b/app/models/integrations/base_slack_notification.rb
index cb785afdcfe..7a2a91aa0d2 100644
--- a/app/models/integrations/base_slack_notification.rb
+++ b/app/models/integrations/base_slack_notification.rb
@@ -32,13 +32,15 @@ module Integrations
true
end
+ private
+
override :log_usage
def log_usage(event, user_id)
return unless user_id
return unless SUPPORTED_EVENTS_FOR_USAGE_LOG.include?(event)
- key = "i_ecosystem_slack_service_#{event}_notification"
+ key = "#{metrics_key_prefix}_#{event}_notification"
Gitlab::UsageDataCounters::HLLRedisCounter.track_event(key, values: user_id)
@@ -55,8 +57,13 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: User.find(user_id),
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
+
+ def metrics_key_prefix
+ raise NotImplementedError
+ end
end
end
diff --git a/app/models/integrations/base_slash_commands.rb b/app/models/integrations/base_slash_commands.rb
index 314f0a6ee5d..11ff7547325 100644
--- a/app/models/integrations/base_slash_commands.rb
+++ b/app/models/integrations/base_slash_commands.rb
@@ -60,7 +60,7 @@ module Integrations
# rubocop: disable CodeReuse/ServiceClass
def find_chat_user(params)
- ChatNames::FindUserService.new(self, params).execute
+ ChatNames::FindUserService.new(params[:team_id], params[:user_id]).execute
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/integrations/confluence.rb b/app/models/integrations/confluence.rb
index c1c43af99bf..31e9a171d1b 100644
--- a/app/models/integrations/confluence.rb
+++ b/app/models/integrations/confluence.rb
@@ -10,7 +10,7 @@ module Integrations
validate :validate_confluence_url_is_cloud, if: :activated?
field :confluence_url,
- title: -> { s_('Confluence Cloud Workspace URL') },
+ title: -> { _('Confluence Cloud Workspace URL') },
placeholder: 'https://example.atlassian.net/wiki',
required: true
diff --git a/app/models/integrations/datadog.rb b/app/models/integrations/datadog.rb
index 27bed5d3f76..80eecc14d0f 100644
--- a/app/models/integrations/datadog.rb
+++ b/app/models/integrations/datadog.rb
@@ -9,7 +9,7 @@ module Integrations
URL_API_KEYS_DOCS = "https://docs.#{DEFAULT_DOMAIN}/account_management/api-app-keys/"
SUPPORTED_EVENTS = %w[
- pipeline job archive_trace
+ pipeline build archive_trace
].freeze
TAG_KEY_VALUE_RE = %r{\A [\w-]+ : .*\S.* \z}x.freeze
@@ -48,8 +48,8 @@ module Integrations
field :archive_trace_events,
storage: :attribute,
type: 'checkbox',
- title: -> { s_('Logs') },
- checkbox_label: -> { s_('Enable logs collection') },
+ title: -> { _('Logs') },
+ checkbox_label: -> { _('Enable logs collection') },
help: -> { s_('When enabled, job logs are collected by Datadog and displayed along with pipeline execution traces.') }
field :datadog_service,
@@ -156,10 +156,10 @@ module Integrations
end
def execute(data)
+ return unless supported_events.include?(data[:object_kind])
+
object_kind = data[:object_kind]
object_kind = 'job' if object_kind == 'build'
- return unless supported_events.include?(object_kind)
-
data = hook_data(data, object_kind)
execute_web_hook!(data, "#{object_kind} hook")
end
diff --git a/app/models/integrations/flowdock.rb b/app/models/integrations/flowdock.rb
index 52efb29f2c1..d7625cfb3d2 100644
--- a/app/models/integrations/flowdock.rb
+++ b/app/models/integrations/flowdock.rb
@@ -1,28 +1,12 @@
# frozen_string_literal: true
+# This integration is scheduled for removal.
+# All records must be deleted before the class can be removed.
+# https://gitlab.com/gitlab-org/gitlab/-/issues/379197
module Integrations
class Flowdock < Integration
- validates :token, presence: true, if: :activated?
-
- field :token,
- type: 'password',
- help: -> { s_('FlowdockService|Enter your Flowdock token.') },
- non_empty_password_title: -> { s_('ProjectService|Enter new token') },
- non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
- placeholder: '1b609b52537...',
- required: true
-
- def title
- 'Flowdock'
- end
-
- def description
- s_('FlowdockService|Send event notifications from GitLab to Flowdock flows.')
- end
-
- def help
- docs_link = ActionController::Base.helpers.link_to _('Learn more.'), Rails.application.routes.url_helpers.help_page_url('api/services', anchor: 'flowdock'), target: '_blank', rel: 'noopener noreferrer'
- s_('FlowdockService|Send event notifications from GitLab to Flowdock flows. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
+ def readonly?
+ true
end
def self.to_param
@@ -30,22 +14,7 @@ module Integrations
end
def self.supported_events
- %w(push)
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- ::Flowdock::Git.post(
- data[:ref],
- data[:before],
- data[:after],
- token: token,
- repo: project.repository,
- repo_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}",
- commit_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/-/commit/%s",
- diff_url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/compare/%s...%s"
- )
+ %w[]
end
end
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 65492bfd9c2..45302a0bd09 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -132,11 +132,9 @@ module Integrations
end
def client
- @client ||= begin
- JIRA::Client.new(options).tap do |client|
- # Replaces JIRA default http client with our implementation
- client.request_client = Gitlab::Jira::HttpClient.new(client.options)
- end
+ @client ||= JIRA::Client.new(options).tap do |client|
+ # Replaces JIRA default http client with our implementation
+ client.request_client = Gitlab::Jira::HttpClient.new(client.options)
end
end
@@ -406,6 +404,7 @@ module Integrations
label: Integration::SNOWPLOW_EVENT_LABEL,
property: key,
user: user,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: key).to_context],
**optional_arguments
)
end
diff --git a/app/models/integrations/mattermost.rb b/app/models/integrations/mattermost.rb
index dd1c98ee06b..e3c5c22ad3a 100644
--- a/app/models/integrations/mattermost.rb
+++ b/app/models/integrations/mattermost.rb
@@ -5,7 +5,7 @@ module Integrations
include SlackMattermostNotifier
def title
- s_('Mattermost notifications')
+ _('Mattermost notifications')
end
def description
diff --git a/app/models/integrations/packagist.rb b/app/models/integrations/packagist.rb
index 7148de66aee..3973b492b6d 100644
--- a/app/models/integrations/packagist.rb
+++ b/app/models/integrations/packagist.rb
@@ -5,15 +5,15 @@ module Integrations
include HasWebHook
field :username,
- title: -> { s_('Username') },
- help: -> { s_('Enter your Packagist username.') },
+ title: -> { _('Username') },
+ help: -> { _('Enter your Packagist username.') },
placeholder: '',
required: true
field :token,
type: 'password',
- title: -> { s_('Token') },
- help: -> { s_('Enter your Packagist token.') },
+ title: -> { _('Token') },
+ help: -> { _('Enter your Packagist token.') },
non_empty_password_title: -> { s_('ProjectService|Enter new token') },
non_empty_password_help: -> { s_('ProjectService|Leave blank to use your current token.') },
placeholder: '',
diff --git a/app/models/integrations/pushover.rb b/app/models/integrations/pushover.rb
index 791e27c5db7..6bb6b6d60f6 100644
--- a/app/models/integrations/pushover.rb
+++ b/app/models/integrations/pushover.rb
@@ -112,7 +112,7 @@ module Integrations
user: user_key,
device: device,
priority: priority,
- title: "#{project.full_name}",
+ title: project.full_name.to_s,
message: message,
url: data[:project][:web_url],
url_title: s_("PushoverService|See project %{project_full_name}") % { project_full_name: project.full_name }
diff --git a/app/models/integrations/slack.rb b/app/models/integrations/slack.rb
index 89326b8174f..07d2d802915 100644
--- a/app/models/integrations/slack.rb
+++ b/app/models/integrations/slack.rb
@@ -20,5 +20,12 @@ module Integrations
def webhook_help
'https://hooks.slack.com/services/…'
end
+
+ private
+
+ override :metrics_key_prefix
+ def metrics_key_prefix
+ 'i_ecosystem_slack_service'
+ end
end
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index fc083002c41..1dd11ff8315 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -91,7 +91,7 @@ class Issue < ApplicationRecord
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
+ has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :issue, validate: false
has_many :prometheus_alerts, through: :prometheus_alert_events
has_many :issue_customer_relations_contacts, class_name: 'CustomerRelations::IssueContact', inverse_of: :issue
has_many :customer_relations_contacts, through: :issue_customer_relations_contacts, source: :contact, class_name: 'CustomerRelations::Contact', inverse_of: :issues
@@ -105,9 +105,10 @@ class Issue < ApplicationRecord
validates :project, presence: true
validates :issue_type, presence: true
- validates :namespace, presence: true, if: -> { project.present? }
+ validates :namespace, presence: true
validates :work_item_type, presence: true
+ validate :allowed_work_item_type_change, on: :update, if: :work_item_type_id_changed?
validate :due_date_after_start_date
validate :parent_link_confidentiality
@@ -180,7 +181,7 @@ class Issue < ApplicationRecord
scope :without_hidden, -> {
if Feature.enabled?(:ban_user_feature_flag)
- where.not(author_id: Users::BannedUser.all.select(:user_id))
+ where('NOT EXISTS (?)', Users::BannedUser.select(1).where('issues.author_id = banned_users.user_id'))
else
all
end
@@ -216,8 +217,8 @@ class Issue < ApplicationRecord
before_validation :ensure_namespace_id, :ensure_work_item_type
- after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
+ after_commit :expire_etag_cache, unless: :importing?
after_create_commit :record_create_action, unless: :importing?
attr_spammable :title, spam_title: true
@@ -743,6 +744,17 @@ class Issue < ApplicationRecord
self.work_item_type = WorkItems::Type.default_by_type(issue_type)
end
+
+ def allowed_work_item_type_change
+ return unless changes[:work_item_type_id]
+
+ involved_types = WorkItems::Type.where(id: changes[:work_item_type_id].compact).pluck(:base_type).uniq
+ disallowed_types = involved_types - WorkItems::Type::CHANGEABLE_BASE_TYPES
+
+ return if disallowed_types.empty?
+
+ errors.add(:work_item_type_id, format(_('can not be changed to %{new_type}'), new_type: work_item_type&.name))
+ end
end
Issue.prepend_mod_with('Issue')
diff --git a/app/models/issue_collection.rb b/app/models/issue_collection.rb
deleted file mode 100644
index 05607fc3a08..00000000000
--- a/app/models/issue_collection.rb
+++ /dev/null
@@ -1,44 +0,0 @@
-# frozen_string_literal: true
-
-# IssueCollection can be used to reduce a list of issues down to a subset.
-#
-# IssueCollection is not meant to be some sort of Enumerable, instead it's meant
-# to take a list of issues and return a new list of issues based on some
-# criteria. For example, given a list of issues you may want to return a list of
-# issues that can be read or updated by a given user.
-class IssueCollection
- attr_reader :collection
-
- def initialize(collection)
- @collection = collection
- end
-
- # Returns all the issues that can be updated by the user.
- def updatable_by_user(user)
- return collection if user.admin?
-
- # Given all the issue projects we get a list of projects that the current
- # user has at least reporter access to.
- projects_with_reporter_access = user
- .projects_with_reporter_access_limited_to(project_ids)
- .pluck(:id)
-
- collection.select do |issue|
- if projects_with_reporter_access.include?(issue.project_id)
- true
- elsif issue.is_a?(Issue)
- issue.assignee_or_author?(user)
- else
- false
- end
- end
- end
-
- alias_method :visible_to, :updatable_by_user
-
- private
-
- def project_ids
- @project_ids ||= collection.map(&:project_id).uniq
- end
-end
diff --git a/app/models/issue_email_participant.rb b/app/models/issue_email_participant.rb
index 76a96151350..dd963bc9e7e 100644
--- a/app/models/issue_email_participant.rb
+++ b/app/models/issue_email_participant.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class IssueEmailParticipant < ApplicationRecord
+ include BulkInsertSafe
+
belongs_to :issue
validates :email, uniqueness: { scope: [:issue_id], case_sensitive: false }
diff --git a/app/models/iteration.rb b/app/models/iteration.rb
index c6269313d8b..ebec24731ed 100644
--- a/app/models/iteration.rb
+++ b/app/models/iteration.rb
@@ -4,9 +4,6 @@
class Iteration < ApplicationRecord
include IgnorableColumns
- # TODO https://gitlab.com/gitlab-org/gitlab/-/issues/372126
- ignore_column :project_id, remove_with: '15.7', remove_after: '2022-11-18'
-
self.table_name = 'sprints'
def self.reference_prefix
diff --git a/app/models/jira_connect_installation.rb b/app/models/jira_connect_installation.rb
index 23813fa138f..0e88d1ceae9 100644
--- a/app/models/jira_connect_installation.rb
+++ b/app/models/jira_connect_installation.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class JiraConnectInstallation < ApplicationRecord
+ include Gitlab::Routing
+
attr_encrypted :shared_secret,
mode: :per_attribute_iv,
algorithm: 'aes-256-gcm',
@@ -37,13 +39,19 @@ class JiraConnectInstallation < ApplicationRecord
def audience_url
return unless proxy?
- Gitlab::Utils.append_path(instance_url, '/-/jira_connect')
+ Gitlab::Utils.append_path(instance_url, jira_connect_base_path)
end
def audience_installed_event_url
return unless proxy?
- Gitlab::Utils.append_path(instance_url, '/-/jira_connect/events/installed')
+ Gitlab::Utils.append_path(instance_url, jira_connect_events_installed_path)
+ end
+
+ def audience_uninstalled_event_url
+ return unless proxy?
+
+ Gitlab::Utils.append_path(instance_url, jira_connect_events_uninstalled_path)
end
def proxy?
diff --git a/app/models/key.rb b/app/models/key.rb
index 78b0a38bcaa..1f2234129ed 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -32,12 +32,18 @@ class Key < ApplicationRecord
delegate :name, :email, to: :user, prefix: true
- after_commit :add_to_authorized_keys, on: :create
+ enum usage_type: {
+ auth_and_signing: 0,
+ auth: 1,
+ signing: 2
+ }
+
after_create :post_create_hook
after_create :refresh_user_cache
- after_commit :remove_from_authorized_keys, on: :destroy
after_destroy :post_destroy_hook
after_destroy :refresh_user_cache
+ after_commit :add_to_authorized_keys, on: :create
+ after_commit :remove_from_authorized_keys, on: :destroy
alias_attribute :fingerprint_md5, :fingerprint
alias_attribute :name, :title
@@ -45,6 +51,8 @@ class Key < ApplicationRecord
scope :preload_users, -> { preload(:user) }
scope :for_user, -> (user) { where(user: user) }
scope :order_last_used_at_desc, -> { reorder(arel_table[:last_used_at].desc.nulls_last) }
+ scope :auth, -> { where(usage_type: [:auth, :auth_and_signing]) }
+ scope :signing, -> { where(usage_type: [:signing, :auth_and_signing]) }
# Date is set specifically in this scope to improve query time.
scope :expired_today_and_not_notified, -> { where(["date(expires_at AT TIME ZONE 'UTC') = CURRENT_DATE AND expiry_notification_delivered_at IS NULL"]) }
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 8aa48561e60..e1f28c0e117 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -4,7 +4,6 @@ class LfsObject < ApplicationRecord
include AfterCommitQueue
include Checksummable
include EachBatch
- include ObjectStorage::BackgroundMove
include FileStoreMounter
has_many :lfs_objects_projects
diff --git a/app/models/member.rb b/app/models/member.rb
index 80c5fd7e468..107530daf51 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -61,6 +61,7 @@ class Member < ApplicationRecord
validate :access_level_inclusion
validate :validate_member_role_access_level
validate :validate_access_level_locked_for_member_role, on: :update
+ validate :validate_member_role_belongs_to_same_root_namespace
scope :with_invited_user_state, -> do
joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
@@ -515,12 +516,22 @@ class Member < ApplicationRecord
end
end
+ def validate_member_role_belongs_to_same_root_namespace
+ return unless member_role_id
+
+ return if member_namespace.id == member_role.namespace_id
+ return if member_namespace.root_ancestor.id == member_role.namespace_id
+
+ errors.add(:member_namespace, _("must be in same hierarchy as custom role's namespace"))
+ end
+
def send_invite
# override in subclass
end
def send_request
notification_service.new_access_request(self)
+ todo_service.create_member_access_request(self) if source_type != 'Project'
end
def post_create_hook
@@ -579,6 +590,12 @@ class Member < ApplicationRecord
end
# rubocop: enable CodeReuse/ServiceClass
+ # rubocop: disable CodeReuse/ServiceClass
+ def todo_service
+ TodoService.new
+ end
+ # rubocop: enable CodeReuse/ServiceClass
+
def notifiable_options
{}
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index ad1ad1e74fe..796b05b7fff 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -55,6 +55,12 @@ class GroupMember < Member
{ group: group }
end
+ def last_owner_of_the_group?
+ return false unless access_level == Gitlab::Access::OWNER
+
+ group.member_last_owner?(self) || group.member_last_blocked_owner?(self)
+ end
+
private
override :refresh_member_authorized_projects
diff --git a/app/models/members/member_role.rb b/app/models/members/member_role.rb
index b4e3d6874ef..e9d7b1d3f80 100644
--- a/app/models/members/member_role.rb
+++ b/app/models/members/member_role.rb
@@ -1,18 +1,30 @@
# frozen_string_literal: true
class MemberRole < ApplicationRecord # rubocop:disable Gitlab/NamespacedClass
+ include IgnorableColumns
+ ignore_column :download_code, remove_with: '15.9', remove_after: '2023-01-22'
+
has_many :members
belongs_to :namespace
validates :namespace, presence: true
validates :base_access_level, presence: true
validate :belongs_to_top_level_namespace
+ validate :validate_namespace_locked, on: :update
+
+ validates_associated :members
private
def belongs_to_top_level_namespace
return if !namespace || namespace.root?
- errors.add(:namespace, s_("must be top-level namespace"))
+ errors.add(:namespace, s_("MemberRole|must be top-level namespace"))
+ end
+
+ def validate_namespace_locked
+ return unless namespace_id_changed?
+
+ errors.add(:namespace, s_("MemberRole|can't be changed"))
end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 1099e0f48c0..6aa6afb595d 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -96,6 +96,10 @@ class ProjectMember < Member
{ project: project }
end
+ def holder_of_the_personal_namespace?
+ project.personal_namespace_holder?(user)
+ end
+
private
override :access_level_inclusion
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 735c0df1529..78c6d983a3d 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -121,6 +121,7 @@ class MergeRequest < ApplicationRecord
has_many :draft_notes
has_many :reviews, inverse_of: :merge_request
+ has_many :reviewed_by_users, -> { distinct }, through: :reviews, source: :author
has_many :created_environments, class_name: 'Environment', foreign_key: :merge_request_id, inverse_of: :merge_request
KNOWN_MERGE_PARAMS = [
@@ -139,6 +140,7 @@ class MergeRequest < ApplicationRecord
after_create :ensure_merge_request_diff, unless: :skip_ensure_merge_request_diff
after_update :clear_memoized_shas
after_update :reload_diff_if_branch_changed
+ after_save :keep_around_commit, unless: :importing?
after_commit :ensure_metrics, on: [:create, :update], unless: :importing?
after_commit :expire_etag_cache, unless: :importing?
@@ -246,7 +248,9 @@ class MergeRequest < ApplicationRecord
end
after_transition any => [:unchecked, :cannot_be_merged_recheck, :checking, :cannot_be_merged_rechecking, :can_be_merged, :cannot_be_merged] do |merge_request, transition|
- GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ merge_request.run_after_commit do
+ GraphqlTriggers.merge_request_merge_status_updated(merge_request)
+ end
end
# rubocop: disable CodeReuse/ServiceClass
@@ -438,8 +442,6 @@ class MergeRequest < ApplicationRecord
.pick(MergeRequest::Metrics.time_to_merge_expression)
end
- after_save :keep_around_commit, unless: :importing?
-
alias_attribute :project, :target_project
alias_attribute :project_id, :target_project_id
@@ -1270,7 +1272,7 @@ class MergeRequest < ApplicationRecord
end
def mergeable_discussions_state?
- return true unless project.only_allow_merge_if_all_discussions_are_resolved?
+ return true unless project.only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: true)
unresolved_notes.none?(&:to_be_resolved?)
end
@@ -1382,7 +1384,7 @@ class MergeRequest < ApplicationRecord
def default_merge_commit_message(include_description: false, user: nil)
if self.target_project.merge_commit_template.present? && !include_description
- return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).merge_message
+ return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).merge_commit_message
end
closes_issues_references = visible_closing_issues_for.map do |issue|
@@ -1398,7 +1400,7 @@ class MergeRequest < ApplicationRecord
message << "Closes #{closes_issues_references.to_sentence}"
end
- message << "#{description}" if include_description && description.present?
+ message << description if include_description && description.present?
message << "See merge request #{to_reference(full: true)}"
message.join("\n\n")
@@ -1406,7 +1408,7 @@ class MergeRequest < ApplicationRecord
def default_squash_commit_message(user: nil)
if self.target_project.squash_commit_template.present?
- return ::Gitlab::MergeRequests::CommitMessageGenerator.new(merge_request: self, current_user: user).squash_message
+ return ::Gitlab::MergeRequests::MessageGenerator.new(merge_request: self, current_user: user).squash_commit_message
end
title
@@ -1451,9 +1453,9 @@ class MergeRequest < ApplicationRecord
end
def mergeable_ci_state?
- return true unless project.only_allow_merge_if_pipeline_succeeds?
+ return true unless project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
return false unless actual_head_pipeline
- return true if project.allow_merge_on_skipped_pipeline? && actual_head_pipeline.skipped?
+ return true if project.allow_merge_on_skipped_pipeline?(inherit_group_setting: true) && actual_head_pipeline.skipped?
actual_head_pipeline.success?
end
diff --git a/app/models/merge_request/predictions.rb b/app/models/merge_request/predictions.rb
deleted file mode 100644
index ef9e00b5f74..00000000000
--- a/app/models/merge_request/predictions.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# frozen_string_literal: true
-
-class MergeRequest::Predictions < ApplicationRecord # rubocop:disable Style/ClassAndModuleChildren
- belongs_to :merge_request, inverse_of: :predictions
-
- validates :suggested_reviewers, json_schema: { filename: 'merge_request_predictions_suggested_reviewers' }
-end
diff --git a/app/models/merge_request_context_commit.rb b/app/models/merge_request_context_commit.rb
index ebbdecf8aa7..281e11c7c13 100644
--- a/app/models/merge_request_context_commit.rb
+++ b/app/models/merge_request_context_commit.rb
@@ -12,7 +12,7 @@ class MergeRequestContextCommit < ApplicationRecord
validates :sha, presence: true
validates :sha, uniqueness: { message: 'has already been added' }
- serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
# Sort by committed date in descending order to ensure latest commits comes on the top
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 98a9ccc2040..cff8911d84b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,7 +6,6 @@ class MergeRequestDiff < ApplicationRecord
include ManualInverseAssociation
include EachBatch
include Gitlab::Utils::StrongMemoize
- include ObjectStorage::BackgroundMove
include BulkInsertableAssociations
# Don't display more than 100 commits at once
@@ -267,7 +266,7 @@ class MergeRequestDiff < ApplicationRecord
end
# This method will rely on repository branch sha
- # in case start_commit_sha is nil. Its necesarry for old merge request diff
+ # in case start_commit_sha is nil. It's necessary for old merge request diff
# created before version 8.4 to work
def safe_start_commit_sha
start_commit_sha || merge_request.target_branch_sha
@@ -414,6 +413,29 @@ class MergeRequestDiff < ApplicationRecord
end
end
+ def paginated_diffs(page, per_page)
+ fetching_repository_diffs({}) do |comparison|
+ reorder_diff_files!
+
+ collection = Gitlab::Diff::FileCollection::PaginatedMergeRequestDiff.new(
+ self,
+ page,
+ per_page
+ )
+
+ if comparison
+ comparison.diffs(
+ paths: collection.diff_paths,
+ page: collection.current_page,
+ per_page: collection.limit_value,
+ count: collection.total_count
+ )
+ else
+ collection
+ end
+ end
+ end
+
def diffs(diff_options = nil)
fetching_repository_diffs(diff_options) do |comparison|
# It should fetch the repository when diffs are cleaned by the system.
diff --git a/app/models/merge_request_diff_commit.rb b/app/models/merge_request_diff_commit.rb
index 152fb195c97..7e2efa2049b 100644
--- a/app/models/merge_request_diff_commit.rb
+++ b/app/models/merge_request_diff_commit.rb
@@ -35,7 +35,7 @@ class MergeRequestDiffCommit < ApplicationRecord
sha_attribute :sha
alias_attribute :id, :sha
- serialize :trailers, Serializers::Json # rubocop:disable Cop/ActiveRecordSerialize
+ attribute :trailers, :ind_jsonb
validates :trailers, json_schema: { filename: 'git_trailers' }
scope :with_users, -> { preload(:commit_author, :committer) }
diff --git a/app/models/ml/candidate.rb b/app/models/ml/candidate.rb
index f7da4418624..f24161d598f 100644
--- a/app/models/ml/candidate.rb
+++ b/app/models/ml/candidate.rb
@@ -11,6 +11,7 @@ module Ml
belongs_to :user
has_many :metrics, class_name: 'Ml::CandidateMetric'
has_many :params, class_name: 'Ml::CandidateParam'
+ has_many :metadata, class_name: 'Ml::CandidateMetadata'
has_many :latest_metrics, -> { latest }, class_name: 'Ml::CandidateMetric', inverse_of: :candidate
attribute :iid, default: -> { SecureRandom.uuid }
@@ -18,7 +19,21 @@ module Ml
scope :including_metrics_and_params, -> { includes(:latest_metrics, :params) }
def artifact_root
- "/ml_candidate_#{iid}/-/"
+ "/#{package_name}/#{package_version}/"
+ end
+
+ def artifact
+ ::Packages::Generic::PackageFinder.new(experiment.project).execute!(package_name, package_version)
+ rescue ActiveRecord::RecordNotFound
+ nil
+ end
+
+ def package_name
+ "ml_candidate_#{iid}"
+ end
+
+ def package_version
+ '-'
end
class << self
diff --git a/app/models/ml/candidate_metadata.rb b/app/models/ml/candidate_metadata.rb
new file mode 100644
index 00000000000..06b893c211f
--- /dev/null
+++ b/app/models/ml/candidate_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class CandidateMetadata < ApplicationRecord
+ validates :candidate, presence: true
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :candidate, message: ->(candidate, _) { "'#{candidate.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :candidate, class_name: 'Ml::Candidate'
+ end
+end
diff --git a/app/models/ml/experiment.rb b/app/models/ml/experiment.rb
index 05b238b960d..0a326b0e005 100644
--- a/app/models/ml/experiment.rb
+++ b/app/models/ml/experiment.rb
@@ -10,6 +10,7 @@ module Ml
belongs_to :project
belongs_to :user
has_many :candidates, class_name: 'Ml::Candidate'
+ has_many :metadata, class_name: 'Ml::ExperimentMetadata'
has_internal_id :iid, scope: :project
diff --git a/app/models/ml/experiment_metadata.rb b/app/models/ml/experiment_metadata.rb
new file mode 100644
index 00000000000..93496807e1a
--- /dev/null
+++ b/app/models/ml/experiment_metadata.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ml
+ class ExperimentMetadata < ApplicationRecord
+ validates :experiment, presence: true
+ validates :name,
+ length: { maximum: 250 },
+ presence: true,
+ uniqueness: { scope: :experiment, message: ->(exp, _) { "'#{exp.name}' already taken" } }
+ validates :value, length: { maximum: 5000 }, presence: true
+
+ belongs_to :experiment, class_name: 'Ml::Experiment'
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 51c39ad4ec3..d7d53956656 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -86,6 +86,7 @@ class Namespace < ApplicationRecord
has_many :issues, inverse_of: :namespace
has_many :timelog_categories, class_name: 'TimeTracking::TimelogCategory'
+ has_many :achievements, class_name: 'Achievements::Achievement'
validates :owner, presence: true, if: ->(n) { n.owner_required? }
validates :name,
@@ -131,26 +132,28 @@ class Namespace < ApplicationRecord
to: :namespace_settings, allow_nil: true
delegate :show_diff_preview_in_email, :show_diff_preview_in_email?, :show_diff_preview_in_email=,
to: :namespace_settings
+ delegate :runner_registration_enabled, :runner_registration_enabled?, :runner_registration_enabled=,
+ to: :namespace_settings
delegate :maven_package_requests_forwarding,
:pypi_package_requests_forwarding,
:npm_package_requests_forwarding,
to: :package_settings
- after_save :reload_namespace_details
-
- after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
-
before_create :sync_share_with_group_lock_with_parent
before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
after_update :force_share_with_group_lock_on_descendants, if: -> { saved_change_to_share_with_group_lock? && share_with_group_lock? }
after_update :expire_first_auto_devops_config_cache, if: -> { saved_change_to_auto_devops_enabled? }
+ after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
+ after_destroy :rm_dir
+ after_save :reload_namespace_details
+
+ after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
+
after_sync_traversal_ids :schedule_sync_event_worker # custom callback defined in Namespaces::Traversal::Linear
# Legacy Storage specific hooks
- after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
before_destroy(prepend: true) { prepare_for_destroy }
- after_destroy :rm_dir
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?
@@ -330,6 +333,13 @@ class Namespace < ApplicationRecord
type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?)
end
+ def bot_user_namespace?
+ return false unless user_namespace?
+ return false unless owner && owner.bot?
+
+ true
+ end
+
def owner_required?
user_namespace?
end
@@ -507,6 +517,10 @@ class Namespace < ApplicationRecord
root? && actual_plan.paid?
end
+ def prevent_delete?
+ paid?
+ end
+
def actual_limits
# We default to PlanLimits.new otherwise a lot of specs would fail
# On production each plan should already have associated limits record
@@ -541,12 +555,10 @@ class Namespace < ApplicationRecord
def shared_runners_setting
if shared_runners_enabled
SR_ENABLED
+ elsif allow_descendants_override_disabled_shared_runners
+ SR_DISABLED_WITH_OVERRIDE
else
- if allow_descendants_override_disabled_shared_runners
- SR_DISABLED_WITH_OVERRIDE
- else
- SR_DISABLED_AND_UNOVERRIDABLE
- end
+ SR_DISABLED_AND_UNOVERRIDABLE
end
end
@@ -597,6 +609,10 @@ class Namespace < ApplicationRecord
namespace_settings&.enabled_git_access_protocol
end
+ def all_ancestors_have_runner_registration_enabled?
+ namespace_settings&.all_ancestors_have_runner_registration_enabled?
+ end
+
private
def cluster_enabled_granted?
diff --git a/app/models/namespace_setting.rb b/app/models/namespace_setting.rb
index 3e6371b0c4d..5081d5cdafe 100644
--- a/app/models/namespace_setting.rb
+++ b/app/models/namespace_setting.rb
@@ -59,6 +59,16 @@ class NamespaceSetting < ApplicationRecord
all_ancestors_allow_diff_preview_in_email?
end
+ def runner_registration_enabled?
+ runner_registration_enabled && all_ancestors_have_runner_registration_enabled?
+ end
+
+ def all_ancestors_have_runner_registration_enabled?
+ return true unless namespace.has_parent?
+
+ !self.class.where(namespace_id: namespace.ancestors, runner_registration_enabled: false).exists?
+ end
+
private
def all_ancestors_allow_diff_preview_in_email?
diff --git a/app/models/namespace_statistics.rb b/app/models/namespace_statistics.rb
index 04ca05d85ff..a17ca2e2c1d 100644
--- a/app/models/namespace_statistics.rb
+++ b/app/models/namespace_statistics.rb
@@ -10,8 +10,8 @@ class NamespaceStatistics < ApplicationRecord # rubocop:disable Gitlab/Namespace
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
before_save :update_storage_size
- after_save :update_root_storage_statistics, if: :saved_change_to_storage_size?
after_destroy :update_root_storage_statistics
+ after_save :update_root_storage_statistics, if: :saved_change_to_storage_size?
delegate :group_namespace?, to: :namespace
diff --git a/app/models/note.rb b/app/models/note.rb
index 8e1f4979602..052df6142c5 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -168,10 +168,10 @@ class Note < ApplicationRecord
# Syncs `confidential` with `internal` as we rename the column.
# https://gitlab.com/gitlab-org/gitlab/-/issues/367923
before_create :set_internal_flag
+ after_destroy :expire_etag_cache
after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits }
after_save :expire_etag_cache, unless: :importing?
after_save :touch_noteable, unless: :importing?
- after_destroy :expire_etag_cache
after_commit :notify_after_create, on: :create
after_commit :notify_after_destroy, on: :destroy
diff --git a/app/models/operations/feature_flags_client.rb b/app/models/operations/feature_flags_client.rb
index e8c237abbc5..5a05d76254d 100644
--- a/app/models/operations/feature_flags_client.rb
+++ b/app/models/operations/feature_flags_client.rb
@@ -19,11 +19,11 @@ module Operations
before_validation :ensure_token!
- def self.find_for_project_and_token(project, token)
- return unless project
+ def self.find_for_project_and_token(project_id, token)
+ return unless project_id
return unless token
- where(project_id: project).find_by_token(token)
+ where(project_id: project_id).find_by_token(token)
end
def self.update_last_feature_flag_updated_at!(project)
diff --git a/app/models/packages/package.rb b/app/models/packages/package.rb
index 317db51f4ef..17c5415939c 100644
--- a/app/models/packages/package.rb
+++ b/app/models/packages/package.rb
@@ -149,6 +149,7 @@ class Packages::Package < ApplicationRecord
end
scope :preload_composer, -> { preload(:composer_metadatum) }
scope :preload_npm_metadatum, -> { preload(:npm_metadatum) }
+ scope :preload_pypi_metadatum, -> { preload(:pypi_metadatum) }
scope :without_nuget_temporary_name, -> { where.not(name: Packages::Nuget::TEMPORARY_PACKAGE_NAME) }
diff --git a/app/models/packages/rpm/repository_file.rb b/app/models/packages/rpm/repository_file.rb
index 4b5fa59c6ee..614ec9b3e56 100644
--- a/app/models/packages/rpm/repository_file.rb
+++ b/app/models/packages/rpm/repository_file.rb
@@ -8,6 +8,8 @@ module Packages
include Packages::Installable
INSTALLABLE_STATUSES = [:default].freeze
+ FILELISTS_FILENAME = 'filelists.xml'
+ FILELISTS_SIZE_LIMITATION = 20.megabytes
enum status: { default: 0, pending_destruction: 1, processing: 2, error: 3 }
@@ -20,6 +22,14 @@ module Packages
mount_file_store_uploader Packages::Rpm::RepositoryFileUploader
update_project_statistics project_statistics_name: :packages_size
+
+ def self.has_oversized_filelists?(project_id:)
+ where(
+ project_id: project_id,
+ file_name: FILELISTS_FILENAME,
+ size: [FILELISTS_SIZE_LIMITATION..]
+ ).exists?
+ end
end
end
end
diff --git a/app/models/pages/lookup_path.rb b/app/models/pages/lookup_path.rb
index c1056d4f6cb..cf0f0f9e92f 100644
--- a/app/models/pages/lookup_path.rb
+++ b/app/models/pages/lookup_path.rb
@@ -19,11 +19,13 @@ module Pages
def access_control
project.private_pages?
end
+ strong_memoize_attr :access_control
def https_only
domain_https = domain ? domain.https? : true
project.pages_https_only? && domain_https
end
+ strong_memoize_attr :https_only
def source
return unless deployment&.file
@@ -41,6 +43,7 @@ module Pages
file_count: deployment.file_count
}
end
+ strong_memoize_attr :source
def prefix
if project.pages_group_root?
@@ -49,6 +52,7 @@ module Pages
project.full_path.delete_prefix(trim_prefix) + '/'
end
end
+ strong_memoize_attr :prefix
private
diff --git a/app/models/pages/virtual_domain.rb b/app/models/pages/virtual_domain.rb
index 119cc7fc166..fafbe449c8c 100644
--- a/app/models/pages/virtual_domain.rb
+++ b/app/models/pages/virtual_domain.rb
@@ -28,6 +28,7 @@ module Pages
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
end
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index 328c67a0711..4e3f4b0c328 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -17,6 +17,7 @@ class PagesDomain < ApplicationRecord
has_many :acme_orders, class_name: "PagesDomainAcmeOrder"
has_many :serverless_domain_clusters, class_name: 'Serverless::DomainCluster', inverse_of: :pages_domain
+ after_initialize :set_verification_code
before_validation :clear_auto_ssl_failure, unless: :auto_ssl_enabled
validates :domain, hostname: { allow_numeric_hostname: true }
@@ -44,8 +45,6 @@ class PagesDomain < ApplicationRecord
key: Settings.attr_encrypted_db_key_base,
algorithm: 'aes-256-cbc'
- after_initialize :set_verification_code
-
scope :for_project, ->(project) { where(project: project) }
scope :enabled, -> { where('enabled_until >= ?', Time.current) }
diff --git a/app/models/performance_monitoring/prometheus_dashboard.rb b/app/models/performance_monitoring/prometheus_dashboard.rb
index 4804f620a99..37bf080ae49 100644
--- a/app/models/performance_monitoring/prometheus_dashboard.rb
+++ b/app/models/performance_monitoring/prometheus_dashboard.rb
@@ -53,8 +53,6 @@ module PerformanceMonitoring
# This method is planned to be refactored as a part of https://gitlab.com/gitlab-org/gitlab/-/issues/219398
# implementation. For new existing logic was reused to faster deliver MVC
def schema_validation_warnings
- return run_custom_validation.map(&:message) if Feature.enabled?(:metrics_dashboard_exhaustive_validations, environment&.project)
-
self.class.from_json(reload_schema)
[]
rescue Gitlab::Metrics::Dashboard::Errors::LayoutError => e
@@ -65,11 +63,6 @@ module PerformanceMonitoring
private
- def run_custom_validation
- Gitlab::Metrics::Dashboard::Validator
- .errors(reload_schema, dashboard_path: path, project: environment&.project)
- end
-
# dashboard finder methods are somehow limited, #find includes checking if
# user is authorised to view selected dashboard, but modifies schema, which in some cases may
# cause false positives returned from validation, and #find_raw does not authorise users
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index 3126dba9d6d..887ef36cc17 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -18,6 +18,7 @@ class PersonalAccessToken < ApplicationRecord
belongs_to :user
+ after_initialize :set_default_scopes, if: :persisted?
before_save :ensure_token
scope :active, -> { not_revoked.not_expired }
@@ -41,8 +42,6 @@ class PersonalAccessToken < ApplicationRecord
validates :scopes, presence: true
validate :validate_scopes
- after_initialize :set_default_scopes, if: :persisted?
-
def revoke!
update!(revoked: true)
end
diff --git a/app/models/postgresql/detached_partition.rb b/app/models/postgresql/detached_partition.rb
index b0dd52c9657..d26778957d5 100644
--- a/app/models/postgresql/detached_partition.rb
+++ b/app/models/postgresql/detached_partition.rb
@@ -7,5 +7,9 @@ module Postgresql
def fully_qualified_table_name
"#{Gitlab::Database::DYNAMIC_PARTITIONS_SCHEMA}.#{table_name}"
end
+
+ def table_schema
+ Gitlab::Database::GitlabSchema.table_schema(table_name)
+ end
end
end
diff --git a/app/models/programming_language.rb b/app/models/programming_language.rb
index 06e3034e56a..4156c672518 100644
--- a/app/models/programming_language.rb
+++ b/app/models/programming_language.rb
@@ -10,4 +10,22 @@ class ProgrammingLanguage < ApplicationRecord
sanitized_names = names.map(&method(:sanitize_sql_like))
where(arel_table[:name].matches_any(sanitized_names))
end
+
+ def self.most_popular(limit = 25)
+ sql = <<~SQL
+ SELECT
+ mcv
+ FROM
+ pg_stats
+ CROSS JOIN LATERAL
+ unnest(most_common_vals::text::int[]) mt(mcv)
+ WHERE
+ tablename = 'repository_languages' and attname='programming_language_id'
+ LIMIT
+ $1
+ SQL
+ ids = connection.exec_query(sql, 'SQL', [limit]).rows.flatten
+
+ where(id: ids).order(:name)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 0c4f76fb2b9..73dbb55a07b 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -89,68 +89,56 @@ class Project < ApplicationRecord
cache_markdown_field :description, pipeline: :description
- default_value_for :packages_enabled, true
- default_value_for :archived, false
- default_value_for :resolve_outdated_diff_discussions, false
- default_value_for(:repository_storage) do
- Repository.pick_storage_shard
- end
+ attribute :packages_enabled, default: true
+ attribute :archived, default: false
+ attribute :resolve_outdated_diff_discussions, default: false
+ attribute :repository_storage, default: -> { Repository.pick_storage_shard }
+ attribute :shared_runners_enabled, default: -> { Gitlab::CurrentSettings.shared_runners_enabled }
+ attribute :only_allow_merge_if_all_discussions_are_resolved, default: false
+ attribute :remove_source_branch_after_merge, default: true
+ attribute :autoclose_referenced_issues, default: true
+ attribute :ci_config_path, default: -> { Gitlab::CurrentSettings.default_ci_config_path }
- default_value_for(:shared_runners_enabled) { Gitlab::CurrentSettings.shared_runners_enabled }
default_value_for :issues_enabled, gitlab_config_features.issues
default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
default_value_for :builds_enabled, gitlab_config_features.builds
default_value_for :wiki_enabled, gitlab_config_features.wiki
default_value_for :snippets_enabled, gitlab_config_features.snippets
- default_value_for :only_allow_merge_if_all_discussions_are_resolved, false
- default_value_for :remove_source_branch_after_merge, true
- default_value_for :autoclose_referenced_issues, true
- default_value_for(:ci_config_path) { Gitlab::CurrentSettings.default_ci_config_path }
add_authentication_token_field :runners_token,
encrypted: -> { Feature.enabled?(:projects_tokens_optional_encryption) ? :optional : :required },
prefix: RunnersTokenPrefixable::RUNNERS_TOKEN_PREFIX
+ # Storage specific hooks
+ after_initialize :use_hashed_storage
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
- before_save :ensure_runners_token
before_validation :ensure_project_namespace_in_sync
-
before_validation :set_package_registry_access_level, if: :packages_enabled_changed?
before_validation :remove_leading_spaces_on_name
-
- after_save :update_project_statistics, if: :saved_change_to_namespace_id?
-
- after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
-
- after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
-
- after_save :save_topics
-
- after_save :reload_project_namespace_details
+ after_validation :check_pending_delete
+ before_save :ensure_runners_token
after_create -> { create_or_load_association(:project_feature) }
-
after_create -> { create_or_load_association(:ci_cd_settings) }
-
after_create -> { create_or_load_association(:container_expiration_policy) }
-
after_create -> { create_or_load_association(:pages_metadatum) }
-
after_create :set_timestamps_for_create
+ after_create :check_repository_absence!
after_update :update_forks_visibility_level
-
before_destroy :remove_private_deploy_keys
+ after_destroy :remove_exports
+ after_save :update_project_statistics, if: :saved_change_to_namespace_id?
- use_fast_destroy :build_trace_chunks
+ after_save :schedule_sync_event_worker, if: -> { saved_change_to_id? || saved_change_to_namespace_id? }
- after_destroy :remove_exports
+ after_save :create_import_state, if: ->(project) { project.import? && project.import_state.nil? }
- after_validation :check_pending_delete
+ after_save :save_topics
- # Storage specific hooks
- after_initialize :use_hashed_storage
- after_create :check_repository_absence!
+ after_save :reload_project_namespace_details
+
+ use_fast_destroy :build_trace_chunks
has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic'
has_many :topics, through: :project_topics, class_name: 'Projects::Topic'
@@ -196,7 +184,6 @@ class Project < ApplicationRecord
has_one :emails_on_push_integration, class_name: 'Integrations::EmailsOnPush'
has_one :ewm_integration, class_name: 'Integrations::Ewm'
has_one :external_wiki_integration, class_name: 'Integrations::ExternalWiki'
- has_one :flowdock_integration, class_name: 'Integrations::Flowdock'
has_one :hangouts_chat_integration, class_name: 'Integrations::HangoutsChat'
has_one :harbor_integration, class_name: 'Integrations::Harbor'
has_one :irker_integration, class_name: 'Integrations::Irker'
@@ -231,6 +218,17 @@ class Project < ApplicationRecord
has_one :fork_network_member
has_one :fork_network, through: :fork_network_member
has_one :forked_from_project, through: :fork_network_member
+
+ # Projects with a very large number of notes may time out destroying them
+ # through the foreign key. Additionally, the deprecated attachment uploader
+ # for notes requires us to use dependent: :destroy to avoid orphaning uploaded
+ # files.
+ #
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/207222
+ # Order of this association is important for project deletion.
+ # has_many :notes` should be the first association among all `has_many` associations.
+ has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+
has_many :forked_to_members, class_name: 'ForkNetworkMember', foreign_key: 'forked_from_project_id'
has_many :forks, through: :forked_to_members, source: :project, inverse_of: :forked_from_project
has_many :fork_network_projects, through: :fork_network, source: :projects
@@ -259,25 +257,30 @@ class Project < ApplicationRecord
has_one :service_desk_setting, class_name: 'ServiceDeskSetting'
# Merge requests for target project should be removed with it
- has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project
+ has_many :merge_requests, foreign_key: 'target_project_id', inverse_of: :target_project, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :merge_request_metrics, foreign_key: 'target_project', class_name: 'MergeRequest::Metrics', inverse_of: :target_project
has_many :source_of_merge_requests, foreign_key: 'source_project_id', class_name: 'MergeRequest'
- has_many :issues
+ has_many :issues, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :incident_management_issuable_escalation_statuses, through: :issues, inverse_of: :project, class_name: 'IncidentManagement::IssuableEscalationStatus'
has_many :incident_management_timeline_event_tags, inverse_of: :project, class_name: 'IncidentManagement::TimelineEventTag'
- has_many :labels, class_name: 'ProjectLabel'
- has_many :integrations
+ has_many :labels, class_name: 'ProjectLabel', dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :events
has_many :milestones
- # Projects with a very large number of notes may time out destroying them
- # through the foreign key. Additionally, the deprecated attachment uploader
- # for notes requires us to use dependent: :destroy to avoid orphaning uploaded
- # files.
- #
- # https://gitlab.com/gitlab-org/gitlab/-/issues/207222
- has_many :notes, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
-
+ has_many :integrations
+ has_many :alert_hooks_integrations, -> { alert_hooks }, class_name: 'Integration'
+ has_many :archive_trace_hooks_integrations, -> { archive_trace_hooks }, class_name: 'Integration'
+ has_many :confidential_issue_hooks_integrations, -> { confidential_issue_hooks }, class_name: 'Integration'
+ has_many :confidential_note_hooks_integrations, -> { confidential_note_hooks }, class_name: 'Integration'
+ has_many :deployment_hooks_integrations, -> { deployment_hooks }, class_name: 'Integration'
+ has_many :issue_hooks_integrations, -> { issue_hooks }, class_name: 'Integration'
+ has_many :job_hooks_integrations, -> { job_hooks }, class_name: 'Integration'
+ has_many :merge_request_hooks_integrations, -> { merge_request_hooks }, class_name: 'Integration'
+ has_many :note_hooks_integrations, -> { note_hooks }, class_name: 'Integration'
+ has_many :pipeline_hooks_integrations, -> { pipeline_hooks }, class_name: 'Integration'
+ has_many :push_hooks_integrations, -> { push_hooks }, class_name: 'Integration'
+ has_many :tag_push_hooks_integrations, -> { tag_push_hooks }, class_name: 'Integration'
+ has_many :wiki_page_hooks_integrations, -> { wiki_page_hooks }, class_name: 'Integration'
has_many :snippets, class_name: 'ProjectSnippet'
has_many :hooks, class_name: 'ProjectHook'
has_many :protected_branches
@@ -380,7 +383,7 @@ class Project < ApplicationRecord
has_one :auto_devops, class_name: 'ProjectAutoDevops', inverse_of: :project, autosave: true
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
- has_many :project_badges, class_name: 'ProjectBadge'
+ has_many :project_badges, class_name: 'ProjectBadge', inverse_of: :project
has_one :ci_cd_settings, class_name: 'ProjectCiCdSetting', inverse_of: :project, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :remote_mirrors, inverse_of: :project
@@ -650,6 +653,11 @@ class Project < ApplicationRecord
.where(repository_languages: { programming_language_id: lang_id_query })
end
+ scope :with_programming_language_id, ->(language_id) do
+ joins(:repository_languages)
+ .where(repository_languages: { programming_language_id: language_id })
+ end
+
scope :service_desk_enabled, -> { where(service_desk_enabled: true) }
scope :with_builds_enabled, -> { with_feature_enabled(:builds) }
scope :with_issues_enabled, -> { with_feature_enabled(:issues) }
@@ -742,6 +750,29 @@ class Project < ApplicationRecord
end
end
+ # Defines instance methods:
+ #
+ # - only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: false)
+ # - allow_merge_on_skipped_pipeline?(inherit_group_setting: false)
+ # - only_allow_merge_if_all_discussions_are_resolved?(inherit_group_setting: false)
+ # - only_allow_merge_if_pipeline_succeeds_locked?
+ # - allow_merge_on_skipped_pipeline_locked?
+ # - only_allow_merge_if_all_discussions_are_resolved_locked?
+ def self.cascading_with_parent_namespace(attribute)
+ # method overriden in EE
+ define_method("#{attribute}?") do |inherit_group_setting: false|
+ self.public_send(attribute) # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ define_method("#{attribute}_locked?") do
+ false
+ end
+ end
+
+ cascading_with_parent_namespace :only_allow_merge_if_pipeline_succeeds
+ cascading_with_parent_namespace :allow_merge_on_skipped_pipeline
+ cascading_with_parent_namespace :only_allow_merge_if_all_discussions_are_resolved
+
def self.with_feature_available_for_user(feature, user)
with_project_feature.merge(ProjectFeature.with_feature_available_for_user(feature, user))
end
@@ -1691,8 +1722,14 @@ class Project < ApplicationRecord
def execute_integrations(data, hooks_scope = :push_hooks)
# Call only service hooks that are active for this scope
run_after_commit_or_now do
- integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
- integration.async_execute(data)
+ if use_integration_relations?
+ association("#{hooks_scope}_integrations").reader.each do |integration|
+ integration.async_execute(data)
+ end
+ else
+ integrations.public_send(hooks_scope).each do |integration| # rubocop:disable GitlabSecurity/PublicSend
+ integration.async_execute(data)
+ end
end
end
end
@@ -2301,6 +2338,7 @@ class Project < ApplicationRecord
.append(key: 'CI_PROJECT_PATH', value: full_path)
.append(key: 'CI_PROJECT_PATH_SLUG', value: full_path_slug)
.append(key: 'CI_PROJECT_NAMESPACE', value: namespace.full_path)
+ .append(key: 'CI_PROJECT_NAMESPACE_ID', value: namespace.id.to_s)
.append(key: 'CI_PROJECT_ROOT_NAMESPACE', value: namespace.root_ancestor.path)
.append(key: 'CI_PROJECT_URL', value: web_url)
.append(key: 'CI_PROJECT_VISIBILITY', value: Gitlab::VisibilityLevel.string_level(visibility_level))
@@ -2700,7 +2738,13 @@ class Project < ApplicationRecord
def access_request_approvers_to_be_notified
access_request_approvers = members.owners_and_maintainers
- access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+ recipients = access_request_approvers.connected_to_user.order_recent_sign_in.limit(Member::ACCESS_REQUEST_APPROVERS_TO_BE_NOTIFIED_LIMIT)
+
+ if recipients.blank?
+ recipients = group.access_request_approvers_to_be_notified
+ end
+
+ recipients
end
def pages_lookup_path(trim_prefix: nil, domain: nil)
@@ -2994,6 +3038,10 @@ class Project < ApplicationRecord
group&.work_items_feature_flag_enabled? || Feature.enabled?(:work_items, self)
end
+ def work_items_mvc_feature_flag_enabled?
+ group&.work_items_mvc_feature_flag_enabled? || Feature.enabled?(:work_items_mvc)
+ end
+
def work_items_mvc_2_feature_flag_enabled?
group&.work_items_mvc_2_feature_flag_enabled? || Feature.enabled?(:work_items_mvc_2)
end
@@ -3321,6 +3369,12 @@ class Project < ApplicationRecord
ProjectFeature::PRIVATE
end
end
+
+ def use_integration_relations?
+ strong_memoize(:use_integration_relations) do
+ Feature.enabled?(:cache_project_integrations, self)
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb
index decc71ee193..d26ce5465cd 100644
--- a/app/models/project_export_job.rb
+++ b/app/models/project_export_job.rb
@@ -1,11 +1,24 @@
# frozen_string_literal: true
class ProjectExportJob < ApplicationRecord
+ include EachBatch
+
+ EXPIRES_IN = 7.days
+
belongs_to :project
has_many :relation_exports, class_name: 'Projects::ImportExport::RelationExport'
validates :project, :jid, :status, presence: true
+ STATUS = {
+ queued: 0,
+ started: 1,
+ finished: 2,
+ failed: 3
+ }.freeze
+
+ scope :prunable, -> { where("updated_at < ?", EXPIRES_IN.ago) }
+
state_machine :status, initial: :queued do
event :start do
transition [:queued] => :started
@@ -19,9 +32,17 @@ class ProjectExportJob < ApplicationRecord
transition [:queued, :started] => :failed
end
- state :queued, value: 0
- state :started, value: 1
- state :finished, value: 2
- state :failed, value: 3
+ state :queued, value: STATUS[:queued]
+ state :started, value: STATUS[:started]
+ state :finished, value: STATUS[:finished]
+ state :failed, value: STATUS[:failed]
+ end
+
+ class << self
+ def prune_expired_jobs
+ prunable.each_batch do |relation| # rubocop:disable Style/SymbolProc
+ relation.delete_all
+ end
+ end
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 0570be85ad1..506f6305791 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -11,21 +11,21 @@ class ProjectStatistics < ApplicationRecord
attribute :snippets_size, default: 0
counter_attribute :build_artifacts_size
+ counter_attribute :packages_size
- counter_attribute_after_flush do |project_statistic|
- project_statistic.refresh_storage_size!
+ counter_attribute_after_commit do |project_statistics|
+ project_statistics.refresh_storage_size!
- Namespaces::ScheduleAggregationWorker.perform_async(project_statistic.namespace_id)
+ Namespaces::ScheduleAggregationWorker.perform_async(project_statistics.namespace_id)
end
before_save :update_storage_size
COLUMNS_TO_REFRESH = [:repository_size, :wiki_size, :lfs_objects_size, :commit_count, :snippets_size, :uploads_size, :container_registry_size].freeze
- INCREMENTABLE_COLUMNS = {
- packages_size: %i[storage_size],
- pipeline_artifacts_size: %i[storage_size],
- snippets_size: %i[storage_size]
- }.freeze
+ INCREMENTABLE_COLUMNS = [
+ :pipeline_artifacts_size,
+ :snippets_size
+ ].freeze
NAMESPACE_RELATABLE_COLUMNS = [:repository_size, :wiki_size, :lfs_objects_size, :uploads_size, :container_registry_size].freeze
STORAGE_SIZE_COMPONENTS = [
:repository_size,
@@ -120,35 +120,27 @@ class ProjectStatistics < ApplicationRecord
# we have to update the storage_size separately.
#
# For counter attributes, storage_size will be refreshed after the counter is flushed,
- # through counter_attribute_after_flush
+ # through counter_attribute_after_commit
#
# For non-counter attributes, storage_size is updated depending on key => [columns] in INCREMENTABLE_COLUMNS
def self.increment_statistic(project, key, amount)
- raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
- return if amount == 0
-
project.statistics.try do |project_statistics|
- if counter_attribute_enabled?(key)
- project_statistics.delayed_increment_counter(key, amount)
- else
- project_statistics.legacy_increment_statistic(key, amount)
- end
+ project_statistics.increment_statistic(key, amount)
end
end
- def self.incrementable_attribute?(key)
- INCREMENTABLE_COLUMNS.key?(key) || counter_attribute_enabled?(key)
- end
-
- def legacy_increment_statistic(key, amount)
- increment_columns!(key, amount)
+ def increment_statistic(key, amount)
+ raise ArgumentError, "Cannot increment attribute: #{key}" unless incrementable_attribute?(key)
- Namespaces::ScheduleAggregationWorker.perform_async( # rubocop: disable CodeReuse/Worker
- project.namespace_id)
+ increment_counter(key, amount)
end
private
+ def incrementable_attribute?(key)
+ INCREMENTABLE_COLUMNS.include?(key) || counter_attribute_enabled?(key)
+ end
+
def storage_size_components
STORAGE_SIZE_COMPONENTS
end
@@ -157,16 +149,6 @@ class ProjectStatistics < ApplicationRecord
storage_size_components.map { |component| "COALESCE (#{component}, 0)" }.join(' + ').freeze
end
- def increment_columns!(key, amount)
- increments = { key => amount }
- additional = INCREMENTABLE_COLUMNS.fetch(key, [])
- additional.each do |column|
- increments[column] = amount
- end
-
- update_counters_with_lease(increments)
- end
-
def schedule_namespace_aggregation_worker
run_after_commit do
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
diff --git a/app/models/projects/forks/divergence_counts.rb b/app/models/projects/forks/divergence_counts.rb
new file mode 100644
index 00000000000..7d630b00083
--- /dev/null
+++ b/app/models/projects/forks/divergence_counts.rb
@@ -0,0 +1,72 @@
+# frozen_string_literal: true
+
+module Projects
+ module Forks
+ # Class for calculating the divergence of a fork with the source project
+ class DivergenceCounts
+ LATEST_COMMITS_COUNT = 10
+ EXPIRATION_TIME = 8.hours
+
+ def initialize(project, ref)
+ @project = project
+ @fork_repo = project.repository
+ @source_repo = project.fork_source.repository
+ @ref = ref
+ end
+
+ def counts
+ ahead, behind = divergence_counts
+
+ { ahead: ahead, behind: behind }
+ end
+
+ private
+
+ attr_reader :project, :fork_repo, :source_repo, :ref
+
+ def cache_key
+ @cache_key ||= ['project_forks', project.id, ref, 'divergence_counts']
+ end
+
+ def divergence_counts
+ fork_sha = fork_repo.commit(ref).sha
+ source_sha = source_repo.commit.sha
+
+ cached_source_sha, cached_fork_sha, counts = Rails.cache.read(cache_key)
+ return counts if cached_source_sha == source_sha && cached_fork_sha == fork_sha
+
+ counts = calculate_divergence_counts(fork_sha, source_sha)
+
+ Rails.cache.write(cache_key, [source_sha, fork_sha, counts], expires_in: EXPIRATION_TIME)
+
+ counts
+ end
+
+ def calculate_divergence_counts(fork_sha, source_sha)
+ # If the upstream latest commit exists in the fork repo, then
+ # it's possible to calculate divergence counts within the fork repository.
+ return fork_repo.diverging_commit_count(fork_sha, source_sha) if fork_repo.commit(source_sha)
+
+ # Otherwise, we need to find a commit that exists both in the fork and upstream
+ # in order to use this commit as a base for calculating divergence counts.
+ # Considering the fact that a user usually creates a fork to contribute to the upstream,
+ # it is expected that they have a limited number of commits ahead of upstream.
+ # Let's take the latest N commits and check their existence upstream.
+ last_commits_shas = fork_repo.commits(ref, limit: LATEST_COMMITS_COUNT).map(&:sha)
+ existence_hash = source_repo.check_objects_exist(last_commits_shas)
+ first_matched_commit_sha = last_commits_shas.find { |sha| existence_hash[sha] }
+
+ # If we can't find such a commit, we return early and tell the user that the branches
+ # have diverged and action is required.
+ return unless first_matched_commit_sha
+
+ # Otherwise, we use upstream to calculate divergence counts from the matched commit
+ ahead, behind = source_repo.diverging_commit_count(first_matched_commit_sha, source_sha)
+ # And add the number of commits a fork is ahead of the first matched commit
+ ahead += last_commits_shas.index(first_matched_commit_sha)
+
+ [ahead, behind]
+ end
+ end
+ end
+end
diff --git a/app/models/projects/import_export/relation_export_upload.rb b/app/models/projects/import_export/relation_export_upload.rb
index 965dc39d19f..12cfb3415d8 100644
--- a/app/models/projects/import_export/relation_export_upload.rb
+++ b/app/models/projects/import_export/relation_export_upload.rb
@@ -4,7 +4,6 @@ module Projects
module ImportExport
class RelationExportUpload < ApplicationRecord
include WithUploads
- include ObjectStorage::BackgroundMove
self.table_name = 'project_relation_export_uploads'
diff --git a/app/models/prometheus_alert.rb b/app/models/prometheus_alert.rb
index 9080f3d9de1..59440947d71 100644
--- a/app/models/prometheus_alert.rb
+++ b/app/models/prometheus_alert.rb
@@ -20,8 +20,8 @@ class PrometheusAlert < ApplicationRecord
has_many :related_issues, through: :prometheus_alert_events
has_many :alert_management_alerts, class_name: 'AlertManagement::Alert', inverse_of: :prometheus_alert
- after_save :clear_prometheus_adapter_cache!
after_destroy :clear_prometheus_adapter_cache!
+ after_save :clear_prometheus_adapter_cache!
validates :environment, :project, :prometheus_metric, :threshold, :operator, presence: true
validates :runbook_url, length: { maximum: 255 }, allow_blank: true,
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 80967c1b072..c59ef4cd80b 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -14,10 +14,12 @@ class ProtectedBranch < ApplicationRecord
scope :allowing_force_push,
-> { where(allow_force_push: true) }
- scope :get_ids_by_name, -> (name) { where(name: name).pluck(:id) }
-
protected_ref_access_levels :merge, :push
+ def self.get_ids_by_name(name)
+ where(name: name).pluck(:id)
+ end
+
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
# Maintainers, owners and admins are allowed to create the default branch
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index f8d500e106b..b830cf313af 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -20,12 +20,11 @@ class RemoteMirror < ApplicationRecord
belongs_to :project, inverse_of: :remote_mirrors
- validates :url, presence: true, public_url: { schemes: %w(ssh git http https), allow_blank: true, enforce_user: true }
-
- after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
- after_update :reset_fields, if: :saved_change_to_mirror_url?
+ validates :url, presence: true, public_url: { schemes: Project::VALID_MIRROR_PROTOCOLS, allow_blank: true, enforce_user: true }
before_validation :store_credentials
+ after_update :reset_fields, if: :saved_change_to_mirror_url?
+ after_save :set_override_remote_mirror_available, unless: -> { Gitlab::CurrentSettings.current_application_settings.mirror_available }
scope :enabled, -> { where(enabled: true) }
scope :started, -> { with_update_status(:started) }
diff --git a/app/models/resource_label_event.rb b/app/models/resource_label_event.rb
index a1753df9294..a1426540cf5 100644
--- a/app/models/resource_label_event.rb
+++ b/app/models/resource_label_event.rb
@@ -14,8 +14,8 @@ class ResourceLabelEvent < ResourceEvent
validates :label, presence: { unless: :importing? }, on: :create
validate :exactly_one_issuable, unless: :importing?
- after_save :expire_etag_cache
after_destroy :expire_etag_cache
+ after_save :expire_etag_cache
enum action: {
add: 1,
diff --git a/app/models/service_desk_setting.rb b/app/models/service_desk_setting.rb
index 6dd7415d928..738f18ca5e3 100644
--- a/app/models/service_desk_setting.rb
+++ b/app/models/service_desk_setting.rb
@@ -53,7 +53,7 @@ class ServiceDeskSetting < ApplicationRecord
def projects_with_same_slug_and_key_exists?
return false unless project_key
- settings = self.class.with_project_key(project_key).preload(:project)
+ settings = self.class.with_project_key(project_key).where.not(project_id: project_id).preload(:project)
project_slug = self.project.full_path_slug
settings.any? do |setting|
diff --git a/app/models/snippet_statistics.rb b/app/models/snippet_statistics.rb
index 6fb6f0ef713..44bff0e1e5b 100644
--- a/app/models/snippet_statistics.rb
+++ b/app/models/snippet_statistics.rb
@@ -12,8 +12,8 @@ class SnippetStatistics < ApplicationRecord
delegate :repository, :project, :project_id, to: :snippet
- after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics?
after_destroy :update_author_root_storage_statistics, unless: :project_snippet?
+ after_save :update_author_root_storage_statistics, if: :update_author_root_storage_statistics?
def update_commit_count
self.commit_count = repository.commit_count
diff --git a/app/models/synthetic_note.rb b/app/models/synthetic_note.rb
index dea7165af9f..a60c0d2f3bc 100644
--- a/app/models/synthetic_note.rb
+++ b/app/models/synthetic_note.rb
@@ -10,6 +10,7 @@ class SyntheticNote < Note
system: true,
author: event.user,
created_at: event.created_at,
+ updated_at: event.created_at,
discussion_id: event.discussion_id,
noteable: resource,
event: event,
diff --git a/app/models/todo.rb b/app/models/todo.rb
index f2fa0df852a..32ec4accb4b 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -19,6 +19,7 @@ class Todo < ApplicationRecord
DIRECTLY_ADDRESSED = 7
MERGE_TRAIN_REMOVED = 8 # This is an EE-only feature
REVIEW_REQUESTED = 9
+ MEMBER_ACCESS_REQUESTED = 10
ACTION_NAMES = {
ASSIGNED => :assigned,
@@ -29,10 +30,11 @@ class Todo < ApplicationRecord
APPROVAL_REQUIRED => :approval_required,
UNMERGEABLE => :unmergeable,
DIRECTLY_ADDRESSED => :directly_addressed,
- MERGE_TRAIN_REMOVED => :merge_train_removed
+ MERGE_TRAIN_REMOVED => :merge_train_removed,
+ MEMBER_ACCESS_REQUESTED => :member_access_requested
}.freeze
- ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED].freeze
+ ACTIONS_MULTIPLE_ALLOWED = [Todo::MENTIONED, Todo::DIRECTLY_ADDRESSED, Todo::MEMBER_ACCESS_REQUESTED].freeze
belongs_to :author, class_name: "User"
belongs_to :note
@@ -198,6 +200,16 @@ class Todo < ApplicationRecord
action == MERGE_TRAIN_REMOVED
end
+ def member_access_requested?
+ action == MEMBER_ACCESS_REQUESTED
+ end
+
+ def access_request_url
+ return "" unless self.target_type == 'Namespace'
+
+ Gitlab::Routing.url_helpers.group_group_members_url(self.target, tab: 'access_requests')
+ end
+
def done?
state == 'done'
end
@@ -209,6 +221,8 @@ class Todo < ApplicationRecord
def body
if note.present?
note.note
+ elsif member_access_requested?
+ target.full_path
else
target.title
end
@@ -246,6 +260,8 @@ class Todo < ApplicationRecord
def target_reference
if for_commit?
target.reference_link_text
+ elsif member_access_requested?
+ target.full_path
else
target.to_reference
end
diff --git a/app/models/upload.rb b/app/models/upload.rb
index ac7ebb31abc..a4fbc703146 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -16,14 +16,13 @@ class Upload < ApplicationRecord
scope :with_files_stored_locally, -> { where(store: ObjectStorage::Store::LOCAL) }
scope :with_files_stored_remotely, -> { where(store: ObjectStorage::Store::REMOTE) }
- before_save :calculate_checksum!, if: :foreground_checksummable?
- after_commit :schedule_checksum, if: :needs_checksum?
-
- after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
-
+ before_save :calculate_checksum!, if: :foreground_checksummable?
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
+ after_commit :schedule_checksum, if: :needs_checksum?
+
+ after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
class << self
def inner_join_local_uploads_projects
diff --git a/app/models/user.rb b/app/models/user.rb
index b4b8a7ef7ad..ba3f7922c9c 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -46,6 +46,8 @@ class User < ApplicationRecord
MAX_USERNAME_LENGTH = 255
MIN_USERNAME_LENGTH = 2
+ MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT = 100
+
SECONDARY_EMAIL_ATTRIBUTES = [
:commit_email,
:notification_email,
@@ -58,16 +60,16 @@ class User < ApplicationRecord
add_authentication_token_field :feed_token
add_authentication_token_field :static_object_token, encrypted: :optional
- default_value_for :admin, false
- default_value_for(:external) { Gitlab::CurrentSettings.user_default_external }
- default_value_for(:can_create_group) { Gitlab::CurrentSettings.can_create_group }
- default_value_for :can_create_team, false
- default_value_for :hide_no_ssh_key, false
- default_value_for :hide_no_password, false
- default_value_for :project_view, :files
- default_value_for :notified_of_own_activity, false
- default_value_for :preferred_language, I18n.default_locale
- default_value_for :theme_id, gitlab_config.default_theme
+ attribute :admin, default: false
+ attribute :external, default: -> { Gitlab::CurrentSettings.user_default_external }
+ attribute :can_create_group, default: -> { Gitlab::CurrentSettings.can_create_group }
+ attribute :can_create_team, default: false
+ attribute :hide_no_ssh_key, default: false
+ attribute :hide_no_password, default: false
+ attribute :project_view, default: :files
+ attribute :notified_of_own_activity, default: false
+ attribute :preferred_language, default: -> { I18n.default_locale }
+ attribute :theme_id, default: -> { gitlab_config.default_theme }
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -298,16 +300,17 @@ class User < ApplicationRecord
validates :website_url, allow_blank: true, url: true, if: :website_url_changed?
+ after_initialize :set_projects_limit
before_validation :sanitize_attrs
+ before_validation :ensure_namespace_correct
+ after_validation :set_username_errors
before_save :default_private_profile_to_false
before_save :ensure_incoming_email_token
before_save :ensure_user_rights_and_limits, if: ->(user) { user.new_record? || user.external_changed? }
before_save :skip_reconfirmation!, if: ->(user) { user.email_changed? && user.read_only_attribute?(:email) }
before_save :check_for_verified_email, if: ->(user) { user.email_changed? && !user.new_record? }
- before_validation :ensure_namespace_correct
before_save :ensure_namespace_correct # in case validation is skipped
before_save :ensure_user_detail_assigned
- after_validation :set_username_errors
after_update :username_changed_hook, if: :saved_change_to_username?
after_destroy :post_destroy_hook
after_destroy :remove_key_cache
@@ -328,8 +331,6 @@ class User < ApplicationRecord
update_invalid_gpg_signatures if previous_changes.key?('email')
end
- after_initialize :set_projects_limit
-
# User's Layout preference
enum layout: { fixed: 0, fluid: 1 }
@@ -360,6 +361,7 @@ class User < ApplicationRecord
:diffs_deletion_color, :diffs_deletion_color=,
:diffs_addition_color, :diffs_addition_color=,
:use_legacy_web_ide, :use_legacy_web_ide=,
+ :use_new_navigation, :use_new_navigation=,
to: :user_preference
delegate :path, to: :namespace, allow_nil: true, prefix: true
@@ -376,6 +378,14 @@ class User < ApplicationRecord
accepts_nested_attributes_for :credit_card_validation, update_only: true, allow_destroy: true
state_machine :state, initial: :active do
+ # state_machine uses this method at class loading time to fetch the default
+ # value for the `state` column but in doing so it also evaluates all other
+ # columns default values which could trigger the recursive generation of
+ # ApplicationSetting records. We're setting it to `nil` here because we
+ # don't have a database default for the `state` column.
+ #
+ def owner_class_attribute_default; end
+
event :block do
transition active: :blocked
transition deactivated: :blocked
@@ -811,7 +821,7 @@ class User < ApplicationRecord
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
- find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').where(id: key_id))
+ find_by('EXISTS (?)', Key.select(1).where('keys.user_id = users.id').auth.where(id: key_id))
end
def find_by_full_path(path, follow_redirects: false)
@@ -896,6 +906,18 @@ class User < ApplicationRecord
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?
@@ -1759,12 +1781,10 @@ class User < ApplicationRecord
end
def ci_owned_runners
- @ci_owned_runners ||= begin
- Ci::Runner
+ @ci_owned_runners ||= Ci::Runner
.from_union([ci_owned_project_runners_from_project_members,
ci_owned_project_runners_from_group_members,
ci_owned_group_runners])
- end
end
def owns_runner?(runner)
@@ -1773,7 +1793,11 @@ class User < ApplicationRecord
def notification_email_for(notification_group)
# Return group-specific email address if present, otherwise return global notification email address
- notification_group&.notification_email_for(self) || notification_email_or_default
+ group_email = if notification_group && notification_group.respond_to?(:notification_email_for)
+ notification_group.notification_email_for(self)
+ end
+
+ group_email || notification_email_or_default
end
def notification_settings_for(source, inherit: false)
@@ -1866,6 +1890,7 @@ class User < ApplicationRecord
def invalidate_issue_cache_counts
Rails.cache.delete(['users', id, 'assigned_open_issues_count'])
+ Rails.cache.delete(['users', id, 'max_assigned_open_issues_count']) if Feature.enabled?(:limit_assigned_issues_count)
end
def invalidate_merge_request_cache_counts
@@ -2323,9 +2348,7 @@ class User < ApplicationRecord
end
def check_password_weakness
- if Feature.enabled?(:block_weak_passwords) &&
- password.present? &&
- Security::WeakPasswords.weak_for_user?(password, self)
+ if password.present? && Security::WeakPasswords.weak_for_user?(password, self)
errors.add(:password, _('must not contain commonly used combinations of words and letters'))
end
end
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index 2e662faea6a..0570bc2f395 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -19,7 +19,7 @@ class UserDetail < ApplicationRecord
validates :skype, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :location, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
validates :organization, length: { maximum: DEFAULT_FIELD_LENGTH }, allow_blank: true
- validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true
+ validates :website_url, length: { maximum: DEFAULT_FIELD_LENGTH }, url: true, allow_blank: true, if: :website_url_changed?
before_validation :sanitize_attrs
before_save :prevent_nil_bio
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index c6ebd550daf..bc2c6b526b8 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -26,10 +26,10 @@ class UserPreference < ApplicationRecord
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
- default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
- default_value_for :time_display_relative, value: true, allows_nil: false
- default_value_for :time_format_in_24h, value: false, allows_nil: false
- default_value_for :render_whitespace_in_code, value: false, allows_nil: false
+ attribute :tab_width, default: -> { Gitlab::TabWidth::DEFAULT }
+ attribute :time_display_relative, default: true
+ attribute :time_format_in_24h, default: false
+ attribute :render_whitespace_in_code, default: false
class << self
def notes_filters
@@ -59,6 +59,67 @@ class UserPreference < ApplicationRecord
self[notes_filter_field_for(resource)]
end
+ def tab_width
+ read_attribute(:tab_width) || self.class.column_defaults['tab_width']
+ end
+
+ def tab_width=(value)
+ if value.nil?
+ default = self.class.column_defaults['tab_width']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def time_display_relative
+ value = read_attribute(:time_display_relative)
+ return value unless value.nil?
+
+ self.class.column_defaults['time_display_relative']
+ end
+
+ def time_display_relative=(value)
+ if value.nil?
+ default = self.class.column_defaults['time_display_relative']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def time_format_in_24h
+ value = read_attribute(:time_format_in_24h)
+ return value unless value.nil?
+
+ self.class.column_defaults['time_format_in_24h']
+ end
+
+ def time_format_in_24h=(value)
+ if value.nil?
+ default = self.class.column_defaults['time_format_in_24h']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
+ def render_whitespace_in_code
+ value = read_attribute(:render_whitespace_in_code)
+ return value unless value.nil?
+
+ self.class.column_defaults['render_whitespace_in_code']
+ end
+
+ def render_whitespace_in_code=(value)
+ if value.nil?
+ default = self.class.column_defaults['render_whitespace_in_code']
+ super(default)
+ else
+ super(value)
+ end
+ end
+
private
def notes_filter_field_for(resource)
diff --git a/app/models/users/callout.rb b/app/models/users/callout.rb
index b037d07658d..3f9353214ee 100644
--- a/app/models/users/callout.rb
+++ b/app/models/users/callout.rb
@@ -63,7 +63,9 @@ 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
+ artifacts_management_page_feedback_banner: 62,
+ vscode_web_ide: 63,
+ vscode_web_ide_callout: 64
}
validates :feature_name,
diff --git a/app/models/users/group_callout.rb b/app/models/users/group_callout.rb
index 3e3e424e9c9..2552407fa4c 100644
--- a/app/models/users/group_callout.rb
+++ b/app/models/users/group_callout.rb
@@ -23,7 +23,8 @@ module Users
namespace_storage_limit_banner_alert_threshold: 12, # EE-only
namespace_storage_limit_banner_error_threshold: 13, # EE-only
usage_quota_trial_alert: 14, # EE-only
- preview_usage_quota_free_plan_alert: 15 # EE-only
+ preview_usage_quota_free_plan_alert: 15, # EE-only
+ enforcement_at_limit_alert: 16 # EE-only
}
validates :group, presence: true
diff --git a/app/models/users/phone_number_validation.rb b/app/models/users/phone_number_validation.rb
index f6123c01fd0..b9e4e908ddd 100644
--- a/app/models/users/phone_number_validation.rb
+++ b/app/models/users/phone_number_validation.rb
@@ -31,11 +31,17 @@ module Users
validates :telesign_reference_xid,
length: { maximum: 255 }
+ scope :for_user, -> (user_id) { where(user_id: user_id) }
+
def self.related_to_banned_user?(international_dial_code, phone_number)
joins(:banned_user).where(
international_dial_code: international_dial_code,
phone_number: phone_number
).exists?
end
+
+ def validated?
+ validated_at.present?
+ end
end
end
diff --git a/app/models/work_item.rb b/app/models/work_item.rb
index ed6f9d161a6..0810c520f7e 100644
--- a/app/models/work_item.rb
+++ b/app/models/work_item.rb
@@ -38,6 +38,18 @@ class WorkItem < Issue
end
end
+ def ancestors
+ hierarchy.ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_base_and_ancestors
+ hierarchy(same_type: true).base_and_ancestors(hierarchy_order: :asc)
+ end
+
+ def same_type_descendants_depth
+ hierarchy(same_type: true).max_descendants_depth.to_i
+ end
+
private
override :parent_link_confidentiality
@@ -56,6 +68,13 @@ class WorkItem < Issue
Gitlab::UsageDataCounters::WorkItemActivityUniqueCounter.track_work_item_created_action(author: author)
end
+
+ def hierarchy(options = {})
+ base = self.class.where(id: id)
+ base = base.where(work_item_type_id: work_item_type_id) if options[:same_type]
+
+ ::Gitlab::WorkItems::WorkItemHierarchy.new(base, options: options)
+ end
end
WorkItem.prepend_mod
diff --git a/app/models/work_items/hierarchy_restriction.rb b/app/models/work_items/hierarchy_restriction.rb
new file mode 100644
index 00000000000..a253447a8db
--- /dev/null
+++ b/app/models/work_items/hierarchy_restriction.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module WorkItems
+ class HierarchyRestriction < ApplicationRecord
+ self.table_name = 'work_item_hierarchy_restrictions'
+
+ belongs_to :parent_type, class_name: 'WorkItems::Type'
+ belongs_to :child_type, class_name: 'WorkItems::Type'
+
+ validates :parent_type, presence: true
+ validates :child_type, presence: true
+ validates :child_type, uniqueness: { scope: :parent_type_id }
+ end
+end
diff --git a/app/models/work_items/parent_link.rb b/app/models/work_items/parent_link.rb
index 13d6db3e08e..33857fb08c2 100644
--- a/app/models/work_items/parent_link.rb
+++ b/app/models/work_items/parent_link.rb
@@ -12,12 +12,14 @@ module WorkItems
validates :work_item_parent, presence: true
validates :work_item, presence: true, uniqueness: true
- validate :validate_child_type
- validate :validate_parent_type
+ validate :validate_hierarchy_restrictions
+ validate :validate_cyclic_reference
validate :validate_same_project
validate :validate_max_children
validate :validate_confidentiality
+ scope :for_parents, ->(parent_ids) { where(work_item_parent_id: parent_ids) }
+
class << self
def has_public_children?(parent_id)
joins(:work_item).where(work_item_parent_id: parent_id, 'issues.confidential': false).exists?
@@ -33,27 +35,6 @@ module WorkItems
private
- def validate_child_type
- return unless work_item
-
- unless work_item.task?
- errors.add :work_item, _('only Task can be assigned as a child in hierarchy.')
- end
- end
-
- def validate_parent_type
- return unless work_item_parent
-
- base_type = work_item_parent.work_item_type.base_type.to_sym
- unless PARENT_TYPES.include?(base_type)
- parent_names = WorkItems::Type::BASE_TYPES.slice(*WorkItems::ParentLink::PARENT_TYPES)
- .values.map { |type| type[:name] }
-
- errors.add :work_item_parent, _('only %{parent_types} can be parent of Task.') %
- { parent_types: parent_names.to_sentence }
- end
- end
-
def validate_same_project
return if work_item.nil? || work_item_parent.nil?
@@ -79,5 +60,40 @@ module WorkItems
"parent. Make the work item confidential and try again.")
end
end
+
+ def validate_hierarchy_restrictions
+ return unless work_item && work_item_parent
+
+ restriction = ::WorkItems::HierarchyRestriction
+ .find_by_parent_type_id_and_child_type_id(work_item_parent.work_item_type_id, work_item.work_item_type_id)
+
+ if restriction.nil?
+ errors.add :work_item, _('is not allowed to add this type of parent')
+ return
+ end
+
+ validate_depth(restriction.maximum_depth)
+ end
+
+ def validate_depth(depth)
+ return unless depth
+ return if work_item.work_item_type_id != work_item_parent.work_item_type_id
+
+ if work_item_parent.same_type_base_and_ancestors.count + work_item.same_type_descendants_depth > depth
+ errors.add :work_item, _('reached maximum depth')
+ end
+ end
+
+ def validate_cyclic_reference
+ return unless work_item_parent&.id && work_item&.id
+
+ if work_item.id == work_item_parent.id
+ errors.add :work_item, _('is not allowed to point to itself')
+ end
+
+ if work_item_parent.ancestors.detect { |ancestor| work_item.id == ancestor.id }
+ errors.add :work_item, _('is already present in ancestors')
+ end
+ end
end
end
diff --git a/app/models/work_items/type.rb b/app/models/work_items/type.rb
index dc30899d24f..e1f6a13f7a7 100644
--- a/app/models/work_items/type.rb
+++ b/app/models/work_items/type.rb
@@ -10,31 +10,86 @@ module WorkItems
include CacheMarkdownField
+ # type name is used in restrictions DB seeder to assure restrictions for
+ # default types are pre-filled
+ TYPE_NAMES = {
+ issue: 'Issue',
+ incident: 'Incident',
+ test_case: 'Test Case',
+ requirement: 'Requirement',
+ task: 'Task',
+ objective: 'Objective',
+ key_result: 'Key Result'
+ }.freeze
+
# Base types need to exist on the DB on app startup
# This constant is used by the DB seeder
# TODO - where to add new icon names created?
BASE_TYPES = {
- issue: { name: 'Issue', icon_name: 'issue-type-issue', enum_value: 0 },
- incident: { name: 'Incident', icon_name: 'issue-type-incident', enum_value: 1 },
- test_case: { name: 'Test Case', icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
- requirement: { name: 'Requirement', icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only
- task: { name: 'Task', icon_name: 'issue-type-task', enum_value: 4 },
- objective: { name: 'Objective', icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only
- key_result: { name: 'Key Result', icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
+ issue: { name: TYPE_NAMES[:issue], icon_name: 'issue-type-issue', enum_value: 0 },
+ incident: { name: TYPE_NAMES[:incident], icon_name: 'issue-type-incident', enum_value: 1 },
+ test_case: { name: TYPE_NAMES[:test_case], icon_name: 'issue-type-test-case', enum_value: 2 }, ## EE-only
+ requirement: { name: TYPE_NAMES[:requirement], icon_name: 'issue-type-requirements', enum_value: 3 }, ## EE-only
+ task: { name: TYPE_NAMES[:task], icon_name: 'issue-type-task', enum_value: 4 },
+ objective: { name: TYPE_NAMES[:objective], icon_name: 'issue-type-objective', enum_value: 5 }, ## EE-only
+ key_result: { name: TYPE_NAMES[:key_result], icon_name: 'issue-type-keyresult', enum_value: 6 } ## EE-only
}.freeze
WIDGETS_FOR_TYPE = {
- issue: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
- Widgets::Milestone],
- incident: [Widgets::Description, Widgets::Hierarchy],
- test_case: [Widgets::Description],
- requirement: [Widgets::Description],
- task: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::StartAndDueDate,
- Widgets::Milestone],
- objective: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::Hierarchy, Widgets::Milestone],
- key_result: [Widgets::Assignees, Widgets::Labels, Widgets::Description, Widgets::StartAndDueDate]
+ issue: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ incident: [
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::Notes
+ ],
+ test_case: [
+ Widgets::Description,
+ Widgets::Notes
+ ],
+ requirement: [
+ Widgets::Description,
+ Widgets::Notes
+ ],
+ task: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ objective: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::Milestone,
+ Widgets::Notes
+ ],
+ key_result: [
+ Widgets::Assignees,
+ Widgets::Labels,
+ Widgets::Description,
+ Widgets::Hierarchy,
+ Widgets::StartAndDueDate,
+ Widgets::Notes
+ ]
}.freeze
+ # A list of types user can change between - both original and new
+ # type must be included in this list. This is needed for legacy issues
+ # 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].freeze
cache_markdown_field :description, pipeline: :single_line
@@ -66,6 +121,7 @@ module WorkItems
return found_type if found_type
Gitlab::DatabaseImporters::WorkItems::BaseTypeImporter.upsert_types
+ Gitlab::DatabaseImporters::WorkItems::HierarchyRestrictionsImporter.upsert_restrictions
find_by(namespace_id: nil, base_type: type)
end
diff --git a/app/models/work_items/widgets/notes.rb b/app/models/work_items/widgets/notes.rb
new file mode 100644
index 00000000000..bde94ea8f43
--- /dev/null
+++ b/app/models/work_items/widgets/notes.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module WorkItems
+ module Widgets
+ class Notes < Base
+ delegate :notes, to: :work_item
+ delegate_missing_to :work_item
+
+ def declarative_policy_delegate
+ work_item
+ end
+ end
+ end
+end
diff --git a/app/policies/base_policy.rb b/app/policies/base_policy.rb
index f8e7a912896..1ce866bd910 100644
--- a/app/policies/base_policy.rb
+++ b/app/policies/base_policy.rb
@@ -19,6 +19,14 @@ class BasePolicy < DeclarativePolicy::Base
with_options scope: :user, score: 0
condition(:deactivated) { @user&.deactivated? }
+ desc "User is bot"
+ with_options scope: :user, score: 0
+ condition(:bot) { @user&.bot? }
+
+ desc "User is alert bot"
+ with_options scope: :user, score: 0
+ condition(:alert_bot) { @user&.alert_bot? }
+
desc "User is support bot"
with_options scope: :user, score: 0
condition(:support_bot) { @user&.support_bot? }
@@ -50,9 +58,6 @@ class BasePolicy < DeclarativePolicy::Base
::Gitlab::ExternalAuthorization.perform_check?
end
- with_options scope: :user, score: 0
- condition(:alert_bot) { @user&.alert_bot? }
-
rule { external_authorization_enabled & ~can?(:read_all_resources) }.policy do
prevent :read_cross_project
end
diff --git a/app/policies/ci/freeze_period_policy.rb b/app/policies/ci/freeze_period_policy.rb
index 60e53a7b2f9..9e2cca5e5a2 100644
--- a/app/policies/ci/freeze_period_policy.rb
+++ b/app/policies/ci/freeze_period_policy.rb
@@ -2,6 +2,6 @@
module Ci
class FreezePeriodPolicy < BasePolicy
- delegate { @subject.resource_parent }
+ delegate { @subject.project }
end
end
diff --git a/app/policies/ci/pipeline_schedule_variable_policy.rb b/app/policies/ci/pipeline_schedule_variable_policy.rb
new file mode 100644
index 00000000000..dbbf9221e77
--- /dev/null
+++ b/app/policies/ci/pipeline_schedule_variable_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class PipelineScheduleVariablePolicy < BasePolicy
+ delegate :pipeline_schedule
+ end
+end
diff --git a/app/policies/commit_signatures/ssh_signature_policy.rb b/app/policies/commit_signatures/ssh_signature_policy.rb
new file mode 100644
index 00000000000..34c8f123029
--- /dev/null
+++ b/app/policies/commit_signatures/ssh_signature_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module CommitSignatures
+ class SshSignaturePolicy < BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/app/policies/concerns/readonly_abilities.rb b/app/policies/concerns/archived_abilities.rb
index 300f17088b7..b4dfad599c7 100644
--- a/app/policies/concerns/readonly_abilities.rb
+++ b/app/policies/concerns/archived_abilities.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
-module ReadonlyAbilities
+module ArchivedAbilities
extend ActiveSupport::Concern
- READONLY_ABILITIES = %i[
+ ARCHIVED_ABILITIES = %i[
admin_tag
push_code
push_to_delete_protected_branch
@@ -16,7 +16,7 @@ module ReadonlyAbilities
create_incident
].freeze
- READONLY_FEATURES = %i[
+ ARCHIVED_FEATURES = %i[
issue
issue_board_list
merge_request
@@ -40,14 +40,14 @@ module ReadonlyAbilities
].freeze
class_methods do
- def readonly_abilities
- READONLY_ABILITIES
+ def archived_abilities
+ ARCHIVED_ABILITIES
end
- def readonly_features
- READONLY_FEATURES
+ def archived_features
+ ARCHIVED_FEATURES
end
end
end
-ReadonlyAbilities::ClassMethods.prepend_mod_with('ReadonlyAbilities::ClassMethods')
+ArchivedAbilities::ClassMethods.prepend_mod_with('ArchivedAbilities::ClassMethods')
diff --git a/app/policies/group_member_policy.rb b/app/policies/group_member_policy.rb
index f61f758a8e8..78ab9fc750b 100644
--- a/app/policies/group_member_policy.rb
+++ b/app/policies/group_member_policy.rb
@@ -6,7 +6,7 @@ class GroupMemberPolicy < BasePolicy
delegate :group
with_scope :subject
- condition(:last_owner) { @subject.group.member_last_owner?(@subject) || @subject.group.member_last_blocked_owner?(@subject) }
+ condition(:last_owner) { @subject.last_owner_of_the_group? }
condition(:project_bot) { @subject.user&.project_bot? && @subject.group.member?(@subject.user) }
desc "Membership is users' own"
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 806c57bab74..858c145de3f 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -83,8 +83,8 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
with_scope :subject
condition(:crm_enabled, score: 0, scope: :subject) { @subject.crm_enabled? }
- condition(:group_runner_registration_allowed, scope: :global) do
- Gitlab::CurrentSettings.valid_runner_registrars.include?('group')
+ condition(:group_runner_registration_allowed, scope: :subject) do
+ Gitlab::CurrentSettings.valid_runner_registrars.include?('group') && @subject.runner_registration_enabled?
end
rule { can?(:read_group) & design_management_enabled }.policy do
@@ -193,6 +193,7 @@ class GroupPolicy < Namespaces::GroupProjectNamespaceSharedPolicy
enable :admin_group_member
enable :change_visibility_level
+ enable :read_usage_quotas
enable :read_group_runners
enable :admin_group_runners
enable :register_group_runners
diff --git a/app/policies/issue_policy.rb b/app/policies/issue_policy.rb
index 87db228a698..491eebe9daf 100644
--- a/app/policies/issue_policy.rb
+++ b/app/policies/issue_policy.rb
@@ -9,7 +9,7 @@ class IssuePolicy < IssuablePolicy
desc "User can read confidential issues"
condition(:can_read_confidential) do
- @user && IssueCollection.new([@subject]).visible_to(@user).any?
+ @user && (@user.admin? || can?(:reporter_access) || assignee_or_author?) # rubocop:disable Cop/UserAdmin
end
desc "Project belongs to a group, crm is enabled and user can read contacts in the root group"
@@ -27,6 +27,23 @@ class IssuePolicy < IssuablePolicy
desc "Issue is persisted"
condition(:persisted, scope: :subject) { @subject.persisted? }
+ # accessing notes requires the notes widget to be available for work items(or issue)
+ condition(:notes_widget_enabled, scope: :subject) do
+ @subject.work_item_type.widgets.include?(::WorkItems::Widgets::Notes)
+ end
+
+ rule { ~notes_widget_enabled }.policy do
+ prevent :create_note
+ prevent :read_note
+ prevent :read_internal_note
+ prevent :set_note_created_at
+ prevent :mark_note_as_confidential
+ # these actions on notes are not available on issues/work items yet,
+ # but preventing any action on work item notes as long as there is no notes widget seems reasonable
+ prevent :resolve_note
+ prevent :reposition_note
+ end
+
rule { confidential & ~can_read_confidential }.policy do
prevent(*create_read_update_admin_destroy(:issue))
prevent :read_issue_iid
diff --git a/app/policies/merge_request_policy.rb b/app/policies/merge_request_policy.rb
index bda327cb661..1759cf057e4 100644
--- a/app/policies/merge_request_policy.rb
+++ b/app/policies/merge_request_policy.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MergeRequestPolicy < IssuablePolicy
+ condition(:can_approve) { can_approve? }
+
rule { locked }.policy do
prevent :reopen_merge_request
end
@@ -14,10 +16,14 @@ class MergeRequestPolicy < IssuablePolicy
prevent :accept_merge_request
end
- rule { can?(:update_merge_request) & is_project_member }.policy do
+ rule { can_approve }.policy do
enable :approve_merge_request
end
+ rule { can?(:approve_merge_request) & bot }.policy do
+ enable :reset_merge_request_approvals
+ end
+
rule { ~anonymous & can?(:read_merge_request) }.policy do
enable :create_todo
enable :update_subscription
@@ -32,6 +38,12 @@ class MergeRequestPolicy < IssuablePolicy
rule { can?(:admin_merge_request) }.policy do
enable :set_merge_request_metadata
end
+
+ private
+
+ def can_approve?
+ can?(:update_merge_request) && is_project_member?
+ end
end
MergeRequestPolicy.prepend_mod_with('MergeRequestPolicy')
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
index 89158578ac1..1deeae8241f 100644
--- a/app/policies/namespaces/user_namespace_policy.rb
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -5,6 +5,7 @@ module Namespaces
rule { anonymous }.prevent_all
condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
+ condition(:bot_user_namespace) { @subject.bot_user_namespace? }
condition(:owner) { @subject.owner == @user }
rule { owner | admin }.policy do
@@ -21,6 +22,8 @@ module Namespaces
rule { ~can_create_personal_project }.prevent :create_projects
+ rule { bot_user_namespace }.prevent :create_projects
+
rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
end
end
diff --git a/app/policies/note_policy.rb b/app/policies/note_policy.rb
index 67b57595beb..9fd95bbe42d 100644
--- a/app/policies/note_policy.rb
+++ b/app/policies/note_policy.rb
@@ -20,12 +20,20 @@ class NotePolicy < BasePolicy
condition(:confidential, scope: :subject) { @subject.confidential? }
+ # if noteable is a work item it needs to check the notes widget availability
+ condition(:notes_widget_enabled, scope: :subject) do
+ !@subject.noteable.respond_to?(:work_item_type) ||
+ @subject.noteable.work_item_type.widgets.include?(::WorkItems::Widgets::Notes)
+ end
+
# Should be matched with IssuablePolicy#read_internal_note
# and EpicPolicy#read_internal_note
condition(:can_read_confidential) do
access_level >= Gitlab::Access::REPORTER || admin?
end
+ rule { ~notes_widget_enabled }.prevent_all
+
rule { ~editable }.prevent :admin_note
# If user can't read the issue/MR/etc then they should not be allowed to do anything to their own notes
diff --git a/app/policies/project_member_policy.rb b/app/policies/project_member_policy.rb
index bcfc7c87d41..ace74dca448 100644
--- a/app/policies/project_member_policy.rb
+++ b/app/policies/project_member_policy.rb
@@ -5,7 +5,7 @@ class ProjectMemberPolicy < BasePolicy
delegate { @subject.project }
condition(:target_is_holder_of_the_personal_namespace, scope: :subject) do
- @subject.project.personal_namespace_holder?(@subject.user)
+ @subject.holder_of_the_personal_namespace?
end
desc "Membership is users' own access request"
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index bfeb1a602ab..7f67e80e432 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -2,7 +2,7 @@
class ProjectPolicy < BasePolicy
include CrudPolicyHelpers
- include ReadonlyAbilities
+ include ArchivedAbilities
desc "Project has public builds enabled"
condition(:public_builds, scope: :subject, score: 0) { project.public_builds? }
@@ -121,7 +121,7 @@ class ProjectPolicy < BasePolicy
desc "If user is authenticated via CI job token then the target project should be in scope"
condition(:project_allowed_for_job_token) do
- !@user&.from_ci_job_token? || @user.ci_job_token_scope.includes?(project)
+ !@user&.from_ci_job_token? || @user.ci_job_token_scope.allows?(project)
end
with_scope :subject
@@ -369,29 +369,12 @@ class ProjectPolicy < BasePolicy
prevent(:metrics_dashboard)
end
- condition(:split_operations_visibility_permissions) do
- ::Feature.enabled?(:split_operations_visibility_permissions, @subject)
- end
-
- rule { ~split_operations_visibility_permissions & operations_disabled }.policy do
- prevent(*create_read_update_admin_destroy(:feature_flag))
- prevent(*create_read_update_admin_destroy(:environment))
- prevent(*create_read_update_admin_destroy(:sentry_issue))
- prevent(*create_read_update_admin_destroy(:alert_management_alert))
- prevent(*create_read_update_admin_destroy(:cluster))
- prevent(*create_read_update_admin_destroy(:terraform_state))
- prevent(*create_read_update_admin_destroy(:deployment))
- prevent(:metrics_dashboard)
- prevent(:read_pod_logs)
- prevent(:read_prometheus)
- end
-
- rule { split_operations_visibility_permissions & environments_disabled }.policy do
+ rule { environments_disabled }.policy do
prevent(*create_read_update_admin_destroy(:environment))
prevent(*create_read_update_admin_destroy(:deployment))
end
- rule { split_operations_visibility_permissions & feature_flags_disabled }.policy do
+ rule { feature_flags_disabled }.policy do
prevent(*create_read_update_admin_destroy(:feature_flag))
prevent(:admin_feature_flags_user_lists)
prevent(:admin_feature_flags_client)
@@ -401,13 +384,13 @@ class ProjectPolicy < BasePolicy
prevent(*create_read_update_admin_destroy(:release))
end
- rule { split_operations_visibility_permissions & monitor_disabled }.policy do
+ rule { monitor_disabled }.policy do
prevent(:metrics_dashboard)
prevent(*create_read_update_admin_destroy(:sentry_issue))
prevent(*create_read_update_admin_destroy(:alert_management_alert))
end
- rule { split_operations_visibility_permissions & infrastructure_disabled }.policy do
+ rule { infrastructure_disabled }.policy do
prevent(*create_read_update_admin_destroy(:terraform_state))
prevent(*create_read_update_admin_destroy(:cluster))
prevent(:read_pod_logs)
@@ -552,15 +535,15 @@ class ProjectPolicy < BasePolicy
rule { can?(:push_code) }.enable :admin_tag
rule { archived }.policy do
- prevent(*readonly_abilities)
+ prevent(*archived_abilities)
- readonly_features.each do |feature|
+ archived_features.each do |feature|
prevent(*create_update_admin(feature))
end
end
rule { archived & ~pending_delete }.policy do
- readonly_features.each do |feature|
+ archived_features.each do |feature|
prevent(:"destroy_#{feature}")
end
end
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index 92dcfeed104..f25436c54be 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -98,7 +98,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
end
def permalink_path
- url_helpers.project_blob_path(project, File.join(project.repository.commit.sha, blob.path))
+ url_helpers.project_blob_path(project, File.join(project.repository.commit(blob.commit_id).sha, blob.path))
end
def environment_formatted_external_url
diff --git a/app/presenters/ci/freeze_period_presenter.rb b/app/presenters/ci/freeze_period_presenter.rb
new file mode 100644
index 00000000000..064197f34dd
--- /dev/null
+++ b/app/presenters/ci/freeze_period_presenter.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Ci::FreezePeriod, as: :freeze_period
+
+ def start_time
+ return freeze_period.time_start if freeze_period.active?
+
+ freeze_period.next_time_start
+ end
+ end
+end
diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb
index 88facc3608d..18554df4bd9 100644
--- a/app/presenters/group_member_presenter.rb
+++ b/app/presenters/group_member_presenter.rb
@@ -3,6 +3,10 @@
class GroupMemberPresenter < MemberPresenter
presents ::GroupMember
+ def last_owner?
+ member.last_owner_of_the_group?
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index 67d044dd01c..4cdaca3c39e 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -37,6 +37,10 @@ class MemberPresenter < Gitlab::View::Presenter::Delegated
false
end
+ def last_owner?
+ raise NotImplementedError
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/packages/pypi/simple_package_versions_presenter.rb b/app/presenters/packages/pypi/simple_package_versions_presenter.rb
index 0baa0714463..2bccaf4db72 100644
--- a/app/presenters/packages/pypi/simple_package_versions_presenter.rb
+++ b/app/presenters/packages/pypi/simple_package_versions_presenter.rb
@@ -13,7 +13,10 @@ module Packages
def links
refs = []
- available_packages.each_batch do |batch|
+ available_packages.each_batch do |relation|
+ batch = relation.preload_files
+ .preload_pypi_metadatum
+
batch.each do |package|
package_files = package.installable_package_files
diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb
index da24972775a..bb389b7a3ab 100644
--- a/app/presenters/project_member_presenter.rb
+++ b/app/presenters/project_member_presenter.rb
@@ -21,6 +21,12 @@ class ProjectMemberPresenter < MemberPresenter
super
end
+ def last_owner?
+ # all owners of a project in a group are removable.
+ # but in personal projects, the namespace holder is not removable.
+ member.holder_of_the_personal_namespace?
+ end
+
private
def admin_member_permission
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 0be13197343..4d1a9b3f589 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -68,7 +68,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
user_view = current_user.project_view
- if can?(current_user, :download_code, project)
+ if can?(current_user, :read_code, project)
user_view
elsif user_view == 'activity'
'activity'
@@ -179,7 +179,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
return if releases_count < 1
AnchorData.new(true,
- statistic_icon('rocket') +
+ statistic_icon('deployments') +
n_('%{strong_start}%{release_count}%{strong_end} Release', '%{strong_start}%{release_count}%{strong_end} Releases', releases_count).html_safe % {
release_count: number_with_delimiter(releases_count),
strong_start: '<strong class="project-stat-value">'.html_safe,
@@ -290,16 +290,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
'btn-default',
nil,
'license')
- else
- if can_current_user_push_to_default_branch?
- AnchorData.new(false,
+ elsif can_current_user_push_to_default_branch?
+ AnchorData.new(false,
content_tag(:span, statistic_icon + _('Add LICENSE'), class: 'add-license-link d-flex'),
empty_repo? ? add_license_ide_path : add_license_path)
- else
- AnchorData.new(false,
- icon + content_tag(:span, _('No license. All rights reserved'), class: 'project-stat-value'),
- nil)
- end
end
end
@@ -423,7 +417,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
end
def anonymous_project_view
- if !project.empty_repo? && can?(current_user, :download_code, project)
+ if !project.empty_repo? && can?(current_user, :read_code, project)
'files'
elsif project.wiki_repository_exists? && can?(current_user, :read_wiki, project)
'wiki'
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index 4755b88cbea..d7d959217b0 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -25,7 +25,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
case scope
when 'users'
- objects.eager_load(:status) if objects.respond_to?(:eager_load) # rubocop:disable CodeReuse/ActiveRecord
+ objects.respond_to?(:eager_load) ? objects.eager_load(:status) : objects # rubocop:disable CodeReuse/ActiveRecord
when 'commits'
prepare_commits_for_rendering(objects)
else
@@ -45,4 +45,10 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
def without_count?
search_objects.is_a?(Kaminari::PaginatableWithoutCount)
end
+
+ def advanced_search_enabled?
+ false
+ end
end
+
+SearchServicePresenter.prepend_mod_with('SearchServicePresenter')
diff --git a/app/serializers/analytics/cycle_analytics/configuration_entity.rb b/app/serializers/analytics/cycle_analytics/configuration_entity.rb
index 45ea7c92758..6a9ec3f5e9e 100644
--- a/app/serializers/analytics/cycle_analytics/configuration_entity.rb
+++ b/app/serializers/analytics/cycle_analytics/configuration_entity.rb
@@ -11,11 +11,7 @@ module Analytics
private
def events
- (stage_events.events - stage_events.internal_events).sort_by(&:name)
- end
-
- def stage_events
- Gitlab::Analytics::CycleAnalytics::StageEvents
+ Gitlab::Analytics::CycleAnalytics::StageEvents.selectable_events
end
end
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index dc7b5e95361..1caa9720c08 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -11,7 +11,7 @@ class BuildDetailsEntity < Ci::JobEntity
expose :metadata, using: BuildMetadataEntity
expose :pipeline, using: Ci::PipelineEntity
- expose :deployment_status, if: -> (*) { build.starts_environment? } do
+ expose :deployment_status, if: -> (*) { build.deployment_job? } do
expose :deployment_status, as: :status
expose :persisted_environment, as: :environment do |build, options|
options.merge(deployment_details: false).yield_self do |opts|
diff --git a/app/serializers/ci/basic_variable_entity.rb b/app/serializers/ci/basic_variable_entity.rb
index dad59e8735b..210c01408a6 100644
--- a/app/serializers/ci/basic_variable_entity.rb
+++ b/app/serializers/ci/basic_variable_entity.rb
@@ -9,5 +9,6 @@ module Ci
expose :protected?, as: :protected
expose :masked?, as: :masked
+ expose :raw?, as: :raw
end
end
diff --git a/app/serializers/issuable_sidebar_basic_entity.rb b/app/serializers/issuable_sidebar_basic_entity.rb
index b66aad6cc65..9039606a8e5 100644
--- a/app/serializers/issuable_sidebar_basic_entity.rb
+++ b/app/serializers/issuable_sidebar_basic_entity.rb
@@ -38,6 +38,10 @@ class IssuableSidebarBasicEntity < Grape::Entity
expose :can_admin_label do |issuable|
can?(current_user, :admin_label, issuable.project)
end
+
+ expose :can_create_timelogs do |issuable|
+ can?(current_user, :create_timelog, issuable)
+ end
end
expose :issuable_json_path do |issuable|
diff --git a/app/serializers/issue_entity.rb b/app/serializers/issue_entity.rb
index 3d94d2e2e9d..397f333008c 100644
--- a/app/serializers/issue_entity.rb
+++ b/app/serializers/issue_entity.rb
@@ -7,11 +7,15 @@ class IssueEntity < IssuableEntity
item.try(:upcase)
end
+ format_with(:iso8601) do |item|
+ item.try(:iso8601)
+ end
+
expose :state
expose :milestone_id
expose :updated_by_id
- expose :created_at
- expose :updated_at
+ expose :created_at, format_with: :iso8601
+ expose :updated_at, format_with: :iso8601
expose :milestone, using: API::Entities::Milestone
expose :labels, using: LabelEntity
expose :lock_version
@@ -85,6 +89,11 @@ class IssueEntity < IssuableEntity
end
expose :issue_email_participants do |issue|
+ # TODO - This is a Temporary solution to avoid leaking participants' emails
+ # on public/internal projects when issue is not confidential.
+ # Should be removed when https://gitlab.com/gitlab-org/gitlab/-/issues/383448 is implemented.
+ next [] unless issue.confidential?
+
issue.issue_email_participants.map { |x| { email: x.email } }
end
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index bfb5b3eeae6..8e5d352e413 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -23,6 +23,8 @@ class MemberEntity < Grape::Entity
member.can_remove?
end
+ expose :last_owner?, as: :is_last_owner
+
expose :is_direct_member do |member, options|
member.source == options[:source]
end
diff --git a/app/serializers/merge_request_metrics_entity.rb b/app/serializers/merge_request_metrics_entity.rb
index 1c9db08d103..ded82a9ef45 100644
--- a/app/serializers/merge_request_metrics_entity.rb
+++ b/app/serializers/merge_request_metrics_entity.rb
@@ -1,8 +1,12 @@
# frozen_string_literal: true
class MergeRequestMetricsEntity < Grape::Entity
- expose :latest_closed_at, as: :closed_at
- expose :merged_at
+ format_with(:iso8601) do |item|
+ item.try(:iso8601)
+ end
+
+ expose :latest_closed_at, as: :closed_at, format_with: :iso8601
+ expose :merged_at, format_with: :iso8601
expose :latest_closed_by, as: :closed_by, using: UserEntity
expose :merged_by, using: UserEntity
end
diff --git a/app/serializers/merge_request_poll_widget_entity.rb b/app/serializers/merge_request_poll_widget_entity.rb
index ab180b35b29..cef3f4555df 100644
--- a/app/serializers/merge_request_poll_widget_entity.rb
+++ b/app/serializers/merge_request_poll_widget_entity.rb
@@ -31,7 +31,7 @@ class MergeRequestPollWidgetEntity < Grape::Entity
end
expose :only_allow_merge_if_pipeline_succeeds do |merge_request|
- merge_request.project.only_allow_merge_if_pipeline_succeeds?
+ merge_request.project.only_allow_merge_if_pipeline_succeeds?(inherit_group_setting: true)
end
# CI related
diff --git a/app/serializers/project_entity.rb b/app/serializers/project_entity.rb
index 77e2115fbe2..cbdc19a83ce 100644
--- a/app/serializers/project_entity.rb
+++ b/app/serializers/project_entity.rb
@@ -13,4 +13,8 @@ class ProjectEntity < Grape::Entity
expose :full_name, documentation: { type: 'string', example: 'GitLab Org / GitLab' } do |project|
project.full_name
end
+
+ expose :refs_url do |project|
+ refs_project_path(project)
+ end
end
diff --git a/app/services/admin/set_feature_flag_service.rb b/app/services/admin/set_feature_flag_service.rb
index d72a18a6a58..3378be7eddd 100644
--- a/app/services/admin/set_feature_flag_service.rb
+++ b/app/services/admin/set_feature_flag_service.rb
@@ -2,82 +2,143 @@
module Admin
class SetFeatureFlagService
+ UnknownOperationError = Class.new(StandardError)
+
def initialize(feature_flag_name:, params:)
@name = feature_flag_name
+ @target = Feature::Target.new(params)
@params = params
+ @force = params[:force]
end
def execute
- unless params[:force]
+ unless force
error = validate_feature_flag_name
return ServiceResponse.error(message: error, reason: :invalid_feature_flag) if error
end
- flag_target = Feature::Target.new(params)
- value = gate_value(params)
-
- case value
- when true
- enable!(flag_target)
- when false
- disable!(flag_target)
+ if target.gate_specified?
+ update_targets
else
- enable_partially!(value, params)
+ update_global
end
feature_flag = Feature.get(name) # rubocop:disable Gitlab/AvoidFeatureGet
ServiceResponse.success(payload: { feature_flag: feature_flag })
- rescue Feature::Target::UnknowTargetError => e
+ rescue Feature::InvalidOperation => e
+ ServiceResponse.error(message: e.message, reason: :illegal_operation)
+ rescue UnknownOperationError => e
+ ServiceResponse.error(message: e.message, reason: :illegal_operation)
+ rescue Feature::Target::UnknownTargetError => e
ServiceResponse.error(message: e.message, reason: :actor_not_found)
end
private
- attr_reader :name, :params
+ attr_reader :name, :params, :target, :force
- def enable!(flag_target)
- if flag_target.gate_specified?
- flag_target.targets.each { |target| Feature.enable(name, target) }
- else
- Feature.enable(name)
+ # Note: the if expressions in `update_targets` and `update_global` are order dependant.
+ def update_targets
+ target.targets.each do |target|
+ if enable?
+ enable(target)
+ elsif disable?
+ Feature.disable(name, target)
+ elsif opt_out?
+ Feature.opt_out(name, target)
+ elsif remove_opt_out?
+ remove_opt_out(target)
+ else
+ raise UnknownOperationError, "Cannot set '#{name}' to #{value.inspect} for #{target}"
+ end
end
end
- def disable!(flag_target)
- if flag_target.gate_specified?
- flag_target.targets.each { |target| Feature.disable(name, target) }
- else
+ def update_global
+ if enable?
+ Feature.enable(name)
+ elsif disable?
Feature.disable(name)
+ elsif percentage_of_actors?
+ Feature.enable_percentage_of_actors(name, percentage)
+ elsif percentage_of_time?
+ Feature.enable_percentage_of_time(name, percentage)
+ else
+ msg = if key.present?
+ "Cannot set '#{name}' (#{key.inspect}) to #{value.inspect}"
+ else
+ "Cannot set '#{name}' to #{value.inspect}"
+ end
+
+ raise UnknownOperationError, msg
end
end
- def enable_partially!(value, params)
- if params[:key] == 'percentage_of_actors'
- Feature.enable_percentage_of_actors(name, value)
- else
- Feature.enable_percentage_of_time(name, value)
+ def remove_opt_out(target)
+ raise Feature::InvalidOperation, "No opt-out exists for #{target}" unless Feature.opted_out?(name, target)
+
+ Feature.remove_opt_out(name, target)
+ end
+
+ def enable(target)
+ if Feature.opted_out?(name, target)
+ target_name = target.respond_to?(:to_reference) ? target.to_reference : target.to_s
+ raise Feature::InvalidOperation, "Opt-out exists for #{target_name} - remove opt-out before enabling"
end
+
+ Feature.enable(name, target)
end
- def validate_feature_flag_name
- # overridden in EE
+ def value
+ params[:value]
end
- def gate_value(params)
- case params[:value]
- when 'true'
- true
- when '0', 'false'
- false
- else
- # https://github.com/jnunemaker/flipper/blob/master/lib/flipper/typecast.rb#L47
- if params[:value].to_s.include?('.')
- params[:value].to_f
- else
- params[:value].to_i
- end
- end
+ def key
+ params[:key]
+ end
+
+ def numeric_value?
+ params[:value].match?(/^\d+(\.\d+)?$/)
+ end
+
+ def percentage
+ raise UnknownOperationError, "Not a percentage" unless numeric_value?
+
+ value.to_f
+ end
+
+ def percentage_of_actors?
+ key == 'percentage_of_actors'
+ end
+
+ def percentage_of_time?
+ return true if key == 'percentage_of_time'
+ return numeric_value? if key.nil?
+
+ false
+ end
+
+ # Note: `key` is NOT considered - setting to a percentage to 0 is the same as disabling.
+ def disable?
+ value.in?(%w[0 0.0 false])
+ end
+
+ # Note: `key` is NOT considered - setting to a percentage to 100 is the same
+ def enable?
+ value.in?(%w[100 100.0 true])
+ end
+
+ def opt_out?
+ value == 'opt_out'
+ end
+
+ def remove_opt_out?
+ value == 'remove_opt_out'
+ end
+
+ def validate_feature_flag_name
+ ## Overridden in EE
end
end
end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
index d3c6dcca588..124b5964232 100644
--- a/app/services/bulk_imports/create_service.rb
+++ b/app/services/bulk_imports/create_service.rb
@@ -57,11 +57,14 @@ module BulkImports
bulk_import = BulkImport.create!(
user: current_user,
source_type: 'gitlab',
- source_version: client.instance_version
+ source_version: client.instance_version,
+ source_enterprise: client.instance_enterprise
)
bulk_import.create_configuration!(credentials.slice(:url, :access_token))
Array.wrap(params).each do |entity|
+ track_access_level(entity)
+
BulkImports::Entity.create!(
bulk_import: bulk_import,
source_type: entity[:source_type],
@@ -75,6 +78,34 @@ module BulkImports
end
end
+ def track_access_level(entity)
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create',
+ label: 'import_access_level',
+ user: current_user,
+ extra: { user_role: user_role(entity[:destination_namespace]), import_type: 'bulk_import_group' }
+ )
+ end
+
+ def user_role(destination_namespace)
+ namespace = Namespace.find_by_full_path(destination_namespace)
+ # if there is no parent namespace we assume user will be group creator/owner
+ return owner_role unless destination_namespace
+ return owner_role unless namespace
+ return owner_role unless namespace.group_namespace? # user namespace
+
+ membership = current_user.group_members.find_by(source_id: namespace.id) # rubocop:disable CodeReuse/ActiveRecord
+
+ return 'Not a member' unless membership
+
+ Gitlab::Access.human_access(membership.access_level)
+ end
+
+ def owner_role
+ Gitlab::Access.human_access(Gitlab::Access::OWNER)
+ end
+
def client
@client ||= BulkImports::Clients::HTTP.new(
url: @credentials[:url],
diff --git a/app/services/bulk_imports/file_download_service.rb b/app/services/bulk_imports/file_download_service.rb
index 45f1350df92..ee499c782b4 100644
--- a/app/services/bulk_imports/file_download_service.rb
+++ b/app/services/bulk_imports/file_download_service.rb
@@ -31,14 +31,13 @@ module BulkImports
@tmpdir = tmpdir
@file_size_limit = file_size_limit
@allowed_content_types = allowed_content_types
+ @remote_content_validated = false
end
def execute
validate_tmpdir
validate_filepath
validate_url
- validate_content_type
- validate_content_length
download_file
@@ -49,7 +48,7 @@ module BulkImports
private
- attr_reader :configuration, :relative_url, :tmpdir, :file_size_limit, :allowed_content_types
+ attr_reader :configuration, :relative_url, :tmpdir, :file_size_limit, :allowed_content_types, :response_headers
def download_file
File.open(filepath, 'wb') do |file|
@@ -58,6 +57,15 @@ module BulkImports
http_client.stream(relative_url) do |chunk|
next if bytes_downloaded == 0 && [301, 302, 303, 307, 308].include?(chunk.code)
+ @response_headers ||= Gitlab::HTTP::Response::Headers.new(chunk.http_response.to_hash)
+
+ unless @remote_content_validated
+ validate_content_type
+ validate_content_length
+
+ @remote_content_validated = true
+ end
+
bytes_downloaded += chunk.size
validate_size!(bytes_downloaded)
@@ -90,10 +98,6 @@ module BulkImports
::Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
- def response_headers
- @response_headers ||= http_client.head(relative_url).headers
- end
-
def validate_tmpdir
Gitlab::Utils.check_allowed_absolute_path!(tmpdir, [Dir.tmpdir])
end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index 3dd3ba7f01c..3b204d51bab 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -2,9 +2,9 @@
module ChatNames
class FindUserService
- def initialize(integration, params)
- @integration = integration
- @params = params
+ def initialize(team_id, user_id)
+ @team_id = team_id
+ @user_id = user_id
end
def execute
@@ -17,12 +17,13 @@ module ChatNames
private
+ attr_reader :team_id, :user_id
+
# rubocop: disable CodeReuse/ActiveRecord
def find_chat_name
ChatName.find_by(
- integration: @integration,
- team_id: @params[:team_id],
- chat_id: @params[:user_id]
+ team_id: team_id,
+ chat_id: user_id
)
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/append_build_trace_service.rb b/app/services/ci/append_build_trace_service.rb
index 0eef0ff0e61..4432eecc24a 100644
--- a/app/services/ci/append_build_trace_service.rb
+++ b/app/services/ci/append_build_trace_service.rb
@@ -24,6 +24,11 @@ module Ci
body_start = content_range[0].to_i
body_end = body_start + body_data.bytesize
+ if first_debug_chunk?(body_start)
+ # Update the build metadata prior to appending trace content
+ build.enable_debug_trace!
+ end
+
if trace_size_exceeded?(body_end)
build.drop(:trace_size_exceeded)
@@ -45,10 +50,18 @@ module Ci
delegate :project, to: :build
+ def first_debug_chunk?(body_start)
+ body_start == 0 && debug_trace
+ end
+
def stream_range
params.fetch(:content_range)
end
+ def debug_trace
+ params.fetch(:debug_trace, false)
+ end
+
def log_range_error(stream_size, body_end)
extra = {
build_id: build.id,
diff --git a/app/services/ci/create_downstream_pipeline_service.rb b/app/services/ci/create_downstream_pipeline_service.rb
index 25cc9045052..3d0a7fb99ea 100644
--- a/app/services/ci/create_downstream_pipeline_service.rb
+++ b/app/services/ci/create_downstream_pipeline_service.rb
@@ -11,24 +11,25 @@ module Ci
DuplicateDownstreamPipelineError = Class.new(StandardError)
MAX_NESTED_CHILDREN = 2
- MAX_HIERARCHY_SIZE = 1000
def execute(bridge)
@bridge = bridge
- if bridge.has_downstream_pipeline?
+ if @bridge.has_downstream_pipeline?
Gitlab::ErrorTracking.track_exception(
DuplicateDownstreamPipelineError.new,
bridge_id: @bridge.id, project_id: @bridge.project_id
)
- return error('Already has a downstream pipeline')
+ return ServiceResponse.error(message: 'Already has a downstream pipeline')
end
pipeline_params = @bridge.downstream_pipeline_params
target_ref = pipeline_params.dig(:target_revision, :ref)
- return error('Pre-conditions not met') unless ensure_preconditions!(target_ref)
+ return ServiceResponse.error(message: 'Pre-conditions not met') unless ensure_preconditions!(target_ref)
+
+ return ServiceResponse.error(message: 'Can not run the bridge') unless @bridge.run
service = ::Ci::CreatePipelineService.new(
pipeline_params.fetch(:project),
@@ -40,10 +41,7 @@ module Ci
.payload
log_downstream_pipeline_creation(downstream_pipeline)
-
- downstream_pipeline.tap do |pipeline|
- update_bridge_status!(@bridge, pipeline)
- end
+ update_bridge_status!(@bridge, downstream_pipeline)
end
private
@@ -54,9 +52,12 @@ module Ci
# If bridge uses `strategy:depend` we leave it running
# and update the status when the downstream pipeline completes.
subject.success! unless subject.dependent?
+ ServiceResponse.success(payload: pipeline)
else
- subject.options[:downstream_errors] = pipeline.errors.full_messages
+ message = pipeline.errors.full_messages
+ subject.options[:downstream_errors] = message
subject.drop!(:downstream_pipeline_creation_failed)
+ ServiceResponse.error(payload: pipeline, message: message)
end
end
rescue StateMachines::InvalidTransition => e
@@ -64,6 +65,7 @@ module Ci
Ci::Bridge::InvalidTransitionError.new(e.message),
bridge_id: bridge.id,
downstream_pipeline_id: pipeline.id)
+ ServiceResponse.error(payload: pipeline, message: e.message)
end
def ensure_preconditions!(target_ref)
@@ -151,7 +153,13 @@ module Ci
return false unless @bridge.triggers_downstream_pipeline?
# Applies to the entire pipeline tree across all projects
- @bridge.pipeline.complete_hierarchy_count >= MAX_HIERARCHY_SIZE
+ # A pipeline tree can be shared between multiple namespaces (customers), the limit that is used here
+ # is the limit of the namespace that has added a downstream pipeline to a pipeline tree.
+ @bridge.project.actual_limits.exceeded?(:pipeline_hierarchy_size, complete_hierarchy_count)
+ end
+
+ def complete_hierarchy_count
+ @bridge.pipeline.complete_hierarchy_count
end
def config_checksum(pipeline)
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 4106dfe0ecc..9c3cc803587 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -4,8 +4,6 @@ module Ci
class CreatePipelineService < BaseService
attr_reader :pipeline, :logger
- CreateError = Class.new(StandardError)
-
LOG_MAX_DURATION_THRESHOLD = 3.seconds
LOG_MAX_PIPELINE_SIZE = 2_000
LOG_MAX_CREATION_THRESHOLD = 20.seconds
@@ -140,25 +138,24 @@ module Ci
def build_logger
Gitlab::Ci::Pipeline::Logger.new(project: project) do |l|
l.log_when do |observations|
- observations.any? do |name, values|
- values.any? &&
+ observations.any? do |name, observation|
name.to_s.end_with?('duration_s') &&
- values.max >= LOG_MAX_DURATION_THRESHOLD
+ Array(observation).max >= LOG_MAX_DURATION_THRESHOLD
end
end
l.log_when do |observations|
- values = observations['pipeline_size_count']
- next false if values.empty?
+ count = observations['pipeline_size_count']
+ next false unless count
- values.max >= LOG_MAX_PIPELINE_SIZE
+ count >= LOG_MAX_PIPELINE_SIZE
end
l.log_when do |observations|
- values = observations['pipeline_creation_duration_s']
- next false if values.empty?
+ duration = observations['pipeline_creation_duration_s']
+ next false unless duration
- values.max >= LOG_MAX_CREATION_THRESHOLD
+ duration >= LOG_MAX_CREATION_THRESHOLD
end
end
end
diff --git a/app/services/ci/enqueue_job_service.rb b/app/services/ci/enqueue_job_service.rb
new file mode 100644
index 00000000000..9e3bea3fd28
--- /dev/null
+++ b/app/services/ci/enqueue_job_service.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Ci
+ class EnqueueJobService
+ attr_accessor :job, :current_user, :variables
+
+ def initialize(job, current_user:, variables: nil)
+ @job = job
+ @current_user = current_user
+ @variables = variables
+ end
+
+ def execute(&transition)
+ job.user = current_user
+ job.job_variables_attributes = variables if variables
+
+ transition ||= ->(job) { job.enqueue! }
+ Gitlab::OptimisticLocking.retry_lock(job, name: 'ci_enqueue_job', &transition)
+
+ ResetSkippedJobsService.new(job.project, current_user).execute(job)
+
+ job
+ end
+ end
+end
diff --git a/app/services/ci/generate_kubeconfig_service.rb b/app/services/ci/generate_kubeconfig_service.rb
index 347bc99dbf5..1c6aaa9d1ff 100644
--- a/app/services/ci/generate_kubeconfig_service.rb
+++ b/app/services/ci/generate_kubeconfig_service.rb
@@ -2,9 +2,11 @@
module Ci
class GenerateKubeconfigService
- def initialize(pipeline, token:)
+ def initialize(pipeline, token:, environment:)
@pipeline = pipeline
@token = token
+ @environment = environment
+
@template = Gitlab::Kubernetes::Kubeconfig::Template.new
end
@@ -36,10 +38,13 @@ module Ci
private
- attr_reader :pipeline, :token, :template
+ attr_reader :pipeline, :token, :environment, :template
def agent_authorizations
- pipeline.cluster_agent_authorizations
+ ::Clusters::Agents::FilterAuthorizationsService.new(
+ pipeline.cluster_agent_authorizations,
+ environment: environment
+ ).execute
end
def cluster_name
diff --git a/app/services/ci/job_artifacts/create_service.rb b/app/services/ci/job_artifacts/create_service.rb
index 3dc097a8603..ee9982cf3ab 100644
--- a/app/services/ci/job_artifacts/create_service.rb
+++ b/app/services/ci/job_artifacts/create_service.rb
@@ -16,7 +16,7 @@ module Ci
def initialize(job)
@job = job
@project = job.project
- @pipeline = job.pipeline if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, @project)
+ @pipeline = job.pipeline
end
def authorize(artifact_type:, filesize: nil)
@@ -85,7 +85,7 @@ module Ci
expire_in: expire_in
}
- artifact_attributes[:locked] = pipeline.locked if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, project)
+ artifact_attributes[:locked] = pipeline.locked
artifact = Ci::JobArtifact.new(
artifact_attributes.merge(
diff --git a/app/services/ci/pipeline_schedule_service.rb b/app/services/ci/pipeline_schedule_service.rb
index 536eaa56f9b..d320382d19f 100644
--- a/app/services/ci/pipeline_schedule_service.rb
+++ b/app/services/ci/pipeline_schedule_service.rb
@@ -8,7 +8,7 @@ module Ci
# Ensure `next_run_at` is set properly before creating a pipeline.
# Otherwise, multiple pipelines could be created in a short interval.
schedule.schedule_next_run!
- RunPipelineScheduleWorker.perform_async(schedule.id, schedule.owner&.id)
+ RunPipelineScheduleWorker.perform_async(schedule.id, current_user&.id)
end
end
end
diff --git a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb
index 6c8ccb017e9..a1b9ab5f82e 100644
--- a/app/services/ci/pipeline_schedules/calculate_next_run_service.rb
+++ b/app/services/ci/pipeline_schedules/calculate_next_run_service.rb
@@ -35,7 +35,7 @@ module Ci
def worker_cron
strong_memoize(:worker_cron) do
- Gitlab::Ci::CronParser.new(worker_cron_expression, Time.zone.name)
+ Gitlab::Ci::CronParser.new(@schedule.worker_cron_expression, Time.zone.name)
end
end
@@ -50,10 +50,6 @@ module Ci
Gitlab::Ci::CronParser.parse_natural("every #{every_x_minutes} minutes", Time.zone.name)
end
end
-
- def worker_cron_expression
- Settings.cron_jobs['pipeline_schedule_worker']['cron']
- end
end
end
end
diff --git a/app/services/ci/play_bridge_service.rb b/app/services/ci/play_bridge_service.rb
index a719467253e..ffcd2b05b31 100644
--- a/app/services/ci/play_bridge_service.rb
+++ b/app/services/ci/play_bridge_service.rb
@@ -5,12 +5,7 @@ module Ci
def execute(bridge)
check_access!(bridge)
- bridge.tap do |bridge|
- bridge.user = current_user
- bridge.enqueue!
-
- AfterRequeueJobService.new(project, current_user).execute(bridge)
- end
+ Ci::EnqueueJobService.new(bridge, current_user: current_user).execute
end
private
diff --git a/app/services/ci/play_build_service.rb b/app/services/ci/play_build_service.rb
index b7aec57f3e3..8d2225aba71 100644
--- a/app/services/ci/play_build_service.rb
+++ b/app/services/ci/play_build_service.rb
@@ -5,17 +5,7 @@ module Ci
def execute(build, job_variables_attributes = nil)
check_access!(build, job_variables_attributes)
- if build.can_enqueue?
- build.user = current_user
- build.job_variables_attributes = job_variables_attributes || []
- build.enqueue!
-
- AfterRequeueJobService.new(project, current_user).execute(build)
-
- build
- else
- retry_build(build)
- end
+ Ci::EnqueueJobService.new(build, current_user: current_user, variables: job_variables_attributes || []).execute
rescue StateMachines::InvalidTransition
retry_build(build.reset)
end
diff --git a/app/services/ci/process_build_service.rb b/app/services/ci/process_build_service.rb
index cb51d918fc2..a5300cfd29f 100644
--- a/app/services/ci/process_build_service.rb
+++ b/app/services/ci/process_build_service.rb
@@ -15,7 +15,7 @@ module Ci
private
def process(build)
- return enqueue(build) if Feature.enabled?(:ci_retry_job_fix, project) && build.enqueue_immediately?
+ return enqueue(build) if build.enqueue_immediately?
if build.schedulable?
build.schedule
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index f11577feb88..cd879e9bc07 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -108,15 +108,13 @@ module Ci
def each_build(params, &blk)
queue = ::Ci::Queue::BuildQueueService.new(runner)
- builds = begin
- if runner.instance_type?
- queue.builds_for_shared_runner
- elsif runner.group_type?
- queue.builds_for_group_runner
- else
- queue.builds_for_project_runner
- end
- end
+ builds = if runner.instance_type?
+ queue.builds_for_shared_runner
+ elsif runner.group_type?
+ queue.builds_for_group_runner
+ else
+ queue.builds_for_project_runner
+ end
if runner.ref_protected?
builds = queue.builds_for_protected_runner(builds)
diff --git a/app/services/ci/after_requeue_job_service.rb b/app/services/ci/reset_skipped_jobs_service.rb
index 4374ccd52e0..eb809b0162c 100644
--- a/app/services/ci/after_requeue_job_service.rb
+++ b/app/services/ci/reset_skipped_jobs_service.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
module Ci
- class AfterRequeueJobService < ::BaseService
+ # This service resets skipped jobs so they can be processed again.
+ # It affects the jobs that depend on the passed in job parameter.
+ class ResetSkippedJobsService < ::BaseService
def execute(processable)
@processable = processable
diff --git a/app/services/ci/retry_job_service.rb b/app/services/ci/retry_job_service.rb
index 74ebaef48b1..da0e80dfed7 100644
--- a/app/services/ci/retry_job_service.rb
+++ b/app/services/ci/retry_job_service.rb
@@ -28,7 +28,7 @@ module Ci
check_access!(job)
new_job = job.clone(current_user: current_user, new_job_variables_attributes: variables)
- if Feature.enabled?(:ci_retry_job_fix, project) && enqueue_if_actionable && new_job.action?
+ if enqueue_if_actionable && new_job.action?
new_job.set_enqueue_immediately!
end
@@ -64,15 +64,10 @@ module Ci
next if new_job.failed?
- Gitlab::OptimisticLocking.retry_lock(new_job, name: 'retry_build', &:enqueue) if Feature.disabled?(
- :ci_retry_job_fix, project)
+ ResetSkippedJobsService.new(project, current_user).execute(job)
- AfterRequeueJobService.new(project, current_user).execute(job)
-
- if Feature.enabled?(:ci_retry_job_fix, project)
- Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
- new_job.reset
- end
+ Ci::PipelineCreation::StartPipelineService.new(job.pipeline).execute
+ new_job.reset
end
end
diff --git a/app/services/ci/test_failure_history_service.rb b/app/services/ci/test_failure_history_service.rb
index 5a8072b2a0d..4f794b3d671 100644
--- a/app/services/ci/test_failure_history_service.rb
+++ b/app/services/ci/test_failure_history_service.rb
@@ -103,7 +103,8 @@ module Ci
{
unit_test_id: ci_unit_test.id,
build_id: build.id,
- failed_at: build.finished_at
+ failed_at: build.finished_at,
+ partition_id: build.partition_id
}
end
end
diff --git a/app/services/ci/track_failed_build_service.rb b/app/services/ci/track_failed_build_service.rb
index caf7034234c..973c43a9445 100644
--- a/app/services/ci/track_failed_build_service.rb
+++ b/app/services/ci/track_failed_build_service.rb
@@ -6,7 +6,7 @@
# @param exit_code [Int] the resulting exit code.
module Ci
class TrackFailedBuildService
- SCHEMA_URL = 'iglu:com.gitlab/ci_build_failed/jsonschema/1-0-0'
+ SCHEMA_URL = 'iglu:com.gitlab/ci_build_failed/jsonschema/1-0-1'
def initialize(build:, exit_code:, failure_reason:)
@build = build
@@ -42,7 +42,8 @@ module Ci
build_name: @build.name,
build_artifact_types: @build.job_artifact_types,
exit_code: @exit_code,
- failure_reason: @failure_reason
+ failure_reason: @failure_reason,
+ project: @build.project_id
}
end
end
diff --git a/app/services/ci/unlock_artifacts_service.rb b/app/services/ci/unlock_artifacts_service.rb
index 574cdae6480..237f1997edb 100644
--- a/app/services/ci/unlock_artifacts_service.rb
+++ b/app/services/ci/unlock_artifacts_service.rb
@@ -11,42 +11,21 @@ module Ci
unlocked_pipeline_artifacts: 0
}
- if ::Feature.enabled?(:ci_update_unlocked_job_artifacts, ci_ref.project)
- loop do
- unlocked_pipelines = []
- unlocked_job_artifacts = []
+ loop do
+ unlocked_pipelines = []
+ unlocked_job_artifacts = []
- ::Ci::Pipeline.transaction do
- unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
- unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)
+ ::Ci::Pipeline.transaction do
+ unlocked_pipelines = unlock_pipelines(ci_ref, before_pipeline)
+ unlocked_job_artifacts = unlock_job_artifacts(unlocked_pipelines)
- results[:unlocked_pipeline_artifacts] += unlock_pipeline_artifacts(unlocked_pipelines)
- end
+ results[:unlocked_pipeline_artifacts] += unlock_pipeline_artifacts(unlocked_pipelines)
+ end
- break if unlocked_pipelines.empty?
+ break if unlocked_pipelines.empty?
- results[:unlocked_pipelines] += unlocked_pipelines.length
- results[:unlocked_job_artifacts] += unlocked_job_artifacts.length
- end
- else
- query = <<~SQL.squish
- UPDATE "ci_pipelines"
- SET "locked" = #{::Ci::Pipeline.lockeds[:unlocked]}
- WHERE "ci_pipelines"."id" in (
- #{collect_pipelines(ci_ref, before_pipeline).select(:id).to_sql}
- LIMIT #{BATCH_SIZE}
- FOR UPDATE SKIP LOCKED
- )
- RETURNING "ci_pipelines"."id";
- SQL
-
- loop do
- unlocked_pipelines = Ci::Pipeline.connection.exec_query(query)
-
- break if unlocked_pipelines.empty?
-
- results[:unlocked_pipelines] += unlocked_pipelines.length
- end
+ results[:unlocked_pipelines] += unlocked_pipelines.length
+ results[:unlocked_job_artifacts] += unlocked_job_artifacts.length
end
results
@@ -88,13 +67,6 @@ module Ci
private
- def collect_pipelines(ci_ref, before_pipeline)
- pipeline_scope = ci_ref.pipelines
- pipeline_scope = pipeline_scope.before_pipeline(before_pipeline) if before_pipeline
-
- pipeline_scope.artifacts_locked
- end
-
def unlock_job_artifacts(pipelines)
return if pipelines.empty?
diff --git a/app/services/clusters/agents/filter_authorizations_service.rb b/app/services/clusters/agents/filter_authorizations_service.rb
new file mode 100644
index 00000000000..68517ceec04
--- /dev/null
+++ b/app/services/clusters/agents/filter_authorizations_service.rb
@@ -0,0 +1,50 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class FilterAuthorizationsService
+ def initialize(authorizations, filter_params)
+ @authorizations = authorizations
+ @filter_params = filter_params
+
+ @environments_matcher = {}
+ end
+
+ def execute
+ filter_by_environment(authorizations)
+ end
+
+ private
+
+ attr_reader :authorizations, :filter_params
+
+ def filter_by_environment(auths)
+ return auths unless filter_by_environment?
+
+ auths.select do |auth|
+ next true if auth.config['environments'].blank?
+
+ auth.config['environments'].any? { |environment_pattern| matches_environment?(environment_pattern) }
+ end
+ end
+
+ def filter_by_environment?
+ filter_params.has_key?(:environment)
+ end
+
+ def environment_filter
+ @environment_filter ||= filter_params[:environment]
+ end
+
+ def matches_environment?(environment_pattern)
+ return false if environment_filter.nil?
+
+ environments_matcher(environment_pattern).match?(environment_filter)
+ end
+
+ def environments_matcher(environment_pattern)
+ @environments_matcher[environment_pattern] ||= ::Gitlab::Ci::EnvironmentMatcher.new(environment_pattern)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb
index 54b90a7304c..53b14ab54da 100644
--- a/app/services/clusters/agents/refresh_authorization_service.rb
+++ b/app/services/clusters/agents/refresh_authorization_service.rb
@@ -83,11 +83,7 @@ module Clusters
end
def allowed_projects
- if group_root_ancestor?
- root_ancestor.all_projects
- else
- ::Project.id_in(project.id)
- end
+ root_ancestor.all_projects
end
def allowed_groups
diff --git a/app/services/clusters/applications/base_service.rb b/app/services/clusters/applications/base_service.rb
deleted file mode 100644
index c6f22cfa04c..00000000000
--- a/app/services/clusters/applications/base_service.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class BaseService
- InvalidApplicationError = Class.new(StandardError)
-
- attr_reader :cluster, :current_user, :params
-
- def initialize(cluster, user, params = {})
- @cluster = cluster
- @current_user = user
- @params = params.dup
- end
-
- def execute(request)
- instantiate_application.tap do |application|
- if application.has_attribute?(:hostname)
- application.hostname = params[:hostname]
- end
-
- if application.has_attribute?(:email)
- application.email = params[:email]
- end
-
- if application.has_attribute?(:stack)
- application.stack = params[:stack]
- end
-
- if application.respond_to?(:oauth_application)
- application.oauth_application = create_oauth_application(application, request)
- end
-
- if application.instance_of?(Knative)
- Serverless::AssociateDomainService
- .new(application, pages_domain_id: params[:pages_domain_id], creator: current_user)
- .execute
- end
-
- worker = worker_class(application)
-
- application.make_scheduled!
-
- worker.perform_async(application.name, application.id)
- end
- end
-
- protected
-
- def worker_class(application)
- raise NotImplementedError
- end
-
- def builder
- raise NotImplementedError
- end
-
- def project_builders
- raise NotImplementedError
- end
-
- def instantiate_application
- raise_invalid_application_error if unknown_application?
-
- builder || raise(InvalidApplicationError, "invalid application: #{application_name}")
- end
-
- def raise_invalid_application_error
- raise(InvalidApplicationError, "invalid application: #{application_name}")
- end
-
- def unknown_application?
- Clusters::Cluster::APPLICATIONS.keys.exclude?(application_name)
- end
-
- def application_name
- params[:application]
- end
-
- def application_class
- Clusters::Cluster::APPLICATIONS[application_name]
- end
-
- def create_oauth_application(application, request)
- oauth_application_params = {
- name: params[:application],
- redirect_uri: application.callback_url,
- scopes: application.oauth_scopes,
- owner: current_user
- }
-
- ::Applications::CreateService.new(current_user, oauth_application_params).execute(request)
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/check_progress_service.rb b/app/services/clusters/applications/check_progress_service.rb
deleted file mode 100644
index 4a07b955f8e..00000000000
--- a/app/services/clusters/applications/check_progress_service.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class CheckProgressService < BaseHelmService
- def execute
- return unless operation_in_progress?
-
- case pod_phase
- when Gitlab::Kubernetes::Pod::SUCCEEDED
- on_success
- when Gitlab::Kubernetes::Pod::FAILED
- on_failed
- else
- check_timeout
- end
- rescue Kubeclient::HttpError => e
- log_error(e)
-
- app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
- end
-
- private
-
- def operation_in_progress?
- raise NotImplementedError
- end
-
- def on_success
- raise NotImplementedError
- end
-
- def pod_name
- raise NotImplementedError
- end
-
- def on_failed
- app.make_errored!(_('Operation failed. Check pod logs for %{pod_name} for more details.') % { pod_name: pod_name })
- end
-
- def timed_out?
- raise NotImplementedError
- end
-
- def pod_phase
- helm_api.status(pod_name)
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/install_service.rb b/app/services/clusters/applications/install_service.rb
deleted file mode 100644
index dffb4ce65ab..00000000000
--- a/app/services/clusters/applications/install_service.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class InstallService < BaseHelmService
- def execute
- return unless app.scheduled?
-
- app.make_installing!
-
- install
- end
-
- private
-
- def install
- log_event(:begin_install)
- helm_api.install(install_command)
-
- log_event(:schedule_wait_for_installation)
- ClusterWaitForAppInstallationWorker.perform_in(
- ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError => e
- log_error(e)
- app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
- rescue StandardError => e
- log_error(e)
- app.make_errored!(_('Failed to install.'))
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/prometheus_config_service.rb b/app/services/clusters/applications/prometheus_config_service.rb
deleted file mode 100644
index d39d63c874f..00000000000
--- a/app/services/clusters/applications/prometheus_config_service.rb
+++ /dev/null
@@ -1,155 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class PrometheusConfigService
- def initialize(project, cluster, app)
- @project = project
- @cluster = cluster
- @app = app
- end
-
- def execute(config = {})
- if has_alerts?
- generate_alert_manager(config)
- else
- reset_alert_manager(config)
- end
- end
-
- private
-
- attr_reader :project, :cluster, :app
-
- def reset_alert_manager(config)
- config = set_alert_manager_enabled(config, false)
- config.delete('alertmanagerFiles')
- config['serverFiles'] ||= {}
- config['serverFiles']['alerts'] = {}
-
- config
- end
-
- def generate_alert_manager(config)
- config = set_alert_manager_enabled(config, true)
- config = set_alert_manager_files(config)
-
- set_alert_manager_groups(config)
- end
-
- def set_alert_manager_enabled(config, enabled)
- config['alertmanager'] ||= {}
- config['alertmanager']['enabled'] = enabled
-
- config
- end
-
- def set_alert_manager_files(config)
- config['alertmanagerFiles'] = {
- 'alertmanager.yml' => {
- 'receivers' => alert_manager_receivers_params,
- 'route' => alert_manager_route_params
- }
- }
-
- config
- end
-
- def set_alert_manager_groups(config)
- config['serverFiles'] ||= {}
- config['serverFiles']['alerts'] ||= {}
- config['serverFiles']['alerts']['groups'] ||= []
-
- environments_with_alerts.each do |env_name, alerts|
- index = config['serverFiles']['alerts']['groups'].find_index do |group|
- group['name'] == env_name
- end
-
- if index
- config['serverFiles']['alerts']['groups'][index]['rules'] = alerts
- else
- config['serverFiles']['alerts']['groups'] << {
- 'name' => env_name,
- 'rules' => alerts
- }
- end
- end
-
- config
- end
-
- def alert_manager_receivers_params
- [
- {
- 'name' => 'gitlab',
- 'webhook_configs' => [
- {
- 'url' => notify_url,
- 'send_resolved' => true,
- 'http_config' => {
- 'bearer_token' => alert_manager_token
- }
- }
- ]
- }
- ]
- end
-
- def alert_manager_token
- app.alert_manager_token
- end
-
- def alert_manager_route_params
- {
- 'receiver' => 'gitlab',
- 'group_wait' => '30s',
- 'group_interval' => '5m',
- 'repeat_interval' => '4h'
- }
- end
-
- def notify_url
- ::Gitlab::Routing.url_helpers
- .notify_project_prometheus_alerts_url(project, format: :json)
- end
-
- def has_alerts?
- environments_with_alerts.values.flatten(1).any?
- end
-
- def environments_with_alerts
- @environments_with_alerts ||=
- environments.each_with_object({}) do |environment, hash|
- name = rule_name(environment)
- hash[name] = alerts(environment)
- end
- end
-
- def rule_name(environment)
- "#{environment.name}.rules"
- end
-
- def alerts(environment)
- alerts = Projects::Prometheus::AlertsFinder
- .new(environment: environment)
- .execute
-
- alerts.map do |alert|
- hash = alert.to_param
- hash['expr'] = substitute_query_variables(hash['expr'], environment)
- hash
- end
- end
-
- def substitute_query_variables(query, environment)
- result = ::Prometheus::ProxyVariableSubstitutionService.new(environment, query: query).execute
-
- result[:params][:query]
- end
-
- def environments
- project.environments_for_scope(cluster.environment_scope)
- end
- end
- end
-end
diff --git a/app/services/clusters/applications/upgrade_service.rb b/app/services/clusters/applications/upgrade_service.rb
deleted file mode 100644
index ac68e64af38..00000000000
--- a/app/services/clusters/applications/upgrade_service.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-# frozen_string_literal: true
-
-module Clusters
- module Applications
- class UpgradeService < BaseHelmService
- def execute
- return unless app.scheduled?
-
- app.make_updating!
-
- upgrade
- end
-
- private
-
- def upgrade
- # install_command works with upgrades too
- # as it basically does `helm upgrade --install`
- log_event(:begin_upgrade)
- helm_api.update(install_command)
-
- log_event(:schedule_wait_for_upgrade)
- ClusterWaitForAppInstallationWorker.perform_in(
- ClusterWaitForAppInstallationWorker::INTERVAL, app.name, app.id)
- rescue Kubeclient::HttpError => e
- log_error(e)
- app.make_errored!(_('Kubernetes error: %{error_code}') % { error_code: e.error_code })
- rescue StandardError => e
- log_error(e)
- app.make_errored!(_('Failed to upgrade.'))
- end
- end
- end
-end
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 eabc428d0d2..e87640f4c76 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
@@ -3,7 +3,7 @@
module Clusters
module Kubernetes
class CreateOrUpdateServiceAccountService
- def initialize(kubeclient, service_account_name:, service_account_namespace:, service_account_namespace_labels: nil, token_name:, rbac:, namespace_creator: false, role_binding_name: nil)
+ def initialize(kubeclient, service_account_name:, service_account_namespace:, token_name:, rbac:, service_account_namespace_labels: nil, namespace_creator: false, role_binding_name: nil)
@kubeclient = kubeclient
@service_account_name = service_account_name
@service_account_namespace = service_account_namespace
diff --git a/app/services/concerns/incident_management/usage_data.rb b/app/services/concerns/incident_management/usage_data.rb
index 27e60029ea3..40183085344 100644
--- a/app/services/concerns/incident_management/usage_data.rb
+++ b/app/services/concerns/incident_management/usage_data.rb
@@ -7,7 +7,23 @@ module IncidentManagement
def track_incident_action(current_user, target, action)
return unless target.incident?
- track_usage_event(:"incident_management_#{action}", current_user.id)
+ event = "incident_management_#{action}"
+ track_usage_event(event, current_user.id)
+
+ namespace = target.try(:namespace)
+ project = target.try(:project)
+
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2, target.try(:namespace))
+
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ event,
+ project: project,
+ namespace: namespace,
+ user: current_user,
+ label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
+ )
end
end
end
diff --git a/app/services/concerns/rate_limited_service.rb b/app/services/concerns/rate_limited_service.rb
index 5d7247a5b99..fa366c1ccd0 100644
--- a/app/services/concerns/rate_limited_service.rb
+++ b/app/services/concerns/rate_limited_service.rb
@@ -49,8 +49,8 @@ module RateLimitedService
end
def evaluated_scope_for(service)
- opts[:scope].each_with_object({}) do |var, all|
- all[var] = service.public_send(var) # rubocop: disable GitlabSecurity/PublicSend
+ opts[:scope].index_with do |var|
+ service.public_send(var) # rubocop: disable GitlabSecurity/PublicSend
end
end
end
diff --git a/app/services/deployments/create_for_build_service.rb b/app/services/deployments/create_for_build_service.rb
index 7bc0ea88910..b58aa50a66f 100644
--- a/app/services/deployments/create_for_build_service.rb
+++ b/app/services/deployments/create_for_build_service.rb
@@ -28,7 +28,7 @@ module Deployments
def to_resource(build, environment)
return build.deployment if build.deployment
- return unless build.starts_environment?
+ return unless build.deployment_job?
deployment = ::Deployment.new(attributes(build, environment))
diff --git a/app/services/design_management/generate_image_versions_service.rb b/app/services/design_management/generate_image_versions_service.rb
index 3ff239b59cc..0771ada72a1 100644
--- a/app/services/design_management/generate_image_versions_service.rb
+++ b/app/services/design_management/generate_image_versions_service.rb
@@ -69,17 +69,15 @@ module DesignManagement
# The LFS pointer file data contains an "OID" that lets us retrieve `LfsObject`
# records, which have an Uploader (`LfsObjectUploader`) for the original design file.
def raw_files_by_path
- @raw_files_by_path ||= begin
- LfsObject.for_oids(blobs_by_oid.keys).each_with_object({}) do |lfs_object, h|
- blob = blobs_by_oid[lfs_object.oid]
- file = lfs_object.file.file
- # The `CarrierWave::SanitizedFile` is loaded without knowing the `content_type`
- # of the file, due to the file not having an extension.
- #
- # Set the content_type from the `Blob`.
- file.content_type = blob.content_type
- h[blob.path] = file
- end
+ @raw_files_by_path ||= LfsObject.for_oids(blobs_by_oid.keys).each_with_object({}) do |lfs_object, h|
+ blob = blobs_by_oid[lfs_object.oid]
+ file = lfs_object.file.file
+ # The `CarrierWave::SanitizedFile` is loaded without knowing the `content_type`
+ # of the file, due to the file not having an extension.
+ #
+ # Set the content_type from the `Blob`.
+ file.content_type = blob.content_type
+ h[blob.path] = file
end
end
diff --git a/app/services/environments/create_for_build_service.rb b/app/services/environments/create_for_build_service.rb
index c46b66ac5b3..ff4da212002 100644
--- a/app/services/environments/create_for_build_service.rb
+++ b/app/services/environments/create_for_build_service.rb
@@ -3,10 +3,10 @@
module Environments
# This class creates an environment record for a build (a pipeline job).
class CreateForBuildService
- def execute(build, merge_request: nil)
+ def execute(build)
return unless build.instance_of?(::Ci::Build) && build.has_environment_keyword?
- environment = to_resource(build, merge_request)
+ environment = to_resource(build)
if environment.persisted?
build.persisted_environment = environment
@@ -21,12 +21,12 @@ module Environments
private
# rubocop: disable Performance/ActiveRecordSubtransactionMethods
- def to_resource(build, merge_request)
+ def to_resource(build)
build.project.environments.safe_find_or_create_by(name: build.expanded_environment_name) do |environment|
# Initialize the attributes at creation
environment.auto_stop_in = expanded_auto_stop_in(build)
environment.tier = build.environment_tier_from_options
- environment.merge_request = merge_request
+ environment.merge_request = build.pipeline.merge_request
end
end
# rubocop: enable Performance/ActiveRecordSubtransactionMethods
diff --git a/app/services/environments/schedule_to_delete_review_apps_service.rb b/app/services/environments/schedule_to_delete_review_apps_service.rb
index 041b834f11b..8e9fe3300c4 100644
--- a/app/services/environments/schedule_to_delete_review_apps_service.rb
+++ b/app/services/environments/schedule_to_delete_review_apps_service.rb
@@ -68,7 +68,7 @@ module Environments
end
def mark_for_deletion(deletable_environments)
- Environment.for_id(deletable_environments).schedule_to_delete
+ Environment.id_in(deletable_environments).schedule_to_delete
end
class Result
diff --git a/app/services/error_tracking/list_projects_service.rb b/app/services/error_tracking/list_projects_service.rb
index 625addaf915..2f23d47029c 100644
--- a/app/services/error_tracking/list_projects_service.rb
+++ b/app/services/error_tracking/list_projects_service.rb
@@ -19,19 +19,18 @@ module ErrorTracking
end
def project_error_tracking_setting
- @project_error_tracking_setting ||= begin
- (super || project.build_error_tracking_setting).tap do |setting|
- setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
- api_host: params[:api_host],
- organization_slug: 'org',
- project_slug: 'proj'
- )
-
- setting.token = token(setting)
- setting.enabled = true
- end
+ (super || project.build_error_tracking_setting).tap do |setting|
+ setting.api_url = ErrorTracking::ProjectErrorTrackingSetting.build_api_url_from(
+ api_host: params[:api_host],
+ organization_slug: 'org',
+ project_slug: 'proj'
+ )
+
+ setting.token = token(setting)
+ setting.enabled = true
end
end
+ strong_memoize_attr :project_error_tracking_setting
def token(setting)
# Use param token if not masked, otherwise use database token
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 662980fe506..bf4a26400e1 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -10,6 +10,10 @@
class EventCreateService
IllegalActionError = Class.new(StandardError)
+ DEGIGN_EVENT_LABEL = 'usage_activity_by_stage_monthly.create.action_monthly_active_users_design_management'
+ MR_EVENT_LABEL = 'usage_activity_by_stage_monthly.create.merge_requests_users'
+ MR_EVENT_PROPERTY = 'merge_requests_users'
+
def open_issue(issue, current_user)
create_record_event(issue, current_user, :created)
end
@@ -26,9 +30,11 @@ class EventCreateService
create_record_event(merge_request, current_user, :created).tap do
track_event(event_action: :created, event_target: MergeRequest, author_id: current_user.id)
track_snowplow_event(
- :created,
- merge_request,
- current_user
+ action: :created,
+ project: merge_request.project,
+ user: current_user,
+ label: MR_EVENT_LABEL,
+ property: MR_EVENT_PROPERTY
)
end
end
@@ -37,9 +43,11 @@ class EventCreateService
create_record_event(merge_request, current_user, :closed).tap do
track_event(event_action: :closed, event_target: MergeRequest, author_id: current_user.id)
track_snowplow_event(
- :closed,
- merge_request,
- current_user
+ action: :closed,
+ project: merge_request.project,
+ user: current_user,
+ label: MR_EVENT_LABEL,
+ property: MR_EVENT_PROPERTY
)
end
end
@@ -52,9 +60,11 @@ class EventCreateService
create_record_event(merge_request, current_user, :merged).tap do
track_event(event_action: :merged, event_target: MergeRequest, author_id: current_user.id)
track_snowplow_event(
- :merged,
- merge_request,
- current_user
+ action: :merged,
+ project: merge_request.project,
+ user: current_user,
+ label: MR_EVENT_LABEL,
+ property: MR_EVENT_PROPERTY
)
end
end
@@ -80,11 +90,12 @@ class EventCreateService
if note.is_a?(DiffNote) && note.for_merge_request?
track_event(event_action: :commented, event_target: MergeRequest, author_id: current_user.id)
track_snowplow_event(
- :commented,
- note,
- current_user
+ action: :commented,
+ project: note.project,
+ user: current_user,
+ label: MR_EVENT_LABEL,
+ property: MR_EVENT_PROPERTY
)
-
end
end
end
@@ -117,17 +128,10 @@ class EventCreateService
records = create.zip([:created].cycle) + update.zip([:updated].cycle)
return [] if records.empty?
- if create.any?
- old_track_snowplow_event(create.first, current_user,
- Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION,
- :create, 'design_users')
- end
+ event_meta = { user: current_user, label: DEGIGN_EVENT_LABEL, property: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION }
+ track_snowplow_event(action: :create, project: create.first.project, **event_meta) if create.any?
- if update.any?
- old_track_snowplow_event(update.first, current_user,
- Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION,
- :update, 'design_users')
- end
+ track_snowplow_event(action: :update, project: update.first.project, **event_meta) if update.any?
create_record_events(records, current_user)
end
@@ -135,9 +139,13 @@ class EventCreateService
def destroy_designs(designs, current_user)
return [] unless designs.present?
- old_track_snowplow_event(designs.first, current_user,
- Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION,
- :destroy, 'design_users')
+ track_snowplow_event(
+ action: :destroy,
+ project: designs.first.project,
+ user: current_user,
+ label: DEGIGN_EVENT_LABEL,
+ property: Gitlab::UsageDataCounters::TrackUniqueEvents::DESIGN_ACTION
+ )
create_record_events(designs.zip([:destroyed].cycle), current_user)
end
@@ -229,7 +237,8 @@ class EventCreateService
namespace: namespace,
user: current_user,
project: project,
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'action_active_users_project_repo').to_context]
+ property: 'project_action',
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'project_action').to_context]
)
end
@@ -270,33 +279,18 @@ class EventCreateService
Gitlab::UsageDataCounters::TrackUniqueEvents.track_event(**params)
end
- # This will be deleted as a part of
- # https://gitlab.com/groups/gitlab-org/-/epics/8641
- # once all the events are fixed
- def old_track_snowplow_event(record, current_user, category, action, label)
+ def track_snowplow_event(action:, project:, user:, label:, property:)
return unless Feature.enabled?(:route_hll_to_snowplow_phase2)
- project = record.project
- Gitlab::Tracking.event(
- category.to_s,
- action.to_s,
- label: label,
- project: project,
- namespace: project.namespace,
- user: current_user
- )
- end
-
- def track_snowplow_event(action, record, user)
- project = record.project
Gitlab::Tracking.event(
self.class.to_s,
action.to_s,
- label: 'usage_activity_by_stage_monthly.create.merge_requests_users',
+ label: label,
namespace: project.namespace,
user: user,
project: project,
- context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: 'merge_requests_users').to_context]
+ property: property.to_s,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: property.to_s).to_context]
)
end
end
diff --git a/app/services/git/branch_hooks_service.rb b/app/services/git/branch_hooks_service.rb
index 7de56c037ed..71dd9501648 100644
--- a/app/services/git/branch_hooks_service.rb
+++ b/app/services/git/branch_hooks_service.rb
@@ -164,16 +164,27 @@ module Git
end
end
- def unsigned_x509_shas(commits)
- CommitSignatures::X509CommitSignature.unsigned_commit_shas(commits.map(&:sha))
+ def signature_types
+ types = [
+ ::CommitSignatures::GpgSignature,
+ ::CommitSignatures::X509CommitSignature
+ ]
+
+ types.push(::CommitSignatures::SshSignature) if Feature.enabled?(:ssh_commit_signatures, project)
+
+ types
end
- def unsigned_gpg_shas(commits)
- CommitSignatures::GpgSignature.unsigned_commit_shas(commits.map(&:sha))
+ def unsigned_commit_shas(commits)
+ commit_shas = commits.map(&:sha)
+
+ signature_types
+ .map { |signature| signature.unsigned_commit_shas(commit_shas) }
+ .reduce(&:&)
end
def enqueue_update_signatures
- unsigned = unsigned_x509_shas(limited_commits) & unsigned_gpg_shas(limited_commits)
+ unsigned = unsigned_commit_shas(limited_commits)
return if unsigned.empty?
signable = Gitlab::Git::Commit.shas_with_signatures(project.repository, unsigned)
diff --git a/app/services/groups/group_links/create_service.rb b/app/services/groups/group_links/create_service.rb
index 56ddf3ec0b4..52180c39972 100644
--- a/app/services/groups/group_links/create_service.rb
+++ b/app/services/groups/group_links/create_service.rb
@@ -2,7 +2,7 @@
module Groups
module GroupLinks
- class CreateService < Groups::BaseService
+ class CreateService < ::Groups::BaseService
include GroupLinkable
def initialize(group, shared_with_group, user, params)
diff --git a/app/services/groups/group_links/destroy_service.rb b/app/services/groups/group_links/destroy_service.rb
index 4d74b5f32e2..d1f16775ab3 100644
--- a/app/services/groups/group_links/destroy_service.rb
+++ b/app/services/groups/group_links/destroy_service.rb
@@ -2,7 +2,7 @@
module Groups
module GroupLinks
- class DestroyService < BaseService
+ class DestroyService < ::Groups::BaseService
def execute(one_or_more_links, skip_authorization: false)
unless skip_authorization || group && can?(current_user, :admin_group_member, group)
return error('Not Found', 404)
diff --git a/app/services/groups/group_links/update_service.rb b/app/services/groups/group_links/update_service.rb
index a1411de36d6..244ec2254a8 100644
--- a/app/services/groups/group_links/update_service.rb
+++ b/app/services/groups/group_links/update_service.rb
@@ -2,7 +2,7 @@
module Groups
module GroupLinks
- class UpdateService < BaseService
+ class UpdateService < ::Groups::BaseService
def initialize(group_link, user = nil)
super(group_link.shared_group, user)
diff --git a/app/services/groups/import_export/import_service.rb b/app/services/groups/import_export/import_service.rb
index 4092ded67bc..ac181245986 100644
--- a/app/services/groups/import_export/import_service.rb
+++ b/app/services/groups/import_export/import_service.rb
@@ -8,6 +8,7 @@ module Groups
def initialize(group:, user:)
@group = group
@current_user = user
+ @user_role = user_role
@shared = Gitlab::ImportExport::Shared.new(@group)
@logger = Gitlab::Import::Logger.build
end
@@ -31,6 +32,14 @@ module Groups
if valid_user_permissions? && import_file && restorers.all?(&:restore)
notify_success
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create',
+ label: 'import_access_level',
+ user: current_user,
+ extra: { user_role: user_role, import_type: 'import_group_from_file' }
+ )
+
group
else
notify_error!
@@ -43,6 +52,15 @@ module Groups
private
+ def user_role
+ # rubocop:disable CodeReuse/ActiveRecord, Style/MultilineTernaryOperator
+ access_level = group.parent ?
+ current_user&.group_members&.find_by(source_id: group.parent&.id)&.access_level :
+ Gitlab::Access::OWNER
+ Gitlab::Access.human_access(access_level)
+ # rubocop:enable CodeReuse/ActiveRecord, Style/MultilineTernaryOperator
+ end
+
def import_file
@import_file ||= Gitlab::ImportExport::FileImporter.import(
importable: group,
diff --git a/app/services/import/base_service.rb b/app/services/import/base_service.rb
index ab3e9c7abba..6b5adcbc39e 100644
--- a/app/services/import/base_service.rb
+++ b/app/services/import/base_service.rb
@@ -35,5 +35,30 @@ module Import
def success(project)
super().merge(project: project, status: :success)
end
+
+ def track_access_level(import_type)
+ Gitlab::Tracking.event(
+ self.class.name,
+ 'create',
+ label: 'import_access_level',
+ user: current_user,
+ extra: { user_role: user_role, import_type: import_type }
+ )
+ end
+
+ def user_role
+ if current_user.id == target_namespace.owner_id
+ 'Owner'
+ else
+ access_level = current_user&.group_members&.find_by(source_id: target_namespace.id)&.access_level
+
+ case access_level
+ when nil
+ 'Not a member'
+ else
+ Gitlab::Access.human_access(access_level)
+ end
+ end
+ end
end
end
diff --git a/app/services/import/bitbucket_server_service.rb b/app/services/import/bitbucket_server_service.rb
index 20f6c987c92..f7f17f1e53e 100644
--- a/app/services/import/bitbucket_server_service.rb
+++ b/app/services/import/bitbucket_server_service.rb
@@ -19,6 +19,8 @@ module Import
project = create_project(credentials)
+ track_access_level('bitbucket')
+
if project.persisted?
success(project)
elsif project.errors[:import_source_disabled].present?
diff --git a/app/services/import/github/gists_import_service.rb b/app/services/import/github/gists_import_service.rb
new file mode 100644
index 00000000000..df1bbe306e7
--- /dev/null
+++ b/app/services/import/github/gists_import_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Import
+ module Github
+ class GistsImportService < ::BaseService
+ def initialize(user, params)
+ @current_user = user
+ @params = params
+ end
+
+ def execute
+ return error('Import already in progress', 422) if import_status.started?
+
+ start_import
+ success
+ end
+
+ private
+
+ def import_status
+ @import_status ||= Gitlab::GithubGistsImport::Status.new(current_user.id)
+ end
+
+ def encrypted_token
+ Gitlab::CryptoHelper.aes256_gcm_encrypt(params[:github_access_token])
+ end
+
+ def start_import
+ Gitlab::GithubGistsImport::StartImportWorker.perform_async(current_user.id, encrypted_token)
+ import_status.start!
+ end
+ end
+ end
+end
diff --git a/app/services/import/github_service.rb b/app/services/import/github_service.rb
index a60963e28c7..2378a4b11b1 100644
--- a/app/services/import/github_service.rb
+++ b/app/services/import/github_service.rb
@@ -13,6 +13,7 @@ module Import
return context_error if context_error
project = create_project(access_params, provider)
+ track_access_level('github')
if project.persisted?
store_import_settings(project)
diff --git a/app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.rb b/app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.rb
index 8bee3067d6c..1652bdab5b8 100644
--- a/app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.rb
+++ b/app/services/import/gitlab_projects/file_acquisition_strategies/file_upload.rb
@@ -8,7 +8,7 @@ module Import
validate :uploaded_file
- def initialize(current_user: nil, params:)
+ def initialize(params:, current_user: nil)
@params = params
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 ac58711a0ac..e179a14c497 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
@@ -21,7 +21,7 @@ module Import
# whole condition of this validation:
validates_with RemoteFileValidator, if: -> { validate_aws_s3? || !s3_request? }
- def initialize(current_user: nil, params:)
+ def initialize(params:, current_user: nil)
@params = params
end
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 5cbca53582d..7599343d4e1 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
@@ -25,7 +25,7 @@ module Import
# we add an expiration a bit longer to ensure it won't expire during the import.
URL_EXPIRATION = 28.hours.seconds
- def initialize(current_user: nil, params:)
+ def initialize(params:, current_user: nil)
@params = params
end
diff --git a/app/services/incident_management/incidents/create_service.rb b/app/services/incident_management/incidents/create_service.rb
index f44842650b7..49019278871 100644
--- a/app/services/incident_management/incidents/create_service.rb
+++ b/app/services/incident_management/incidents/create_service.rb
@@ -23,7 +23,7 @@ module IncidentManagement
description: description,
issue_type: ISSUE_TYPE,
severity: severity,
- alert_management_alert: alert
+ alert_management_alerts: [alert].compact
},
spam_params: nil
).execute
diff --git a/app/services/incident_management/link_alerts/base_service.rb b/app/services/incident_management/link_alerts/base_service.rb
new file mode 100644
index 00000000000..474a63ab528
--- /dev/null
+++ b/app/services/incident_management/link_alerts/base_service.rb
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module LinkAlerts
+ class BaseService < ::BaseProjectService
+ private
+
+ attr_reader :incident
+
+ def allowed?
+ current_user&.can?(:admin_issue, project)
+ end
+
+ def success
+ ServiceResponse.success(payload: { incident: incident })
+ end
+
+ def error(message)
+ ServiceResponse.error(message: message)
+ end
+
+ def error_no_permissions
+ error(_('You have insufficient permissions to manage alerts for this project'))
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/link_alerts/create_service.rb b/app/services/incident_management/link_alerts/create_service.rb
new file mode 100644
index 00000000000..5e5a974efdd
--- /dev/null
+++ b/app/services/incident_management/link_alerts/create_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module LinkAlerts
+ class CreateService < BaseService
+ # @param incident [Issue] an incident to link alerts
+ # @param current_user [User]
+ # @param alert_references [[String]] a list of alert references. Can be either a short reference or URL
+ # Examples:
+ # "^alert#IID"
+ # "https://gitlab.com/company/project/-/alert_management/IID/details"
+ def initialize(incident, current_user, alert_references)
+ @incident = incident
+ @current_user = current_user
+ @alert_references = alert_references
+
+ super(project: incident.project, current_user: current_user)
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ references = extract_alerts_from_references
+ incident.alert_management_alerts << references if references.present?
+
+ success
+ end
+
+ private
+
+ attr_reader :alert_references
+
+ def extract_alerts_from_references
+ text = alert_references.join(' ')
+ extractor = Gitlab::ReferenceExtractor.new(project, current_user)
+ extractor.analyze(text, {})
+
+ extractor.alerts
+ end
+ end
+ end
+end
diff --git a/app/services/incident_management/link_alerts/destroy_service.rb b/app/services/incident_management/link_alerts/destroy_service.rb
new file mode 100644
index 00000000000..baeedaf74b6
--- /dev/null
+++ b/app/services/incident_management/link_alerts/destroy_service.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module IncidentManagement
+ module LinkAlerts
+ class DestroyService < BaseService
+ # @param incident [Issue] an incident to unlink alert from
+ # @param current_user [User]
+ # @param alert [AlertManagement::Alert] an alert to unlink from the incident
+ def initialize(incident, current_user, alert)
+ @incident = incident
+ @current_user = current_user
+ @alert = alert
+
+ super(project: incident.project, current_user: current_user)
+ end
+
+ def execute
+ return error_no_permissions unless allowed?
+
+ incident.alert_management_alerts.delete(alert)
+
+ success
+ end
+
+ private
+
+ attr_reader :alert
+ end
+ end
+end
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 a49e639ea62..3ce2674616e 100644
--- a/app/services/incident_management/pager_duty/process_webhook_service.rb
+++ b/app/services/incident_management/pager_duty/process_webhook_service.rb
@@ -9,8 +9,8 @@ module IncidentManagement
# https://developer.pagerduty.com/docs/webhooks/webhook-behavior/#size-limit
PAGER_DUTY_PAYLOAD_SIZE_LIMIT = 55.kilobytes
- # https://developer.pagerduty.com/docs/webhooks/v2-overview/#webhook-types
- PAGER_DUTY_PROCESSABLE_EVENT_TYPES = %w(incident.trigger).freeze
+ # https://developer.pagerduty.com/docs/db0fa8c8984fc-overview#event-types
+ PAGER_DUTY_PROCESSABLE_EVENT_TYPES = %w(incident.triggered).freeze
def initialize(project, payload)
super(project: project)
@@ -33,16 +33,18 @@ module IncidentManagement
attr_reader :payload
def process_incidents
- pager_duty_processable_events.each do |event|
- ::IncidentManagement::PagerDuty::ProcessIncidentWorker.perform_async(project.id, event['incident'])
- end
+ event = pager_duty_processable_event
+ return unless event
+
+ ::IncidentManagement::PagerDuty::ProcessIncidentWorker
+ .perform_async(project.id, event['incident'])
end
- def pager_duty_processable_events
- strong_memoize(:pager_duty_processable_events) do
- ::PagerDuty::WebhookPayloadParser
- .call(payload.to_h)
- .filter { |msg| msg['event'].to_s.in?(PAGER_DUTY_PROCESSABLE_EVENT_TYPES) }
+ def pager_duty_processable_event
+ strong_memoize(:pager_duty_processable_event) do
+ event = ::PagerDuty::WebhookPayloadParser.call(payload.to_h)
+
+ event if event['event'].to_s.in?(PAGER_DUTY_PROCESSABLE_EVENT_TYPES)
end
end
diff --git a/app/services/incident_management/timeline_events/base_service.rb b/app/services/incident_management/timeline_events/base_service.rb
index 7168e2fdd38..e0ca4320091 100644
--- a/app/services/incident_management/timeline_events/base_service.rb
+++ b/app/services/incident_management/timeline_events/base_service.rb
@@ -5,6 +5,8 @@ module IncidentManagement
class BaseService
include Gitlab::Utils::UsageData
+ AUTOCREATE_TAGS = [TimelineEventTag::START_TIME_TAG_NAME, TimelineEventTag::END_TIME_TAG_NAME].freeze
+
def allowed?
user&.can?(:admin_incident_management_timeline_event, incident)
end
@@ -24,6 +26,33 @@ module IncidentManagement
def error_in_save(timeline_event)
error(timeline_event.errors.full_messages.to_sentence)
end
+
+ def track_timeline_event(event, project)
+ namespace = project.namespace
+ track_usage_event(event, user.id)
+
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
+
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ event,
+ project: project,
+ namespace: namespace,
+ user: user,
+ label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
+ )
+ end
+
+ def auto_create_predefined_tags(new_tags)
+ new_tags = new_tags.map(&:downcase)
+
+ tags_to_create = AUTOCREATE_TAGS.select { |tag| tag.downcase.in?(new_tags) }
+
+ tags_to_create.each do |name|
+ project.incident_management_timeline_event_tags.create(name: name)
+ end
+ end
end
end
end
diff --git a/app/services/incident_management/timeline_events/create_service.rb b/app/services/incident_management/timeline_events/create_service.rb
index 71ff5b64515..06e8fc32335 100644
--- a/app/services/incident_management/timeline_events/create_service.rb
+++ b/app/services/incident_management/timeline_events/create_service.rb
@@ -5,7 +5,6 @@ module IncidentManagement
DEFAULT_ACTION = 'comment'
DEFAULT_EDITABLE = false
DEFAULT_AUTO_CREATED = false
- AUTOCREATE_TAGS = [TimelineEventTag::START_TIME_TAG_NAME, TimelineEventTag::END_TIME_TAG_NAME].freeze
class CreateService < TimelineEvents::BaseService
def initialize(incident, user, params)
@@ -106,7 +105,7 @@ module IncidentManagement
create_timeline_event_tag_links(timeline_event, params[:timeline_event_tag_names])
- track_usage_event(:incident_management_timeline_event_created, user.id)
+ track_timeline_event("incident_management_timeline_event_created", project)
success(timeline_event)
else
@@ -153,16 +152,6 @@ module IncidentManagement
IncidentManagement::TimelineEventTagLink.insert_all(tag_links) if tag_links.any?
end
- def auto_create_predefined_tags(new_tags)
- new_tags = new_tags.map(&:downcase)
-
- tags_to_create = AUTOCREATE_TAGS.select { |tag| tag.downcase.in?(new_tags) }
-
- tags_to_create.each do |name|
- project.incident_management_timeline_event_tags.create(name: name)
- end
- end
-
def validate_tags(project, tag_names)
return [] unless tag_names&.any?
diff --git a/app/services/incident_management/timeline_events/destroy_service.rb b/app/services/incident_management/timeline_events/destroy_service.rb
index e1c6bbbdb85..aba46cdda27 100644
--- a/app/services/incident_management/timeline_events/destroy_service.rb
+++ b/app/services/incident_management/timeline_events/destroy_service.rb
@@ -18,7 +18,7 @@ module IncidentManagement
if timeline_event.destroy
add_system_note(incident, user)
- track_usage_event(:incident_management_timeline_event_deleted, user.id)
+ track_timeline_event('incident_management_timeline_event_deleted', project)
success(timeline_event)
else
error_in_save(timeline_event)
diff --git a/app/services/incident_management/timeline_events/update_service.rb b/app/services/incident_management/timeline_events/update_service.rb
index 8d4e29c6857..4949a5a0bd1 100644
--- a/app/services/incident_management/timeline_events/update_service.rb
+++ b/app/services/incident_management/timeline_events/update_service.rb
@@ -13,21 +13,41 @@ module IncidentManagement
def initialize(timeline_event, user, params)
@timeline_event = timeline_event
@incident = timeline_event.incident
+ @project = incident.project
@user = user
@note = params[:note]
@occurred_at = params[:occurred_at]
@validation_context = VALIDATION_CONTEXT
+ @timeline_event_tags = params[:timeline_event_tag_names]
end
def execute
return error_no_permissions unless allowed?
- timeline_event.assign_attributes(update_params)
+ unless timeline_event_tags.nil?
+ auto_create_predefined_tags(timeline_event_tags)
- if timeline_event.save(context: validation_context)
+ # Refetches the tag objects to consider predefined tags as well
+ new_tags = timeline_event
+ .project
+ .incident_management_timeline_event_tags
+ .by_names(timeline_event_tags)
+
+ non_existing_tags = validate_tags(new_tags)
+
+ return error("#{_("Following tags don't exist")}: #{non_existing_tags}") if non_existing_tags.any?
+ end
+
+ begin
+ timeline_event_saved = update_timeline_event_and_event_tags(new_tags)
+ rescue ActiveRecord::RecordInvalid
+ error_in_save(timeline_event)
+ end
+
+ if timeline_event_saved
add_system_note(timeline_event)
- track_usage_event(:incident_management_timeline_event_edited, user.id)
+ track_timeline_event('incident_management_timeline_event_edited', timeline_event.project)
success(timeline_event)
else
error_in_save(timeline_event)
@@ -36,7 +56,18 @@ module IncidentManagement
private
- attr_reader :timeline_event, :incident, :user, :note, :occurred_at, :validation_context
+ attr_reader :timeline_event, :incident, :project, :user,
+ :note, :occurred_at, :validation_context, :timeline_event_tags
+
+ def update_timeline_event_and_event_tags(new_tags)
+ ApplicationRecord.transaction do
+ timeline_event.timeline_event_tags = new_tags unless timeline_event_tags.nil?
+
+ timeline_event.assign_attributes(update_params)
+
+ timeline_event.save!(context: validation_context)
+ end
+ end
def update_params
{ updated_by_user: user, note: note, occurred_at: occurred_at }.compact
@@ -61,6 +92,10 @@ module IncidentManagement
:none
end
+ def validate_tags(new_tags)
+ timeline_event_tags.map(&:downcase) - new_tags.map(&:name).map(&:downcase)
+ end
+
def allowed?
user&.can?(:edit_incident_management_timeline_event, timeline_event)
end
diff --git a/app/services/issuable/discussions_list_service.rb b/app/services/issuable/discussions_list_service.rb
index 7aa0363af01..1e5e37e4e1b 100644
--- a/app/services/issuable/discussions_list_service.rb
+++ b/app/services/issuable/discussions_list_service.rb
@@ -16,7 +16,7 @@ module Issuable
end
def execute
- return Note.none unless can_read_issuable?
+ return Note.none unless can_read_issuable_notes?
notes = NotesFinder.new(current_user, params.merge({ target: issuable, project: issuable.project }))
.execute.with_web_entity_associations.inc_relations_for_view.fresh
@@ -39,12 +39,9 @@ module Issuable
notes = prepare_notes_for_rendering(notes)
- # TODO: optimize this permission check.
- # Given this loads notes on a single issuable and current permission system, we should not have to check
- # permission on every single note. We should be able to check permission on the given issuable or its container,
- # which should result in just one permission check. Perhaps that should also either be passed to NotesFinder or
- # should be done in NotesFinder, which would decide right away if it would need to return no notes
- # or if it should just filter out internal notes.
+ # we need to check the permission on every note, because some system notes for instance can have references to
+ # resources that some user do not have read access, so those notes are filtered out from the list of notes.
+ # see Note#all_referenced_mentionables_allowed?
notes = notes.select { |n| n.readable_by?(current_user) }
Discussion.build_collection(notes, issuable)
@@ -61,10 +58,11 @@ module Issuable
end
end
- def can_read_issuable?
+ def can_read_issuable_notes?
return Ability.allowed?(current_user, :read_security_resource, issuable) if issuable.is_a?(Vulnerability)
- Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable)
+ Ability.allowed?(current_user, :"read_#{issuable.to_ability_name}", issuable) &&
+ Ability.allowed?(current_user, :read_note, issuable)
end
end
end
diff --git a/app/services/issue_links/create_service.rb b/app/services/issue_links/create_service.rb
index 7f509f3b3e0..80c6af88f21 100644
--- a/app/services/issue_links/create_service.rb
+++ b/app/services/issue_links/create_service.rb
@@ -5,9 +5,7 @@ module IssueLinks
include IncidentManagement::UsageData
def linkable_issuables(issues)
- @linkable_issuables ||= begin
- issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
- end
+ @linkable_issuables ||= issues.select { |issue| can?(current_user, :admin_issue_link, issue) }
end
def previous_related_issuables
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 28ea6b0ebf8..10407e99715 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -114,6 +114,11 @@ module Issues
Milestones::IssuesCountService.new(milestone).delete_cache
end
+
+ override :allowed_create_params
+ def allowed_create_params(params)
+ super(params).except(:issue_type, :work_item_type_id, :work_item_type)
+ end
end
end
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index da888386e0a..4f6a859e20e 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -56,7 +56,7 @@ module Issues
end
def perform_incident_management_actions(issue)
- resolve_alert(issue)
+ resolve_alerts(issue)
resolve_incident(issue)
end
@@ -71,12 +71,17 @@ module Issues
SystemNoteService.change_status(issue, issue.project, current_user, issue.state, current_commit)
end
- def resolve_alert(issue)
- return unless alert = issue.alert_management_alert
+ def resolve_alerts(issue)
+ issue.alert_management_alerts.each { |alert| resolve_alert(alert) }
+ end
+
+ def resolve_alert(alert)
return if alert.resolved?
+ issue = alert.issue
+
if alert.resolve
- SystemNoteService.change_alert_status(alert, current_user, " by closing incident #{issue.to_reference(project)}")
+ SystemNoteService.change_alert_status(alert, User.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 89b35bbab24..afad8d0c6bf 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -13,7 +13,7 @@ module Issues
# spam_checking is likely to be necessary. However, if there is not a request available in scope
# in the caller (for example, an issue created via email) and the required arguments to the
# SpamParams constructor are not otherwise available, spam_params: must be explicitly passed as nil.
- def initialize(project:, current_user: nil, params: {}, spam_params:, build_service: nil)
+ def initialize(project:, spam_params:, current_user: nil, params: {}, build_service: nil)
@extra_params = params.delete(:extra_params) || {}
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index 6366ff4076b..f7f7d85611b 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -19,6 +19,7 @@ module Issues
# to receive service desk emails on the new moved issue.
update_service_desk_sent_notifications
+ copy_email_participants
queue_copy_designs
new_entity
@@ -49,6 +50,18 @@ module Issues
.sent_notifications.update_all(project_id: new_entity.project_id, noteable_id: new_entity.id)
end
+ def copy_email_participants
+ new_attributes = { id: nil, issue_id: new_entity.id }
+
+ new_participants = original_entity.issue_email_participants.dup
+
+ new_participants.each do |participant|
+ participant.assign_attributes(new_attributes)
+ end
+
+ IssueEmailParticipant.bulk_insert!(new_participants)
+ end
+
override :update_old_entity
def update_old_entity
super
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index 0aed9e3ba40..71cc5581ae6 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -146,7 +146,7 @@ module Issues
# don't enqueue immediately to prevent todos removal in case of a mistake
TodosDestroyer::ConfidentialIssueWorker.perform_in(Todo::WAIT_FOR_DELETE, issue.id) if issue.confidential?
create_confidentiality_note(issue)
- track_usage_event(:incident_management_incident_change_confidential, current_user.id)
+ track_incident_action(current_user, issue, :incident_change_confidential)
end
end
diff --git a/app/services/jira_connect/create_asymmetric_jwt_service.rb b/app/services/jira_connect/create_asymmetric_jwt_service.rb
index 71aba6feddd..0f24128c20b 100644
--- a/app/services/jira_connect/create_asymmetric_jwt_service.rb
+++ b/app/services/jira_connect/create_asymmetric_jwt_service.rb
@@ -4,10 +4,11 @@ module JiraConnect
class CreateAsymmetricJwtService
ARGUMENT_ERROR_MESSAGE = 'jira_connect_installation is not a proxy installation'
- def initialize(jira_connect_installation)
+ def initialize(jira_connect_installation, event: :installed)
raise ArgumentError, ARGUMENT_ERROR_MESSAGE unless jira_connect_installation.proxy?
@jira_connect_installation = jira_connect_installation
+ @event = event
end
def execute
@@ -30,12 +31,18 @@ module JiraConnect
def qsh_claim
Atlassian::Jwt.create_query_string_hash(
- @jira_connect_installation.audience_installed_event_url,
+ audience_event_url,
'POST',
@jira_connect_installation.audience_url
)
end
+ def audience_event_url
+ return @jira_connect_installation.audience_uninstalled_event_url if @event == :uninstalled
+
+ @jira_connect_installation.audience_installed_event_url
+ end
+
def private_key
@private_key ||= OpenSSL::PKey::RSA.generate(3072)
end
diff --git a/app/services/jira_connect_installations/proxy_lifecycle_event_service.rb b/app/services/jira_connect_installations/proxy_lifecycle_event_service.rb
new file mode 100644
index 00000000000..d94d9e1324e
--- /dev/null
+++ b/app/services/jira_connect_installations/proxy_lifecycle_event_service.rb
@@ -0,0 +1,91 @@
+# frozen_string_literal: true
+
+module JiraConnectInstallations
+ class ProxyLifecycleEventService
+ SUPPOERTED_EVENTS = %i[installed uninstalled].freeze
+
+ def self.execute(installation, event, instance_url)
+ new(installation, event, instance_url).execute
+ end
+
+ def initialize(installation, event, instance_url)
+ # To ensure the event is sent to the right instance, this makes
+ # a copy of the installation and assigns the instance_url
+ #
+ # The installation might be modified already with a new instance_url.
+ # This can be the case for an uninstalled event.
+ # The installation is updated first, and the uninstalled event has to be sent to
+ # the old instance_url.
+ @installation = installation.dup
+ @installation.instance_url = instance_url
+
+ @event = event.to_sym
+
+ raise(ArgumentError, "Unknown event '#{@event}'") unless SUPPOERTED_EVENTS.include?(@event)
+ end
+
+ def execute
+ result = send_hook
+
+ return ServiceResponse.new(status: :success) if result.code == 200
+
+ log_unsuccessful_response(result.code, result.body)
+
+ ServiceResponse.error(message: { type: :response_error, code: result.code })
+ rescue *Gitlab::HTTP::HTTP_ERRORS => error
+ ServiceResponse.error(message: { type: :network_error, message: error.message })
+ end
+
+ private
+
+ attr_reader :installation, :event
+
+ def send_hook
+ Gitlab::HTTP.post(hook_uri, body: body)
+ end
+
+ def hook_uri
+ case event
+ when :installed
+ installation.audience_installed_event_url
+ when :uninstalled
+ installation.audience_uninstalled_event_url
+ end
+ end
+
+ def body
+ return base_body unless event == :installed
+
+ base_body.merge(installed_body)
+ end
+
+ def base_body
+ {
+ clientKey: installation.client_key,
+ jwt: jwt_token,
+ eventType: event
+ }
+ end
+
+ def installed_body
+ {
+ sharedSecret: installation.shared_secret,
+ baseUrl: installation.base_url
+ }
+ end
+
+ def jwt_token
+ @jwt_token ||= JiraConnect::CreateAsymmetricJwtService.new(@installation, event: event).execute
+ end
+
+ def log_unsuccessful_response(status_code, body)
+ Gitlab::IntegrationsLogger.info(
+ integration: 'JiraConnect',
+ message: 'Proxy lifecycle event received error response',
+ event_type: event,
+ status_code: status_code,
+ body: body
+ )
+ end
+ end
+end
diff --git a/app/services/jira_connect_installations/update_service.rb b/app/services/jira_connect_installations/update_service.rb
new file mode 100644
index 00000000000..b2b6f2a91f2
--- /dev/null
+++ b/app/services/jira_connect_installations/update_service.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module JiraConnectInstallations
+ class UpdateService
+ def self.execute(installation, update_params)
+ new(installation, update_params).execute
+ end
+
+ def initialize(installation, update_params)
+ @installation = installation
+ @update_params = update_params
+ end
+
+ def execute
+ return update_error unless @installation.update(@update_params)
+
+ if @installation.instance_url?
+ hook_result = ProxyLifecycleEventService.execute(@installation, :installed, @installation.instance_url)
+
+ if instance_url_changed? && hook_result.error?
+ @installation.update!(instance_url: @installation.instance_url_before_last_save)
+
+ return instance_installation_creation_error(hook_result.message)
+ end
+ end
+
+ send_uninstalled_hook if instance_url_changed?
+
+ ServiceResponse.new(status: :success)
+ end
+
+ private
+
+ def instance_url_changed?
+ @installation.instance_url_before_last_save != @installation.instance_url
+ end
+
+ def send_uninstalled_hook
+ return if @installation.instance_url_before_last_save.blank?
+
+ JiraConnect::SendUninstalledHookWorker.perform_async(
+ @installation.id,
+ @installation.instance_url_before_last_save
+ )
+ end
+
+ def instance_installation_creation_error(error_message)
+ message = if error_message[:type] == :response_error
+ "Could not be installed on the instance. Error response code #{error_message[:code]}"
+ else
+ 'Could not be installed on the instance. Network error'
+ end
+
+ ServiceResponse.error(message: { instance_url: [message] })
+ end
+
+ def update_error
+ ServiceResponse.error(message: @installation.errors)
+ end
+ end
+end
diff --git a/app/services/jira_import/start_import_service.rb b/app/services/jira_import/start_import_service.rb
index 9cd56cf339e..ef376e2f24a 100644
--- a/app/services/jira_import/start_import_service.rb
+++ b/app/services/jira_import/start_import_service.rb
@@ -73,7 +73,7 @@ module JiraImport
jira_imports_for_project = project.jira_imports.by_jira_project_key(jira_project_key).size + 1
title = "jira-import::#{jira_project_key}-#{jira_imports_for_project}"
description = "Label for issues that were imported from Jira on #{import_start_time.strftime('%Y-%m-%d %H:%M:%S')}"
- color = "#{::Gitlab::Color.color_for(title)}"
+ color = ::Gitlab::Color.color_for(title).to_s
{ title: title, description: description, color: color }
end
diff --git a/app/services/markup/rendering_service.rb b/app/services/markup/rendering_service.rb
index c4abbb6b5b0..cd89c170efa 100644
--- a/app/services/markup/rendering_service.rb
+++ b/app/services/markup/rendering_service.rb
@@ -2,12 +2,6 @@
module Markup
class RenderingService
- # Let's increase the render timeout
- # For a smaller one, a test that renders the blob content statically fails
- # We can consider removing this custom timeout when markup_rendering_timeout FF is removed:
- # https://gitlab.com/gitlab-org/gitlab/-/issues/365358
- RENDER_TIMEOUT = 5.seconds
-
def initialize(text, file_name: nil, context: {}, postprocess_context: {})
@text = text
@file_name = file_name
@@ -19,7 +13,7 @@ module Markup
return '' unless text.present?
return context.delete(:rendered) if context.has_key?(:rendered)
- html = file_name ? markup_unsafe : markdown_unsafe
+ html = markup_unsafe
return '' unless html.present?
@@ -29,27 +23,17 @@ module Markup
private
def markup_unsafe
- markup = proc do
- if Gitlab::MarkupHelper.gitlab_markdown?(file_name)
- markdown_unsafe
- elsif Gitlab::MarkupHelper.asciidoc?(file_name)
- asciidoc_unsafe
- elsif Gitlab::MarkupHelper.plain?(file_name)
- plain_unsafe
- else
- other_markup_unsafe
- end
- end
-
- if Feature.enabled?(:markup_rendering_timeout, context[:project])
- Gitlab::RenderTimeout.timeout(foreground: RENDER_TIMEOUT, &markup)
+ return markdown_unsafe unless file_name
+
+ if Gitlab::MarkupHelper.gitlab_markdown?(file_name)
+ markdown_unsafe
+ elsif Gitlab::MarkupHelper.asciidoc?(file_name)
+ asciidoc_unsafe
+ elsif Gitlab::MarkupHelper.plain?(file_name)
+ plain_unsafe
else
- markup.call
+ other_markup_unsafe
end
- rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, project_id: context[:project]&.id, file_name: file_name)
-
- ActionController::Base.helpers.simple_format(text)
end
def markdown_unsafe
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index f18269454e3..5afc13701e0 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -15,15 +15,15 @@ module Members
@skip_auth = skip_authorization
last_owner = true
- in_lock("delete_members:#{member.source.class}:#{member.source.id}") do
+ in_lock("delete_members:#{member.source.class}:#{member.source.id}", sleep_sec: 0.1.seconds) do
break if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
last_owner = false
member.destroy
- member.user&.invalidate_cache_counts
end
unless last_owner
+ member.user&.invalidate_cache_counts
delete_member_associations(member, skip_subresources, unassign_issuables)
end
diff --git a/app/services/merge_requests/after_create_service.rb b/app/services/merge_requests/after_create_service.rb
index 20b32dbc2a0..9e39aa94246 100644
--- a/app/services/merge_requests/after_create_service.rb
+++ b/app/services/merge_requests/after_create_service.rb
@@ -22,7 +22,7 @@ module MergeRequests
def prepare_merge_request(merge_request)
event_service.open_mr(merge_request, current_user)
- merge_request_activity_counter.track_create_mr_action(user: current_user)
+ merge_request_activity_counter.track_create_mr_action(user: current_user, merge_request: merge_request)
merge_request_activity_counter.track_mr_including_ci_config(user: current_user, merge_request: merge_request)
notification_service.new_merge_request(merge_request, current_user)
diff --git a/app/services/merge_requests/approval_service.rb b/app/services/merge_requests/approval_service.rb
index 72f398ce415..8560a15b7c4 100644
--- a/app/services/merge_requests/approval_service.rb
+++ b/app/services/merge_requests/approval_service.rb
@@ -13,6 +13,7 @@ module MergeRequests
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)
# Approval side effects (things not required to be done immediately but
# should happen after a successful approval) should be done asynchronously
diff --git a/app/services/merge_requests/assign_issues_service.rb b/app/services/merge_requests/assign_issues_service.rb
index f016c16e816..c107280efb1 100644
--- a/app/services/merge_requests/assign_issues_service.rb
+++ b/app/services/merge_requests/assign_issues_service.rb
@@ -3,15 +3,13 @@
module MergeRequests
class AssignIssuesService < BaseProjectService
def assignable_issues
- @assignable_issues ||= begin
- if current_user == merge_request.author
- closes_issues.select do |issue|
- !issue.is_a?(ExternalIssue) && !issue.assignees.present? && can?(current_user, :admin_issue, issue)
- end
- else
- []
- end
- end
+ @assignable_issues ||= if current_user == merge_request.author
+ closes_issues.select do |issue|
+ !issue.is_a?(ExternalIssue) && !issue.assignees.present? && can?(current_user, :admin_issue, issue)
+ end
+ else
+ []
+ end
end
def execute
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index e7ab2c062ee..468cadb03c7 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -59,6 +59,8 @@ module MergeRequests
merge_request_activity_counter.track_users_review_requested(users: new_reviewers)
merge_request_activity_counter.track_reviewers_changed_action(user: current_user)
trigger_merge_request_reviewers_updated(merge_request)
+
+ capture_suggested_reviewers_accepted(merge_request)
end
def cleanup_environments(merge_request)
@@ -137,6 +139,7 @@ module MergeRequests
end
filter_reviewer(merge_request)
+ filter_suggested_reviewers
end
def filter_reviewer(merge_request)
@@ -163,6 +166,10 @@ module MergeRequests
end
end
+ def filter_suggested_reviewers
+ # Implemented in EE
+ end
+
def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
@@ -253,6 +260,14 @@ module MergeRequests
def trigger_merge_request_merge_status_updated(merge_request)
GraphqlTriggers.merge_request_merge_status_updated(merge_request)
end
+
+ def trigger_merge_request_approval_state_updated(merge_request)
+ GraphqlTriggers.merge_request_approval_state_updated(merge_request)
+ end
+
+ def capture_suggested_reviewers_accepted(merge_request)
+ # Implemented in EE
+ end
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index cc786ac02bd..b9a681f29db 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -224,6 +224,7 @@ module MergeRequests
#
def assign_title_and_description
assign_description_from_repository_template
+ replace_variables_in_description
assign_title_and_description_from_commits
merge_request.title ||= title_from_issue if target_project.issues_enabled? || target_project.external_issue_tracker
merge_request.title ||= source_branch.titleize.humanize
@@ -318,6 +319,15 @@ module MergeRequests
merge_request.description = repository_template.content
end
+ def replace_variables_in_description
+ return unless merge_request.description.present?
+
+ merge_request.description = ::Gitlab::MergeRequests::MessageGenerator.new(
+ merge_request: merge_request,
+ current_user: current_user
+ ).new_mr_description
+ end
+
def issue_iid
strong_memoize(:issue_iid) do
@params_issue_iid || begin
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index 04d08f257f1..8fa80dc3513 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -39,7 +39,7 @@ module MergeRequests
# open while the Gitaly RPC waits. To avoid an idle in transaction
# timeout, we do this before we attempt to save the merge request.
- if Feature.enabled?(:async_merge_request_diff_creation, current_user)
+ if Feature.enabled?(:async_merge_request_diff_creation, merge_request.target_project)
merge_request.skip_ensure_merge_request_diff = true
else
merge_request.eager_fetch_ref!
diff --git a/app/services/merge_requests/push_options_handler_service.rb b/app/services/merge_requests/push_options_handler_service.rb
index aa52349b0ee..711978dc3f7 100644
--- a/app/services/merge_requests/push_options_handler_service.rb
+++ b/app/services/merge_requests/push_options_handler_service.rb
@@ -7,7 +7,7 @@ module MergeRequests
attr_reader :errors, :changes,
:push_options, :target_project
- def initialize(project:, current_user:, params: {}, changes:, push_options:)
+ def initialize(project:, current_user:, changes:, push_options:, params: {})
super(project: project, current_user: current_user, params: params)
@target_project = @project.default_merge_request_target
diff --git a/app/services/merge_requests/remove_approval_service.rb b/app/services/merge_requests/remove_approval_service.rb
index 8387c23fe3f..c0bb257eda6 100644
--- a/app/services/merge_requests/remove_approval_service.rb
+++ b/app/services/merge_requests/remove_approval_service.rb
@@ -19,6 +19,7 @@ module MergeRequests
merge_request_activity_counter.track_unapprove_mr_action(user: current_user)
trigger_merge_request_merge_status_updated(merge_request)
trigger_merge_request_reviewers_updated(merge_request)
+ trigger_merge_request_approval_state_updated(merge_request)
end
success
diff --git a/app/services/metrics/dashboard/grafana_metric_embed_service.rb b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
index e94c8d92c3a..26ccded45f8 100644
--- a/app/services/metrics/dashboard/grafana_metric_embed_service.rb
+++ b/app/services/metrics/dashboard/grafana_metric_embed_service.rb
@@ -51,8 +51,7 @@ module Metrics
# being passed to #get_dashboard (which accepts none)
::Metrics::Dashboard::BaseService
.instance_method(:get_dashboard)
- .bind(self)
- .call() # rubocop:disable Style/MethodCallWithoutArgsParentheses
+ .bind_call(self)
end
def cache_key(*args)
diff --git a/app/services/ml/experiment_tracking/candidate_repository.rb b/app/services/ml/experiment_tracking/candidate_repository.rb
index b6f87995185..1dbeb30145b 100644
--- a/app/services/ml/experiment_tracking/candidate_repository.rb
+++ b/app/services/ml/experiment_tracking/candidate_repository.rb
@@ -14,11 +14,15 @@ module Ml
::Ml::Candidate.with_project_id_and_iid(project.id, iid)
end
- def create!(experiment, start_time)
- experiment.candidates.create!(
+ def create!(experiment, start_time, tags = nil)
+ candidate = experiment.candidates.create!(
user: user,
start_time: start_time || 0
)
+
+ add_tags(candidate, tags)
+
+ candidate
end
def update(candidate, status, end_time)
@@ -41,36 +45,21 @@ module Ml
candidate.params.create!(name: name, value: value)
end
- def add_metrics(candidate, metric_definitions)
- return unless candidate.present?
-
- metrics = metric_definitions.map do |metric|
- {
- candidate_id: candidate.id,
- name: metric[:key],
- value: metric[:value],
- tracked_at: metric[:timestamp],
- step: metric[:step],
- **timestamps
- }
- end
+ def add_tag!(candidate, name, value)
+ candidate.metadata.create!(name: name, value: value)
+ end
- ::Ml::CandidateMetric.insert_all(metrics, returning: false) unless metrics.empty?
+ def add_metrics(candidate, metric_definitions)
+ extra_keys = { tracked_at: :timestamp, step: :step }
+ insert_many(candidate, metric_definitions, ::Ml::CandidateMetric, extra_keys)
end
def add_params(candidate, param_definitions)
- return unless candidate.present?
-
- parameters = param_definitions.map do |p|
- {
- candidate_id: candidate.id,
- name: p[:key],
- value: p[:value],
- **timestamps
- }
- end
+ insert_many(candidate, param_definitions, ::Ml::CandidateParam)
+ end
- ::Ml::CandidateParam.insert_all(parameters, returning: false) unless parameters.empty?
+ def add_tags(candidate, tag_definitions)
+ insert_many(candidate, tag_definitions, ::Ml::CandidateMetadata)
end
private
@@ -80,6 +69,22 @@ module Ml
{ created_at: current_time, updated_at: current_time }
end
+
+ def insert_many(candidate, definitions, entity_class, extra_keys = {})
+ return unless candidate.present? && definitions.present?
+
+ entities = definitions.map do |d|
+ {
+ candidate_id: candidate.id,
+ name: d[:key],
+ value: d[:value],
+ **extra_keys.transform_values { |old_key| d[old_key] },
+ **timestamps
+ }
+ end
+
+ entity_class.insert_all(entities, returning: false) unless entities.empty?
+ end
end
end
end
diff --git a/app/services/ml/experiment_tracking/experiment_repository.rb b/app/services/ml/experiment_tracking/experiment_repository.rb
index 891674adc2a..90f4cf1abec 100644
--- a/app/services/ml/experiment_tracking/experiment_repository.rb
+++ b/app/services/ml/experiment_tracking/experiment_repository.rb
@@ -20,10 +20,43 @@ module Ml
::Ml::Experiment.by_project_id(project.id)
end
- def create!(name)
- ::Ml::Experiment.create!(name: name,
- user: user,
- project: project)
+ def create!(name, tags = nil)
+ experiment = ::Ml::Experiment.create!(name: name,
+ user: user,
+ project: project)
+
+ add_tags(experiment, tags)
+
+ experiment
+ end
+
+ def add_tag!(experiment, key, value)
+ return unless experiment.present?
+
+ experiment.metadata.create!(name: key, value: value)
+ end
+
+ private
+
+ def timestamps
+ current_time = Time.zone.now
+
+ { created_at: current_time, updated_at: current_time }
+ end
+
+ def add_tags(experiment, tag_definitions)
+ return unless experiment.present? && tag_definitions.present?
+
+ entities = tag_definitions.map do |d|
+ {
+ experiment_id: experiment.id,
+ name: d[:key],
+ value: d[:value],
+ **timestamps
+ }
+ end
+
+ ::Ml::ExperimentMetadata.insert_all(entities, returning: false) unless entities.empty?
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 660d9891e46..550bd6d4c55 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -98,10 +98,10 @@ class NotificationService
end
# Notify the user when one of their personal access tokens is revoked
- def access_token_revoked(user, token_name)
+ def access_token_revoked(user, token_name, source = nil)
return unless user.can?(:receive_notifications)
- mailer.access_token_revoked_email(user, token_name).deliver_later
+ mailer.access_token_revoked_email(user, token_name, source).deliver_later
end
# Notify the user when at least one of their ssh key has expired today
@@ -495,13 +495,7 @@ class NotificationService
def new_access_request(member)
return true unless member.notifiable?(:subscription)
- source = member.source
-
- recipients = source.access_request_approvers_to_be_notified
-
- if fallback_to_group_access_request_approvers?(recipients, source)
- recipients = source.group.access_request_approvers_to_be_notified
- end
+ recipients = member.source.access_request_approvers_to_be_notified
return true if recipients.empty?
@@ -959,12 +953,6 @@ class NotificationService
mailer.member_access_requested_email(member.real_source_type, member.id, recipient.user.id).deliver_later
end
- def fallback_to_group_access_request_approvers?(recipients, source)
- return false if recipients.present?
-
- source.respond_to?(:group) && source.group
- end
-
def warn_skipping_notifications(user, object)
Gitlab::AppLogger.warn(message: "Skipping sending notifications", user: user.id, klass: object.class.to_s, object_id: object.id)
end
diff --git a/app/services/packages/debian/process_package_file_service.rb b/app/services/packages/debian/process_package_file_service.rb
new file mode 100644
index 00000000000..59e8ac3425b
--- /dev/null
+++ b/app/services/packages/debian/process_package_file_service.rb
@@ -0,0 +1,101 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class ProcessPackageFileService
+ include ExclusiveLeaseGuard
+ include Gitlab::Utils::StrongMemoize
+
+ SOURCE_FIELD_SPLIT_REGEX = /[ ()]/.freeze
+ # used by ExclusiveLeaseGuard
+ DEFAULT_LEASE_TIMEOUT = 1.hour.to_i.freeze
+
+ def initialize(package_file, creator, distribution_name, component_name)
+ @package_file = package_file
+ @creator = creator
+ @distribution_name = distribution_name
+ @component_name = component_name
+ end
+
+ def execute
+ try_obtain_lease do
+ validate!
+
+ @package_file.transaction do
+ update_file_metadata
+ end
+
+ ::Packages::Debian::GenerateDistributionWorker.perform_async(:project, package.debian_distribution.id)
+ end
+ end
+
+ private
+
+ def validate!
+ raise ArgumentError, 'package file without Debian metadata' unless @package_file.debian_file_metadatum
+ raise ArgumentError, 'already processed package file' unless @package_file.debian_file_metadatum.unknown?
+
+ return if file_metadata[:file_type] == :deb || file_metadata[:file_type] == :udeb
+
+ raise ArgumentError, "invalid package file type: #{file_metadata[:file_type]}"
+ end
+
+ def update_file_metadata
+ ::Packages::UpdatePackageFileService.new(@package_file, package_id: package.id)
+ .execute
+
+ # Force reload from database, as package has changed
+ @package_file.reload_package
+
+ @package_file.debian_file_metadatum.update!(
+ file_type: file_metadata[:file_type],
+ component: @component_name,
+ architecture: file_metadata[:architecture],
+ fields: file_metadata[:fields]
+ )
+ end
+
+ def package
+ strong_memoize(:package) do
+ package_name = file_metadata[:fields]['Package']
+ package_version = file_metadata[:fields]['Version']
+
+ if file_metadata[:fields]['Source']
+ # "sample" or "sample (1.2.3~alpha2)"
+ source_field_parts = file_metadata[:fields]['Source'].split(SOURCE_FIELD_SPLIT_REGEX)
+ package_name = source_field_parts[0]
+ package_version = source_field_parts[2] || package_version
+ end
+
+ params = {
+ 'name': package_name,
+ 'version': package_version,
+ 'distribution_name': @distribution_name
+ }
+ response = Packages::Debian::FindOrCreatePackageService.new(project, @creator, params).execute
+ response.payload[:package]
+ end
+ end
+
+ def file_metadata
+ strong_memoize(:metadata) do
+ ::Packages::Debian::ExtractMetadataService.new(@package_file).execute
+ end
+ end
+
+ def project
+ @package_file.package.project
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_key
+ "packages:debian:process_package_file_service:package_file:#{@package_file.id}"
+ end
+
+ # used by ExclusiveLeaseGuard
+ def lease_timeout
+ DEFAULT_LEASE_TIMEOUT
+ end
+ end
+ end
+end
diff --git a/app/services/packages/rpm/parse_package_service.rb b/app/services/packages/rpm/parse_package_service.rb
index 18b916a9d8b..d2751c77c5b 100644
--- a/app/services/packages/rpm/parse_package_service.rb
+++ b/app/services/packages/rpm/parse_package_service.rb
@@ -49,8 +49,8 @@ module Packages
end
def extract_static_attributes
- STATIC_ATTRIBUTES.each_with_object({}) do |attribute, hash|
- hash[attribute] = package_tags[attribute]
+ STATIC_ATTRIBUTES.index_with do |attribute|
+ package_tags[attribute]
end
end
diff --git a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
index ca5df4ce017..1733021cbb5 100644
--- a/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
+++ b/app/services/pages_domains/obtain_lets_encrypt_certificate_service.rb
@@ -28,7 +28,7 @@ module PagesDomains
api_order = ::Gitlab::LetsEncrypt::Client.new.load_order(acme_order.url)
- # https://tools.ietf.org/html/rfc8555#section-7.1.6 - statuses diagram
+ # https://www.rfc-editor.org/rfc/rfc8555#section-7.1.6 - statuses diagram
case api_order.status
when 'ready'
api_order.request_certificate(private_key: acme_order.private_key, domain: pages_domain.domain)
diff --git a/app/services/pages_domains/retry_acme_order_service.rb b/app/services/pages_domains/retry_acme_order_service.rb
index ef3d8ce0b67..6251c9d3615 100644
--- a/app/services/pages_domains/retry_acme_order_service.rb
+++ b/app/services/pages_domains/retry_acme_order_service.rb
@@ -15,7 +15,26 @@ module PagesDomains
pages_domain.update!(auto_ssl_failed: false)
end
- PagesDomainSslRenewalWorker.perform_async(pages_domain.id) if updated
+ return unless updated
+
+ PagesDomainSslRenewalWorker.perform_async(pages_domain.id)
+
+ publish_event(pages_domain)
+ end
+
+ private
+
+ def publish_event(domain)
+ event = PagesDomainUpdatedEvent.new(
+ data: {
+ project_id: domain.project.id,
+ namespace_id: domain.project.namespace_id,
+ root_namespace_id: domain.project.root_namespace.id,
+ domain: domain.domain
+ }
+ )
+
+ Gitlab::EventStore.publish(event)
end
end
end
diff --git a/app/services/personal_access_tokens/revoke_service.rb b/app/services/personal_access_tokens/revoke_service.rb
index 5371b6c91ef..bb5edc27340 100644
--- a/app/services/personal_access_tokens/revoke_service.rb
+++ b/app/services/personal_access_tokens/revoke_service.rb
@@ -4,10 +4,13 @@ module PersonalAccessTokens
class RevokeService < BaseService
attr_reader :token, :current_user, :group
- def initialize(current_user = nil, token: nil, group: nil)
+ VALID_SOURCES = %w[secret_detection].freeze
+
+ def initialize(current_user = nil, token: nil, group: nil, source: nil)
@current_user = current_user
@token = token
@group = group
+ @source = source
end
def execute
@@ -15,7 +18,7 @@ module PersonalAccessTokens
if token.revoke!
log_event
- notification_service.access_token_revoked(token.user, token.name)
+ notification_service.access_token_revoked(token.user, token.name, @source)
ServiceResponse.success(message: success_message)
else
ServiceResponse.error(message: error_message)
@@ -33,11 +36,24 @@ module PersonalAccessTokens
end
def revocation_permitted?
- Ability.allowed?(current_user, :revoke_token, token)
+ if current_user
+ Ability.allowed?(current_user, :revoke_token, token)
+ else
+ source && VALID_SOURCES.include?(source)
+ end
+ end
+
+ def source
+ current_user&.username || @source
end
def log_event
- Gitlab::AppLogger.info("PAT REVOCATION: revoked_by: '#{current_user.username}', revoked_for: '#{token.user.username}', token_id: '#{token.id}'")
+ Gitlab::AppLogger.info(
+ class: self.class.name,
+ message: "PAT Revoked",
+ revoked_by: source,
+ revoked_for: token.user.username,
+ token_id: token.id)
end
end
end
diff --git a/app/services/projects/batch_forks_count_service.rb b/app/services/projects/batch_forks_count_service.rb
index 6467744a435..78663d8dad5 100644
--- a/app/services/projects/batch_forks_count_service.rb
+++ b/app/services/projects/batch_forks_count_service.rb
@@ -7,11 +7,9 @@ module Projects
class BatchForksCountService < Projects::BatchCountService
# rubocop: disable CodeReuse/ActiveRecord
def global_count
- @global_count ||= begin
- count_service.query(project_ids)
+ @global_count ||= count_service.query(project_ids)
.group(:forked_from_project_id)
.count
- end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/projects/batch_open_issues_count_service.rb b/app/services/projects/batch_open_issues_count_service.rb
index d6ff2291af8..c396d7c0cfc 100644
--- a/app/services/projects/batch_open_issues_count_service.rb
+++ b/app/services/projects/batch_open_issues_count_service.rb
@@ -7,9 +7,7 @@ module Projects
class BatchOpenIssuesCountService < Projects::BatchCountService
# rubocop: disable CodeReuse/ActiveRecord
def global_count
- @global_count ||= begin
- count_service.query(project_ids).group(:project_id).count
- end
+ @global_count ||= count_service.query(project_ids).group(:project_id).count
end
# rubocop: enable CodeReuse/ActiveRecord
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 5393c2c080d..45557d03502 100644
--- a/app/services/projects/container_repository/cleanup_tags_base_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_base_service.rb
@@ -6,6 +6,8 @@ module Projects
private
def filter_out_latest!(tags)
+ return unless keep_latest
+
tags.reject!(&:latest?)
end
@@ -84,6 +86,10 @@ module Projects
params['keep_n']
end
+ def keep_latest
+ params.fetch('keep_latest', true)
+ end
+
def project
container_repository.project
end
diff --git a/app/services/projects/container_repository/destroy_service.rb b/app/services/projects/container_repository/destroy_service.rb
index 83bb8624bba..6db6b449671 100644
--- a/app/services/projects/container_repository/destroy_service.rb
+++ b/app/services/projects/container_repository/destroy_service.rb
@@ -3,12 +3,46 @@
module Projects
module ContainerRepository
class DestroyService < BaseService
- def execute(container_repository)
+ CLEANUP_TAGS_SERVICE_PARAMS = {
+ 'name_regex_delete' => '.*',
+ 'container_expiration_policy' => true, # to avoid permissions checks
+ 'keep_latest' => false
+ }.freeze
+
+ def execute(container_repository, disable_timeout: true)
return false unless can?(current_user, :update_container_image, project)
# Delete tags outside of the transaction to avoid hitting an idle-in-transaction timeout
- container_repository.delete_tags!
- container_repository.delete_failed! unless container_repository.destroy
+ unless delete_tags(container_repository, disable_timeout) &&
+ destroy_container_repository(container_repository)
+ container_repository.delete_failed!
+ end
+ end
+
+ private
+
+ def delete_tags(container_repository, disable_timeout)
+ service = Projects::ContainerRepository::CleanupTagsService.new(
+ container_repository: container_repository,
+ params: CLEANUP_TAGS_SERVICE_PARAMS.merge('disable_timeout' => disable_timeout)
+ )
+ result = service.execute
+ return true if result[:status] == :success
+
+ log_error(error_message(container_repository, 'error in deleting tags'))
+ false
+ end
+
+ def destroy_container_repository(container_repository)
+ return true if container_repository.destroy
+
+ log_error(error_message(container_repository, container_repository.errors.full_messages.join('. ')))
+ false
+ end
+
+ def error_message(container_repository, message)
+ "Container repository with ID: #{container_repository.id} and path: #{container_repository.path}" \
+ " failed with message: #{message}"
end
end
end
diff --git a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
index e947e9575e2..b69a3cc1a2c 100644
--- a/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/gitlab/cleanup_tags_service.rb
@@ -18,7 +18,7 @@ module Projects
container_repository.each_tags_page(page_size: TAGS_PAGE_SIZE) do |tags|
execute_for_tags(tags, result)
- raise TimeoutError if timeout?(start_time)
+ raise TimeoutError if !timeout_disabled? && timeout?(start_time)
end
end
end
@@ -72,6 +72,10 @@ module Projects
def pushed_at(tag)
tag.updated_at || tag.created_at
end
+
+ def timeout_disabled?
+ params['disable_timeout'] || false
+ end
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index c72f9b4b602..a4b473f35c6 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -317,6 +317,3 @@ module Projects
end
Projects::CreateService.prepend_mod_with('Projects::CreateService')
-
-# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::CreateService as well
-Projects::CreateService.prepend(Measurable)
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index ddbcfbb675c..a1f55f547a1 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -3,8 +3,6 @@
module Projects
module ImportExport
class ExportService < BaseService
- prepend Measurable
-
def initialize(*args)
super
diff --git a/app/services/projects/import_export/parallel_export_service.rb b/app/services/projects/import_export/parallel_export_service.rb
new file mode 100644
index 00000000000..7e4c0279b06
--- /dev/null
+++ b/app/services/projects/import_export/parallel_export_service.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class ParallelExportService
+ def initialize(export_job, current_user, after_export_strategy)
+ @export_job = export_job
+ @current_user = current_user
+ @after_export_strategy = after_export_strategy
+ @shared = project.import_export_shared
+ @logger = Gitlab::Export::Logger.build
+ end
+
+ def execute
+ log_info('Parallel project export started')
+
+ if save_exporters && save_export_archive
+ log_info('Parallel project export finished successfully')
+ execute_after_export_action(after_export_strategy)
+ else
+ notify_error
+ end
+
+ ensure
+ cleanup
+ end
+
+ private
+
+ attr_reader :export_job, :current_user, :after_export_strategy, :shared, :logger
+
+ delegate :project, to: :export_job
+
+ def execute_after_export_action(after_export_strategy)
+ return if after_export_strategy.execute(current_user, project)
+
+ notify_error
+ end
+
+ def exporters
+ [version_saver, exported_relations_merger]
+ end
+
+ def save_exporters
+ exporters.all? do |exporter|
+ log_info("Parallel project export - #{exporter.class.name} saver started")
+
+ exporter.save
+ end
+ end
+
+ def save_export_archive
+ Gitlab::ImportExport::Saver.save(exportable: project, shared: shared)
+ end
+
+ def version_saver
+ @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared)
+ end
+
+ def exported_relations_merger
+ @relation_saver ||= Gitlab::ImportExport::Project::ExportedRelationsMerger.new(
+ export_job: export_job,
+ shared: shared)
+ end
+
+ def cleanup
+ FileUtils.rm_rf(shared.export_path) if File.exist?(shared.export_path)
+ FileUtils.rm_rf(shared.archive_path) if File.exist?(shared.archive_path)
+ end
+
+ def log_info(message)
+ logger.info(
+ message: message,
+ **log_base_data
+ )
+ end
+
+ def notify_error
+ logger.error(
+ message: 'Parallel project export error',
+ export_errors: shared.errors.join(', '),
+ export_job_id: export_job.id,
+ **log_base_data
+ )
+
+ NotificationService.new.project_not_exported(project, current_user, shared.errors)
+ end
+
+ def log_base_data
+ {
+ project_id: project.id,
+ project_name: project.name,
+ project_path: project.full_path
+ }
+ end
+ end
+ end
+end
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index 6a13b8e38c1..967a1e990b2 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -179,6 +179,3 @@ module Projects
end
Projects::ImportService.prepend_mod_with('Projects::ImportService')
-
-# Measurable should be at the bottom of the ancestor chain, so it will measure execution of EE::Projects::ImportService as well
-Projects::ImportService.prepend(Measurable)
diff --git a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
index c91103f897f..f7de7f98768 100644
--- a/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_link_list_service.rb
@@ -1,6 +1,6 @@
# frozen_string_literal: true
-# This service lists the download link from a remote source based on the
+# This service yields operation on each download link from a remote source based on the
# oids provided
module Projects
module LfsPointers
@@ -23,29 +23,22 @@ module Projects
@remote_uri = remote_uri
end
- # This method accepts two parameters:
# - oids: hash of oids to query. The structure is { lfs_file_oid => lfs_file_size }
- #
- # Returns an array of LfsDownloadObject
- def execute(oids)
- return [] unless project&.lfs_enabled? && remote_uri && oids.present?
+ # Yields operation for each link batch-by-batch
+ def each_link(oids, &block)
+ return unless project&.lfs_enabled? && remote_uri && oids.present?
- get_download_links_in_batches(oids)
+ download_links_in_batches(oids, &block)
end
private
- def get_download_links_in_batches(oids, batch_size = REQUEST_BATCH_SIZE)
- download_links = []
-
+ def download_links_in_batches(oids, batch_size = REQUEST_BATCH_SIZE, &block)
oids.each_slice(batch_size) do |batch|
- download_links += get_download_links(batch)
+ download_links_for(batch).each(&block)
end
-
- download_links
-
rescue DownloadLinksRequestEntityTooLargeError => e
- # Log this exceptions to see how open it happens
+ # Log this exceptions to see how often it happens
Gitlab::ErrorTracking
.track_exception(e, project_id: project&.id, batch_size: batch_size, oids_count: oids.count)
@@ -57,7 +50,7 @@ module Projects
raise DownloadLinksError, 'Unable to download due to RequestEntityTooLarge errors'
end
- def get_download_links(oids)
+ def download_links_for(oids)
response = Gitlab::HTTP.post(remote_uri,
body: request_body(oids),
headers: headers)
diff --git a/app/services/projects/lfs_pointers/lfs_download_service.rb b/app/services/projects/lfs_pointers/lfs_download_service.rb
index eaf73b78c1c..26352198e5c 100644
--- a/app/services/projects/lfs_pointers/lfs_download_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_download_service.rb
@@ -92,9 +92,15 @@ module Projects
end
def fetch_file(&block)
+ attempts ||= 1
response = Gitlab::HTTP.get(lfs_sanitized_url, download_options, &block)
raise ResponseError, "Received error code #{response.code}" unless response.success?
+ rescue Net::OpenTimeout
+ raise if attempts >= 3
+
+ attempts += 1
+ retry
end
def with_tmp_file
diff --git a/app/services/projects/lfs_pointers/lfs_import_service.rb b/app/services/projects/lfs_pointers/lfs_import_service.rb
index 3fc82f2c410..c9791041088 100644
--- a/app/services/projects/lfs_pointers/lfs_import_service.rb
+++ b/app/services/projects/lfs_pointers/lfs_import_service.rb
@@ -9,9 +9,7 @@ module Projects
def execute
return success unless project&.lfs_enabled?
- lfs_objects_to_download = LfsObjectDownloadListService.new(project).execute
-
- lfs_objects_to_download.each do |lfs_download_object|
+ LfsObjectDownloadListService.new(project).each_list_item do |lfs_download_object|
LfsDownloadService.new(project, lfs_download_object).execute
end
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 b4872cd9442..09fec9939b9 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
@@ -1,8 +1,8 @@
# frozen_string_literal: true
-# This service manages the whole worflow of discovering the Lfs files in a
-# repository, linking them to the project and downloading (and linking) the non
-# existent ones.
+# This service discovers the Lfs files that are linked in repository,
+# but not downloaded yet and yields the operation
+# on each Lfs file link (url) to remote repository.
module Projects
module LfsPointers
class LfsObjectDownloadListService < BaseService
@@ -14,30 +14,31 @@ module Projects
LfsObjectDownloadListError = Class.new(StandardError)
- def execute
- return [] unless project&.lfs_enabled?
-
- if external_lfs_endpoint?
- # If the endpoint host is different from the import_url it means
- # that the repo is using a third party service for storing the LFS files.
- # In this case, we have to disable lfs in the project
- disable_lfs!
-
- return []
- end
+ def each_list_item(&block)
+ return unless context_valid?
# Downloading the required information and gathering it inside an
# LfsDownloadObject for each oid
- #
LfsDownloadLinkListService
.new(project, remote_uri: current_endpoint_uri)
- .execute(missing_lfs_files)
+ .each_link(missing_lfs_files, &block)
rescue LfsDownloadLinkListService::DownloadLinksError => e
raise LfsObjectDownloadListError, "The LFS objects download list couldn't be imported. Error: #{e.message}"
end
private
+ def context_valid?
+ return false unless project&.lfs_enabled?
+ return true unless external_lfs_endpoint?
+
+ # If the endpoint host is different from the import_url it means
+ # that the repo is using a third party service for storing the LFS files.
+ # In this case, we have to disable lfs in the project
+ disable_lfs!
+ false
+ end
+
def external_lfs_endpoint?
lfsconfig_endpoint_uri && lfsconfig_endpoint_uri.host != import_uri.host
end
@@ -99,12 +100,10 @@ module Projects
# The import url must end with '.git' here we ensure it is
def default_endpoint_uri
- @default_endpoint_uri ||= begin
- import_uri.dup.tap do |uri|
- path = uri.path.gsub(%r(/$), '')
- path += '.git' unless path.ends_with?('.git')
- uri.path = path + LFS_BATCH_API_ENDPOINT
- end
+ @default_endpoint_uri ||= import_uri.dup.tap do |uri|
+ path = uri.path.gsub(%r(/$), '')
+ path += '.git' unless path.ends_with?('.git')
+ uri.path = path + LFS_BATCH_API_ENDPOINT
end
end
end
diff --git a/app/services/projects/refresh_build_artifacts_size_statistics_service.rb b/app/services/projects/refresh_build_artifacts_size_statistics_service.rb
index 1f86e5f4ba9..8e006dc8c34 100644
--- a/app/services/projects/refresh_build_artifacts_size_statistics_service.rb
+++ b/app/services/projects/refresh_build_artifacts_size_statistics_service.rb
@@ -18,7 +18,7 @@ module Projects
# Mark the refresh ready for another worker to pick up and process the next batch
refresh.requeue!(batch.last.id)
- refresh.project.statistics.delayed_increment_counter(:build_artifacts_size, total_artifacts_size)
+ refresh.project.statistics.increment_counter(:build_artifacts_size, total_artifacts_size)
end
else
# Remove the refresh job from the table if there are no more
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index 6a963e7fcd1..0fadd75669e 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -63,16 +63,19 @@ module Projects
end
def build_commit_status
+ stage = create_stage
+
GenericCommitStatus.new(
user: build.user,
ci_stage: stage,
name: 'pages:deploy',
- stage: 'deploy'
+ stage: 'deploy',
+ stage_idx: stage.position
)
end
# rubocop: disable Performance/ActiveRecordSubtransactionMethods
- def stage
+ def create_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
diff --git a/app/services/projects/update_remote_mirror_service.rb b/app/services/projects/update_remote_mirror_service.rb
index f686f14b5b3..aca6fa91eb1 100644
--- a/app/services/projects/update_remote_mirror_service.rb
+++ b/app/services/projects/update_remote_mirror_service.rb
@@ -10,7 +10,7 @@ module Projects
return success unless remote_mirror.enabled?
# Blocked URLs are a hard failure, no need to attempt to retry
- if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url))
+ if Gitlab::UrlBlocker.blocked_url?(normalized_url(remote_mirror.url), schemes: Project::VALID_MIRROR_PROTOCOLS)
hard_retry_or_fail(remote_mirror, _('The remote mirror URL is invalid.'), tries)
return error(remote_mirror.last_error)
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index f9a2c825608..301d11d841c 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -10,7 +10,6 @@ module Projects
def execute
build_topics
remove_unallowed_params
- mirror_operations_access_level_changes
validate!
ensure_wiki_exists if enabling_wiki?
@@ -65,16 +64,36 @@ module Projects
return unless changing_default_branch?
previous_default_branch = project.default_branch
+ new_default_branch = params[:default_branch]
- if project.change_head(params[:default_branch])
+ if project.change_head(new_default_branch)
params[:previous_default_branch] = previous_default_branch
+ if !project.root_ref?(new_default_branch) && has_custom_head_branch?
+ raise ValidationError,
+ format(
+ s_("UpdateProject|Could not set the default branch. Do you have a branch named 'HEAD' in your repository? (%{linkStart}How do I fix this?%{linkEnd})"),
+ linkStart: ambiguous_head_documentation_link, linkEnd: '</a>'
+ ).html_safe
+ end
+
after_default_branch_change(previous_default_branch)
else
raise ValidationError, s_("UpdateProject|Could not set the default branch")
end
end
+ def ambiguous_head_documentation_link
+ url = Rails.application.routes.url_helpers.help_page_path('user/project/repository/branches/index.md', anchor: 'error-ambiguous-head-branch-exists')
+
+ format('<a href="%{url}" target="_blank" rel="noopener noreferrer">', url: url)
+ end
+
+ # See issue: https://gitlab.com/gitlab-org/gitlab/-/issues/381731
+ def has_custom_head_branch?
+ project.repository.branch_names.any? { |name| name.casecmp('head') == 0 }
+ end
+
def after_default_branch_change(previous_default_branch)
# overridden by EE module
end
@@ -83,21 +102,6 @@ module Projects
params.delete(:emails_disabled) unless can?(current_user, :set_emails_disabled, project)
end
- # Temporary code to sync permissions changes as operations access setting
- # is being split into monitor_access_level, deployments_access_level, infrastructure_access_level.
- # To be removed as part of https://gitlab.com/gitlab-org/gitlab/-/issues/364240
- def mirror_operations_access_level_changes
- return if Feature.enabled?(:split_operations_visibility_permissions, project)
-
- operations_access_level = params.dig(:project_feature_attributes, :operations_access_level)
-
- return if operations_access_level.nil?
-
- [:monitor_access_level, :infrastructure_access_level, :feature_flags_access_level, :environments_access_level].each do |key|
- params[:project_feature_attributes][key] = operations_access_level
- end
- end
-
def after_update
todos_features_changes = %w(
issues_access_level
diff --git a/app/services/protected_branches/api_service.rb b/app/services/protected_branches/api_service.rb
index b8fe9bac13e..0a7777c7fed 100644
--- a/app/services/protected_branches/api_service.rb
+++ b/app/services/protected_branches/api_service.rb
@@ -3,11 +3,11 @@
module ProtectedBranches
class ApiService < ProtectedBranches::BaseService
def create
- ::ProtectedBranches::CreateService.new(@project, @current_user, protected_branch_params).execute
+ ::ProtectedBranches::CreateService.new(project_or_group, @current_user, protected_branch_params).execute
end
def update(protected_branch)
- ::ProtectedBranches::UpdateService.new(@project, @current_user,
+ ::ProtectedBranches::UpdateService.new(project_or_group, @current_user,
protected_branch_params(with_defaults: false)).execute(protected_branch)
end
@@ -36,4 +36,4 @@ protected_branch_params(with_defaults: false)).execute(protected_branch)
end
end
-ProtectedBranches::ApiService.prepend_mod_with('ProtectedBranches::ApiService')
+ProtectedBranches::ApiService.prepend_mod
diff --git a/app/services/protected_branches/base_service.rb b/app/services/protected_branches/base_service.rb
index d26c1b148bf..951017b2d01 100644
--- a/app/services/protected_branches/base_service.rb
+++ b/app/services/protected_branches/base_service.rb
@@ -2,10 +2,12 @@
module ProtectedBranches
class BaseService < ::BaseService
+ attr_reader :project_or_group
+
# current_user - The user that performs the action
# params - A hash of parameters
- def initialize(project, current_user = nil, params = {})
- @project = project
+ def initialize(project_or_group, current_user = nil, params = {})
+ @project_or_group = project_or_group
@current_user = current_user
@params = params
end
@@ -15,7 +17,7 @@ module ProtectedBranches
end
def refresh_cache
- CacheService.new(@project, @current_user, @params).refresh
+ CacheService.new(@project_or_group, @current_user, @params).refresh
end
end
end
diff --git a/app/services/protected_branches/cache_service.rb b/app/services/protected_branches/cache_service.rb
index 66ca549c508..af8c9ce74bb 100644
--- a/app/services/protected_branches/cache_service.rb
+++ b/app/services/protected_branches/cache_service.rb
@@ -66,13 +66,18 @@ module ProtectedBranches
log_error(
'class' => self.class.name,
'message' => "Cache mismatch '#{encoded_ref_name}': cached value: #{cached_value}, real value: #{real_value}",
- 'project_id' => @project.id,
- 'project_path' => @project.full_path
+ 'record_class' => project_or_group.class.name,
+ 'record_id' => project_or_group.id,
+ 'record_path' => project_or_group.full_path
)
end
def redis_key
- @redis_key ||= [CACHE_ROOT_KEY, @project.id].join(':')
+ @redis_key ||= if Feature.enabled?(:group_protected_branches)
+ [CACHE_ROOT_KEY, project_or_group.class.name, project_or_group.id].join(':')
+ else
+ [CACHE_ROOT_KEY, project_or_group.id].join(':')
+ end
end
def metrics
diff --git a/app/services/protected_branches/create_service.rb b/app/services/protected_branches/create_service.rb
index 903addf7afc..46585e0b65d 100644
--- a/app/services/protected_branches/create_service.rb
+++ b/app/services/protected_branches/create_service.rb
@@ -23,9 +23,9 @@ module ProtectedBranches
end
def protected_branch
- @protected_branch ||= project.protected_branches.new(params)
+ @protected_branch ||= project_or_group.protected_branches.new(params)
end
end
end
-ProtectedBranches::CreateService.prepend_mod_with('ProtectedBranches::CreateService')
+ProtectedBranches::CreateService.prepend_mod
diff --git a/app/services/protected_branches/destroy_service.rb b/app/services/protected_branches/destroy_service.rb
index 01d3b68314f..a32a867491e 100644
--- a/app/services/protected_branches/destroy_service.rb
+++ b/app/services/protected_branches/destroy_service.rb
@@ -10,4 +10,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::DestroyService.prepend_mod_with('ProtectedBranches::DestroyService')
+ProtectedBranches::DestroyService.prepend_mod
diff --git a/app/services/protected_branches/legacy_api_create_service.rb b/app/services/protected_branches/legacy_api_create_service.rb
index aef99a860a0..f662d9d1bf0 100644
--- a/app/services/protected_branches/legacy_api_create_service.rb
+++ b/app/services/protected_branches/legacy_api_create_service.rb
@@ -24,7 +24,7 @@ module ProtectedBranches
@params.merge!(push_access_levels_attributes: [{ access_level: push_access_level }],
merge_access_levels_attributes: [{ access_level: merge_access_level }])
- service = ProtectedBranches::CreateService.new(@project, @current_user, @params)
+ service = ProtectedBranches::CreateService.new(project_or_group, @current_user, @params)
service.execute
end
end
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index 8ff6c4bd734..b144797ab6d 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -30,7 +30,7 @@ module ProtectedBranches
params[:merge_access_levels_attributes] = [{ access_level: Gitlab::Access::MAINTAINER }]
end
- service = ProtectedBranches::UpdateService.new(project, current_user, params)
+ service = ProtectedBranches::UpdateService.new(project_or_group, current_user, params)
service.execute(protected_branch)
end
end
diff --git a/app/services/protected_branches/update_service.rb b/app/services/protected_branches/update_service.rb
index c155e0022f5..4b54bf92989 100644
--- a/app/services/protected_branches/update_service.rb
+++ b/app/services/protected_branches/update_service.rb
@@ -19,4 +19,4 @@ module ProtectedBranches
end
end
-ProtectedBranches::UpdateService.prepend_mod_with('ProtectedBranches::UpdateService')
+ProtectedBranches::UpdateService.prepend_mod
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 1d7c5d2c80a..f1e4dac8835 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -158,15 +158,15 @@ module QuickActions
end
def map_commands(commands, method)
- commands.map do |name, arg|
- definition = self.class.definition_by_name(name)
+ commands.map do |name_or_alias, arg|
+ definition = self.class.definition_by_name(name_or_alias)
next unless definition
case method
when :explain
definition.explain(self, arg)
when :execute_message
- @execution_message[name.to_sym] || definition.execute_message(self, arg)
+ @execution_message[definition.name.to_sym] || definition.execute_message(self, arg)
end
end.compact
end
diff --git a/app/services/repositories/housekeeping_service.rb b/app/services/repositories/housekeeping_service.rb
index de80390e60b..e12d69807f2 100644
--- a/app/services/repositories/housekeeping_service.rb
+++ b/app/services/repositories/housekeeping_service.rb
@@ -84,7 +84,11 @@ module Repositories
end
def period_match?
- [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 }
+ if Feature.enabled?(:optimized_housekeeping)
+ pushes_since_gc % repack_period == 0
+ else
+ [gc_period, full_repack_period, repack_period, PACK_REFS_PERIOD].any? { |period| pushes_since_gc % period == 0 }
+ end
end
def housekeeping_enabled?
diff --git a/app/services/search_service.rb b/app/services/search_service.rb
index f38522b9764..403a2f077b0 100644
--- a/app/services/search_service.rb
+++ b/app/services/search_service.rb
@@ -45,9 +45,9 @@ class SearchService
end
def show_snippets?
- return @show_snippets if defined?(@show_snippets)
-
- @show_snippets = params[:snippets] == 'true'
+ strong_memoize(:show_snippets) do
+ params[:snippets] == 'true'
+ end
end
delegate :scope, to: :search_service
diff --git a/app/services/snippets/create_service.rb b/app/services/snippets/create_service.rb
index 5cadff42958..a62d5290271 100644
--- a/app/services/snippets/create_service.rb
+++ b/app/services/snippets/create_service.rb
@@ -4,7 +4,7 @@ module Snippets
class CreateService < Snippets::BaseService
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary.
- def initialize(project:, current_user: nil, params: {}, spam_params:)
+ def initialize(project:, spam_params:, current_user: nil, params: {})
super(project: project, current_user: current_user, params: params)
@spam_params = spam_params
end
diff --git a/app/services/system_notes/commit_service.rb b/app/services/system_notes/commit_service.rb
index c89998f77c7..592351079aa 100644
--- a/app/services/system_notes/commit_service.rb
+++ b/app/services/system_notes/commit_service.rb
@@ -81,12 +81,10 @@ module SystemNotes
commit_ids = if count == 1
existing_commits.first.short_id
+ elsif oldrev && !Gitlab::Git.blank_ref?(oldrev)
+ "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
else
- if oldrev && !Gitlab::Git.blank_ref?(oldrev)
- "#{Commit.truncate_sha(oldrev)}...#{existing_commits.last.short_id}"
- else
- "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
- end
+ "#{existing_commits.first.short_id}..#{existing_commits.last.short_id}"
end
commits_text = "#{count} commit".pluralize(count)
diff --git a/app/services/task_list_toggle_service.rb b/app/services/task_list_toggle_service.rb
index 082fa1447fc..8e20ffc2a52 100644
--- a/app/services/task_list_toggle_service.rb
+++ b/app/services/task_list_toggle_service.rb
@@ -44,8 +44,8 @@ class TaskListToggleService
# any `[ ]` or `[x]` in the middle of the text
if currently_checked
markdown_task.sub!(Taskable::COMPLETE_PATTERN, '[ ]') unless toggle_as_checked
- else
- markdown_task.sub!(Taskable::INCOMPLETE_PATTERN, '[x]') if toggle_as_checked
+ elsif toggle_as_checked
+ markdown_task.sub!(Taskable::INCOMPLETE_PATTERN, '[x]')
end
source_lines[source_line_index] = markdown_task
diff --git a/app/services/timelogs/base_service.rb b/app/services/timelogs/base_service.rb
index e09264864fd..712a0a4f128 100644
--- a/app/services/timelogs/base_service.rb
+++ b/app/services/timelogs/base_service.rb
@@ -22,9 +22,9 @@ module Timelogs
end
def error_in_save(timelog)
- return error(_("Failed to save timelog")) if timelog.errors.empty?
+ return error(_("Failed to save timelog"), 404) if timelog.errors.empty?
- error(timelog.errors.full_messages.to_sentence)
+ error(timelog.errors.full_messages.to_sentence, 404)
end
end
end
diff --git a/app/services/timelogs/create_service.rb b/app/services/timelogs/create_service.rb
index 12181cec20a..19428864fa9 100644
--- a/app/services/timelogs/create_service.rb
+++ b/app/services/timelogs/create_service.rb
@@ -21,6 +21,9 @@ module Timelogs
}, 404)
end
+ return error(_("Spent at can't be a future date and time."), 404) if spent_at.future?
+ return error(_("Time spent can't be zero."), 404) if time_spent == 0
+
issue = issuable if issuable.is_a?(Issue)
merge_request = issuable if issuable.is_a?(MergeRequest)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 06352d36215..9ae31f8ac58 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -166,8 +166,9 @@ class TodoService
# When user marks a target as todo
def mark_todo(target, current_user)
- attributes = attributes_for_todo(target.project, target, current_user, Todo::MARKED)
- create_todos(current_user, attributes)
+ project = target.project
+ attributes = attributes_for_todo(project, target, current_user, Todo::MARKED)
+ create_todos(current_user, attributes, project&.namespace, project)
end
def todo_exist?(issuable, current_user)
@@ -214,13 +215,32 @@ class TodoService
end
def create_request_review_todo(target, author, reviewers)
- attributes = attributes_for_todo(target.project, target, author, Todo::REVIEW_REQUESTED)
- create_todos(reviewers, attributes)
+ project = target.project
+ attributes = attributes_for_todo(project, target, author, Todo::REVIEW_REQUESTED)
+ create_todos(reviewers, attributes, project.namespace, project)
+ end
+
+ def create_member_access_request(member)
+ source = member.source
+ attributes = attributes_for_access_request_todos(source, member.user, Todo::MEMBER_ACCESS_REQUESTED)
+
+ approvers = source.access_request_approvers_to_be_notified.map(&:user)
+ return true if approvers.empty?
+
+ if source.instance_of? Project
+ project = source
+ namespace = project.namespace
+ else
+ project = nil
+ namespace = source
+ end
+
+ create_todos(approvers, attributes, namespace, project)
end
private
- def create_todos(users, attributes)
+ def create_todos(users, attributes, namespace, project)
users = Array(users)
return if users.empty?
@@ -246,7 +266,7 @@ class TodoService
todos = users.map do |user|
issue_type = attributes.delete(:issue_type)
- track_todo_creation(user, issue_type)
+ track_todo_creation(user, issue_type, namespace, project)
Todo.create(attributes.merge(user_id: user.id))
end
@@ -286,9 +306,10 @@ class TodoService
def create_assignment_todo(target, author, old_assignees = [])
if target.assignees.any?
+ project = target.project
assignees = target.assignees - old_assignees
- attributes = attributes_for_todo(target.project, target, author, Todo::ASSIGNED)
- create_todos(assignees, attributes)
+ attributes = attributes_for_todo(project, target, author, Todo::ASSIGNED)
+ create_todos(assignees, attributes, project.namespace, project)
end
end
@@ -303,22 +324,24 @@ class TodoService
# Create Todos for directly addressed users
directly_addressed_users = filter_directly_addressed_users(parent, note || target, author, skip_users)
attributes = attributes_for_todo(parent, target, author, Todo::DIRECTLY_ADDRESSED, note)
- create_todos(directly_addressed_users, attributes)
+ create_todos(directly_addressed_users, attributes, parent&.namespace, parent)
# Create Todos for mentioned users
mentioned_users = filter_mentioned_users(parent, note || target, author, skip_users + directly_addressed_users)
attributes = attributes_for_todo(parent, target, author, Todo::MENTIONED, note)
- create_todos(mentioned_users, attributes)
+ create_todos(mentioned_users, attributes, parent&.namespace, parent)
end
def create_build_failed_todo(merge_request, todo_author)
- attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::BUILD_FAILED)
- create_todos(todo_author, attributes)
+ project = merge_request.project
+ attributes = attributes_for_todo(project, merge_request, todo_author, Todo::BUILD_FAILED)
+ create_todos(todo_author, attributes, project.namespace, project)
end
def create_unmergeable_todo(merge_request, todo_author)
- attributes = attributes_for_todo(merge_request.project, merge_request, todo_author, Todo::UNMERGEABLE)
- create_todos(todo_author, attributes)
+ project = merge_request.project
+ attributes = attributes_for_todo(project, merge_request, todo_author, Todo::UNMERGEABLE)
+ create_todos(todo_author, attributes, project.namespace, project)
end
def attributes_for_target(target)
@@ -382,10 +405,37 @@ class TodoService
PendingTodosFinder.new(users, criteria).execute
end
- def track_todo_creation(user, issue_type)
+ def track_todo_creation(user, issue_type, namespace, project)
return unless issue_type == 'incident'
- track_usage_event(:incident_management_incident_todo, user.id)
+ event = "incident_management_incident_todo"
+ track_usage_event(event, user.id)
+
+ return unless Feature.enabled?(:route_hll_to_snowplow_phase2, namespace)
+
+ Gitlab::Tracking.event(
+ self.class.to_s,
+ event,
+ project: project,
+ namespace: namespace,
+ user: user,
+ label: 'redis_hll_counters.incident_management.incident_management_total_unique_counts_monthly',
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis_hll, event: event).to_context]
+ )
+ end
+
+ def attributes_for_access_request_todos(source, author, action, note = nil)
+ attributes = {
+ target_id: source.id,
+ target_type: source.class.polymorphic_name,
+ author_id: author.id,
+ action: action,
+ note: note
+ }
+
+ attributes[:group_id] = source.id unless source.instance_of? Project
+
+ attributes
end
end
diff --git a/app/services/users/approve_service.rb b/app/services/users/approve_service.rb
index 15486ddcd43..353456c545d 100644
--- a/app/services/users/approve_service.rb
+++ b/app/services/users/approve_service.rb
@@ -42,7 +42,7 @@ module Users
end
def log_event(user)
- Gitlab::AppLogger.info(message: "User instance access request approved", user: "#{user.username}", email: "#{user.email}", approved_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ Gitlab::AppLogger.info(message: "User instance access request approved", user: user.username.to_s, email: user.email.to_s, approved_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
end
end
diff --git a/app/services/users/assigned_issues_count_service.rb b/app/services/users/assigned_issues_count_service.rb
new file mode 100644
index 00000000000..6590902587d
--- /dev/null
+++ b/app/services/users/assigned_issues_count_service.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module Users
+ class AssignedIssuesCountService < ::BaseCountService
+ def initialize(current_user:, max_limit: User::MAX_LIMIT_FOR_ASSIGNEED_ISSUES_COUNT)
+ @current_user = current_user
+ @max_limit = max_limit
+ end
+
+ def cache_key
+ ['users', @current_user.id, 'max_assigned_open_issues_count']
+ end
+
+ def cache_options
+ { force: false, expires_in: User::COUNT_CACHE_VALIDITY_PERIOD }
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def uncached_count
+ # When a user has many assigned issues, counting them all can be very slow.
+ # As a workaround, we will short-circuit the counting query once the count reaches some threshold.
+ #
+ # Concretely, given a threshold, say 100 (= max_limit),
+ # iterate through the first 100 issues, sorted by ID desc, assigned to the user using `issue_assignees` table.
+ # For each issue iterated, use IssuesFinder to check if the issue should be counted.
+ initializer = IssueAssignee
+ .select(:issue_id).joins(", LATERAL (#{finder_constraint.to_sql}) as issues")
+ .where(user_id: @current_user.id)
+ .order(issue_id: :desc)
+ .limit(1)
+ recursive_finder = initializer.where("issue_assignees.issue_id < assigned_issues.issue_id")
+
+ cte = <<~SQL
+ WITH RECURSIVE assigned_issues AS (
+ (
+ #{initializer.to_sql}
+ )
+ UNION ALL
+ (
+ SELECT next_assigned_issue.issue_id
+ FROM assigned_issues,
+ LATERAL (
+ #{recursive_finder.to_sql}
+ ) next_assigned_issue
+ )
+ ) SELECT COUNT(*) FROM (SELECT * FROM assigned_issues LIMIT #{@max_limit}) issues
+ SQL
+
+ ApplicationRecord.connection.execute(cte).first["count"]
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def finder_constraint
+ IssuesFinder.new(@current_user, assignee_id: @current_user.id, state: 'opened', non_archived: true)
+ .execute
+ .where("issues.id=issue_assignees.issue_id").limit(1)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+end
diff --git a/app/services/users/banned_user_base_service.rb b/app/services/users/banned_user_base_service.rb
index a582816283a..74c10581a6e 100644
--- a/app/services/users/banned_user_base_service.rb
+++ b/app/services/users/banned_user_base_service.rb
@@ -36,7 +36,7 @@ module Users
end
def log_event(user)
- Gitlab::AppLogger.info(message: "User #{action}", user: "#{user.username}", email: "#{user.email}", "#{action}_by": "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ Gitlab::AppLogger.info(message: "User #{action}", user: user.username.to_s, email: user.email.to_s, "#{action}_by": current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
end
end
diff --git a/app/services/users/build_service.rb b/app/services/users/build_service.rb
index 8ef1b3e0613..064bf132d3d 100644
--- a/app/services/users/build_service.rb
+++ b/app/services/users/build_service.rb
@@ -117,7 +117,7 @@ module Users
end
def skip_user_confirmation_email_from_setting
- !Gitlab::CurrentSettings.send_user_confirmation_email
+ Gitlab::CurrentSettings.email_confirmation_setting_off?
end
def use_fallback_name?
diff --git a/app/services/users/keys_count_service.rb b/app/services/users/keys_count_service.rb
index f82d27eded9..378093f2e1b 100644
--- a/app/services/users/keys_count_service.rb
+++ b/app/services/users/keys_count_service.rb
@@ -11,7 +11,7 @@ module Users
end
def relation_for_count
- user.keys
+ user.keys.auth
end
def raw?
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 2d92aaed7da..5d518803315 100644
--- a/app/services/users/migrate_records_to_ghost_user_service.rb
+++ b/app/services/users/migrate_records_to_ghost_user_service.rb
@@ -42,6 +42,7 @@ module Users
migrate_award_emoji
migrate_snippets
migrate_reviews
+ migrate_releases
end
def post_migrate_records
@@ -96,6 +97,10 @@ module Users
batched_migrate(Review, :author_id)
end
+ def migrate_releases
+ batched_migrate(Release, :author_id)
+ end
+
# rubocop:disable CodeReuse/ActiveRecord
def batched_migrate(base_scope, column, batch_size: 50)
loop do
diff --git a/app/services/users/reject_service.rb b/app/services/users/reject_service.rb
index 459dd81b74d..dc22b2ec21d 100644
--- a/app/services/users/reject_service.rb
+++ b/app/services/users/reject_service.rb
@@ -34,7 +34,7 @@ module Users
end
def log_event(user)
- Gitlab::AppLogger.info(message: "User instance access request rejected", user: "#{user.username}", email: "#{user.email}", rejected_by: "#{current_user.username}", ip_address: "#{current_user.current_sign_in_ip}")
+ Gitlab::AppLogger.info(message: "User instance access request rejected", user: user.username.to_s, email: user.email.to_s, rejected_by: current_user.username.to_s, ip_address: current_user.current_sign_in_ip.to_s)
end
end
end
diff --git a/app/services/users/update_highest_member_role_service.rb b/app/services/users/update_highest_member_role_service.rb
index 90a5966265d..fff001e04d7 100644
--- a/app/services/users/update_highest_member_role_service.rb
+++ b/app/services/users/update_highest_member_role_service.rb
@@ -17,9 +17,7 @@ module Users
private
def user_highest_role
- @user_highest_role ||= begin
- @user.user_highest_role || @user.build_user_highest_role
- end
+ @user_highest_role ||= @user.user_highest_role || @user.build_user_highest_role
end
def highest_access_level
diff --git a/app/services/web_hooks/log_execution_service.rb b/app/services/web_hooks/log_execution_service.rb
index 448bb7d4097..b1da0c1642f 100644
--- a/app/services/web_hooks/log_execution_service.rb
+++ b/app/services/web_hooks/log_execution_service.rb
@@ -17,18 +17,32 @@ module WebHooks
end
def execute
- update_hook_failure_state if WebHook.web_hooks_disable_failed?(hook)
+ update_hook_failure_state
log_execution
end
private
def log_execution
+ mask_response_headers
+
log_data[:request_headers]['X-Gitlab-Token'] = _('[REDACTED]') if hook.token?
WebHookLog.create!(web_hook: hook, **log_data)
end
+ def mask_response_headers
+ return unless hook.url_variables?
+ return unless log_data.key?(:response_headers)
+
+ variables_map = hook.url_variables.invert.transform_values { "{#{_1}}" }
+ regex = Regexp.union(variables_map.keys)
+
+ log_data[:response_headers].transform_values! do |value|
+ regex === value ? value.gsub(regex, variables_map) : value
+ end
+ end
+
# Perform this operation within an `Gitlab::ExclusiveLease` lock to make it
# safe to be called concurrently from different workers.
def update_hook_failure_state
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
index 12b2cf87d5d..cf9eddbd13f 100644
--- a/app/services/wiki_pages/update_service.rb
+++ b/app/services/wiki_pages/update_service.rb
@@ -12,7 +12,7 @@ module WikiPages
execute_hooks(page)
ServiceResponse.success(payload: { page: page })
else
- raise UpdateError, s_('Could not update wiki page')
+ raise UpdateError, _('Could not update wiki page')
end
rescue UpdateError, WikiPage::PageChangedError, WikiPage::PageRenameError => e
page.update_attributes(@params) # rubocop:disable Rails/ActiveRecordAliases
diff --git a/app/services/work_items/create_and_link_service.rb b/app/services/work_items/create_and_link_service.rb
index 5cc358c4b4f..351ebc14564 100644
--- a/app/services/work_items/create_and_link_service.rb
+++ b/app/services/work_items/create_and_link_service.rb
@@ -6,7 +6,7 @@ module WorkItems
# This class should always be run inside a transaction as we could end up with
# new work items that were never associated with other work items as expected.
class CreateAndLinkService
- def initialize(project:, current_user: nil, params: {}, spam_params:, link_params: {})
+ def initialize(project:, spam_params:, current_user: nil, params: {}, link_params: {})
@project = project
@current_user = current_user
@params = params
diff --git a/app/services/work_items/create_from_task_service.rb b/app/services/work_items/create_from_task_service.rb
index ef1d47c560d..ced5b17a21c 100644
--- a/app/services/work_items/create_from_task_service.rb
+++ b/app/services/work_items/create_from_task_service.rb
@@ -2,7 +2,7 @@
module WorkItems
class CreateFromTaskService
- def initialize(work_item:, current_user: nil, work_item_params: {}, spam_params:)
+ def initialize(work_item:, spam_params:, current_user: nil, work_item_params: {})
@work_item = work_item
@current_user = current_user
@work_item_params = work_item_params
diff --git a/app/services/work_items/create_service.rb b/app/services/work_items/create_service.rb
index 87cc690d666..c89ebc75b80 100644
--- a/app/services/work_items/create_service.rb
+++ b/app/services/work_items/create_service.rb
@@ -4,7 +4,7 @@ module WorkItems
class CreateService < Issues::CreateService
include WidgetableService
- def initialize(project:, current_user: nil, params: {}, spam_params:, widget_params: {})
+ def initialize(project:, spam_params:, current_user: nil, params: {}, widget_params: {})
super(
project: project,
current_user: current_user,
diff --git a/app/services/work_items/delete_task_service.rb b/app/services/work_items/delete_task_service.rb
index 3bb23576442..2a82a993b71 100644
--- a/app/services/work_items/delete_task_service.rb
+++ b/app/services/work_items/delete_task_service.rb
@@ -2,7 +2,7 @@
module WorkItems
class DeleteTaskService
- def initialize(work_item:, current_user: nil, task_params: {}, lock_version:)
+ def initialize(work_item:, lock_version:, current_user: nil, task_params: {})
@work_item = work_item
@current_user = current_user
@task_params = task_params
diff --git a/app/uploaders/ci/secure_file_uploader.rb b/app/uploaders/ci/secure_file_uploader.rb
index 8aa624d6b30..11cbfc6c1f2 100644
--- a/app/uploaders/ci/secure_file_uploader.rb
+++ b/app/uploaders/ci/secure_file_uploader.rb
@@ -34,10 +34,6 @@ module Ci
false
end
- def background_upload_enabled?
- false
- end
-
def default_store
object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL
end
diff --git a/app/uploaders/file_mover.rb b/app/uploaders/file_mover.rb
index 95bc2680ed6..92ab2d88b41 100644
--- a/app/uploaders/file_mover.rb
+++ b/app/uploaders/file_mover.rb
@@ -24,7 +24,6 @@ class FileMover
if update_markdown
update_upload_model
- uploader.schedule_background_upload
end
end
diff --git a/app/uploaders/file_uploader.rb b/app/uploaders/file_uploader.rb
index 7250ce5c0b0..f947f70985c 100644
--- a/app/uploaders/file_uploader.rb
+++ b/app/uploaders/file_uploader.rb
@@ -27,10 +27,6 @@ class FileUploader < GitlabUploader
after :remove, :prune_store_dir
- # FileUploader do not run in a model transaction, so we can simply
- # enqueue a job after the :store hook.
- after :store, :schedule_background_upload
-
def self.root
File.join(options.storage_path, 'uploads')
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index abe06bd97e1..62024bff4c0 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -101,8 +101,8 @@ class GitlabUploader < CarrierWave::Uploader::Base
stream =
if file_storage?
File.open(path, "rb") if path
- else
- ::Gitlab::HttpIO.new(url, cached_size) if url
+ elsif url
+ ::Gitlab::HttpIO.new(url, cached_size)
end
return unless stream
@@ -115,6 +115,15 @@ class GitlabUploader < CarrierWave::Uploader::Base
end
end
+ def multi_read(offsets)
+ open do |stream|
+ offsets.map do |start_offset, end_offset|
+ stream.seek(start_offset)
+ stream.read(end_offset - start_offset + 1)
+ end
+ end
+ end
+
# Used to replace an existing upload with another +file+ without modifying stored metadata
# Use this method only to repair/replace an existing upload, or to upload to a Geo secondary node
#
diff --git a/app/uploaders/object_storage.rb b/app/uploaders/object_storage.rb
index 063aca7937c..e74998ce4a8 100644
--- a/app/uploaders/object_storage.rb
+++ b/app/uploaders/object_storage.rb
@@ -67,16 +67,6 @@ module ObjectStorage
super
end
- def schedule_background_upload(*args)
- return unless schedule_background_upload?
- return unless upload
-
- ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
- upload.class.to_s,
- mounted_as,
- upload.id)
- end
-
def exclusive_lease_key
# For FileUploaders, model may have many uploaders. In that case
# we want to use exclusive key per upload, not per model to allow
@@ -99,40 +89,6 @@ module ObjectStorage
end
end
- # Add support for automatic background uploading after the file is stored.
- #
- module BackgroundMove
- extend ActiveSupport::Concern
-
- def background_upload(mount_points = [])
- return unless mount_points.any?
-
- run_after_commit do
- mount_points.each { |mount| send(mount).schedule_background_upload } # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
- def changed_mounts
- self.class.uploaders.select do |mount, uploader_class|
- mounted_as = uploader_class.serialization_column(self.class, mount)
- uploader = send(:"#{mounted_as}") # rubocop:disable GitlabSecurity/PublicSend
-
- next unless uploader
- next unless uploader.exists?
- next unless send(:"saved_change_to_#{mounted_as}?") # rubocop:disable GitlabSecurity/PublicSend
-
- mount
- end.keys
- end
-
- included do
- include AfterCommitQueue
- after_save do
- background_upload(changed_mounts)
- end
- end
- end
-
module Concern
extend ActiveSupport::Concern
@@ -155,10 +111,6 @@ module ObjectStorage
object_store_options&.direct_upload
end
- def background_upload_enabled?
- object_store_options.background_upload
- end
-
def proxy_download_enabled?
object_store_options.proxy_download
end
@@ -311,15 +263,6 @@ module ObjectStorage
end
end
- def schedule_background_upload(*args)
- return unless schedule_background_upload?
-
- ObjectStorage::BackgroundMoveWorker.perform_async(self.class.name,
- model.class.name,
- mounted_as,
- model.id)
- end
-
def fog_directory
self.class.remote_store_path
end
@@ -405,12 +348,6 @@ module ObjectStorage
private
- def schedule_background_upload?
- self.class.object_store_enabled? &&
- self.class.background_upload_enabled? &&
- self.file_storage?
- end
-
def cache_remote_file!(remote_object_id, original_filename)
file_path = File.join(TMP_UPLOAD_PATH, remote_object_id)
file_path = Pathname.new(file_path).cleanpath.to_s
diff --git a/app/uploaders/packages/composer/cache_uploader.rb b/app/uploaders/packages/composer/cache_uploader.rb
index f8052ec4810..ad7c017c4ba 100644
--- a/app/uploaders/packages/composer/cache_uploader.rb
+++ b/app/uploaders/packages/composer/cache_uploader.rb
@@ -4,8 +4,6 @@ class Packages::Composer::CacheUploader < GitlabUploader
storage_options Gitlab.config.packages
- after :store, :schedule_background_upload
-
alias_method :upload, :model
def filename
diff --git a/app/uploaders/packages/debian/component_file_uploader.rb b/app/uploaders/packages/debian/component_file_uploader.rb
index e4d637fecac..2de4743d7f7 100644
--- a/app/uploaders/packages/debian/component_file_uploader.rb
+++ b/app/uploaders/packages/debian/component_file_uploader.rb
@@ -5,8 +5,6 @@ class Packages::Debian::ComponentFileUploader < GitlabUploader
storage_options Gitlab.config.packages
- after :store, :schedule_background_upload
-
alias_method :upload, :model
def filename
diff --git a/app/uploaders/packages/debian/distribution_release_file_uploader.rb b/app/uploaders/packages/debian/distribution_release_file_uploader.rb
index a6ff3767b22..268d42796e9 100644
--- a/app/uploaders/packages/debian/distribution_release_file_uploader.rb
+++ b/app/uploaders/packages/debian/distribution_release_file_uploader.rb
@@ -5,8 +5,6 @@ class Packages::Debian::DistributionReleaseFileUploader < GitlabUploader
storage_options Gitlab.config.packages
- after :store, :schedule_background_upload
-
alias_method :upload, :model
def filename
diff --git a/app/uploaders/packages/package_file_uploader.rb b/app/uploaders/packages/package_file_uploader.rb
index 9c0a88c9bf8..c8a09c50dc6 100644
--- a/app/uploaders/packages/package_file_uploader.rb
+++ b/app/uploaders/packages/package_file_uploader.rb
@@ -5,8 +5,6 @@ class Packages::PackageFileUploader < GitlabUploader
storage_options Gitlab.config.packages
- after :store, :schedule_background_upload
-
alias_method :upload, :model
def filename
diff --git a/app/uploaders/packages/rpm/repository_file_uploader.rb b/app/uploaders/packages/rpm/repository_file_uploader.rb
index ff7e2bc719a..f95f861585c 100644
--- a/app/uploaders/packages/rpm/repository_file_uploader.rb
+++ b/app/uploaders/packages/rpm/repository_file_uploader.rb
@@ -6,8 +6,6 @@ module Packages
storage_options Gitlab.config.packages
- after :store, :schedule_background_upload
-
alias_method :upload, :model
def filename
diff --git a/app/uploaders/pages/deployment_uploader.rb b/app/uploaders/pages/deployment_uploader.rb
index e510025fc7d..c5ba65673ab 100644
--- a/app/uploaders/pages/deployment_uploader.rb
+++ b/app/uploaders/pages/deployment_uploader.rb
@@ -36,13 +36,6 @@ module Pages
false
end
- # we don't need background uploads because we upload files
- # to the right store right away, and we already do that in
- # the background job
- def background_upload_enabled?
- false
- end
-
def default_store
object_store_enabled? ? ObjectStorage::Store::REMOTE : ObjectStorage::Store::LOCAL
end
diff --git a/app/uploaders/terraform/state_uploader.rb b/app/uploaders/terraform/state_uploader.rb
index 091b253b0ed..61e7ed7b0e6 100644
--- a/app/uploaders/terraform/state_uploader.rb
+++ b/app/uploaders/terraform/state_uploader.rb
@@ -48,10 +48,6 @@ module Terraform
false
end
- def background_upload_enabled?
- false
- end
-
def proxy_download_enabled?
true
end
diff --git a/app/validators/iso8601_date_validator.rb b/app/validators/iso8601_date_validator.rb
new file mode 100644
index 00000000000..2b4682f0572
--- /dev/null
+++ b/app/validators/iso8601_date_validator.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+class Iso8601DateValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ Date.iso8601(record.read_attribute_before_type_cast(attribute).to_s)
+ rescue ArgumentError, TypeError
+ record.errors.add(attribute, _('must be in ISO 8601 format'))
+ end
+end
diff --git a/app/validators/json_schemas/build_metadata_id_tokens.json b/app/validators/json_schemas/build_metadata_id_tokens.json
index 7f39c7274f3..d97b2241ca3 100644
--- a/app/validators/json_schemas/build_metadata_id_tokens.json
+++ b/app/validators/json_schemas/build_metadata_id_tokens.json
@@ -5,18 +5,27 @@
"patternProperties": {
".*": {
"type": "object",
- "patternProperties": {
- "^id_token$": {
- "type": "object",
- "required": ["aud"],
- "properties": {
- "aud": { "type": "string" },
- "field": { "type": "string" }
- },
- "additionalProperties": false
+ "required": [
+ "aud"
+ ],
+ "properties": {
+ "aud": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "array",
+ "items": {
+ "type": "string"
+ },
+ "minItems": 1,
+ "uniqueItems": true
+ }
+ ]
}
},
"additionalProperties": false
}
}
-}
+} \ No newline at end of file
diff --git a/app/validators/json_schemas/build_report_result_data.json b/app/validators/json_schemas/build_report_result_data.json
index 0a12c9c39a7..d109389a046 100644
--- a/app/validators/json_schemas/build_report_result_data.json
+++ b/app/validators/json_schemas/build_report_result_data.json
@@ -3,11 +3,16 @@
"description": "Build report result data",
"type": "object",
"properties": {
- "coverage": { "type": "float" },
+ "coverage": {
+ "type": "number",
+ "format": "float"
+ },
"tests": {
"type": "object",
- "items": { "$ref": "./build_report_result_data_tests.json" }
+ "items": {
+ "$ref": "./build_report_result_data_tests.json"
+ }
}
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/app/validators/json_schemas/build_report_result_data_tests.json b/app/validators/json_schemas/build_report_result_data_tests.json
index 610070fde5f..3b6a2688313 100644
--- a/app/validators/json_schemas/build_report_result_data_tests.json
+++ b/app/validators/json_schemas/build_report_result_data_tests.json
@@ -3,12 +3,24 @@
"description": "Build report result data tests",
"type": "object",
"properties": {
- "name": { "type": "string" },
- "duration": { "type": "string" },
- "failed": { "type": "integer" },
- "errored": { "type": "integer" },
- "skipped": { "type": "integer" },
- "success": { "type": "integer" }
+ "name": {
+ "type": "string"
+ },
+ "duration": {
+ "type": "string"
+ },
+ "failed": {
+ "type": "integer"
+ },
+ "errored": {
+ "type": "integer"
+ },
+ "skipped": {
+ "type": "integer"
+ },
+ "success": {
+ "type": "integer"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/app/validators/json_schemas/ci_secure_file_metadata.json b/app/validators/json_schemas/ci_secure_file_metadata.json
index 46a7ff60b8f..66e778d6026 100644
--- a/app/validators/json_schemas/ci_secure_file_metadata.json
+++ b/app/validators/json_schemas/ci_secure_file_metadata.json
@@ -4,10 +4,10 @@
"properties": {
"id": { "type": "string" },
"team_name": { "type": "string" },
- "team_id": { "type": "string" },
+ "team_id": { "type": "array" },
"app_name": { "type": "string" },
"app_id": { "type": "string" },
- "app_id_prefix": { "type": "string" },
+ "app_id_prefix": { "type": "array" },
"xcode_managed": { "type": "boolean" },
"entitlements": { "type": "object" },
"devices": { "type": "array" },
diff --git a/app/validators/json_schemas/daily_build_group_report_result_data.json b/app/validators/json_schemas/daily_build_group_report_result_data.json
index 2b073506375..5b153b47b1e 100644
--- a/app/validators/json_schemas/daily_build_group_report_result_data.json
+++ b/app/validators/json_schemas/daily_build_group_report_result_data.json
@@ -3,7 +3,10 @@
"description": "Daily build group report result data",
"type": "object",
"properties": {
- "coverage": { "type": "float" }
+ "coverage": {
+ "type": "number",
+ "format": "float"
+ }
},
"additionalProperties": false
-}
+} \ No newline at end of file
diff --git a/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json b/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json
deleted file mode 100644
index 8e80b52d9b8..00000000000
--- a/app/validators/json_schemas/merge_request_predictions_suggested_reviewers.json
+++ /dev/null
@@ -1,10 +0,0 @@
-{
- "description": "Merge request predictions suggested reviewers",
- "type": "object",
- "properties": {
- "top_n": { "type": "number" },
- "version": { "type": "string" },
- "reviewers": { "type": "array" }
- },
- "additionalProperties": true
-}
diff --git a/app/validators/json_schemas/web_hooks_url_variables.json b/app/validators/json_schemas/web_hooks_url_variables.json
index ea504d114e3..27b251a059f 100644
--- a/app/validators/json_schemas/web_hooks_url_variables.json
+++ b/app/validators/json_schemas/web_hooks_url_variables.json
@@ -8,7 +8,7 @@
"^[A-Za-z]+[0-9]*(?:[._-][A-Za-z0-9]+)*$": {
"type": "string",
"minLength": 1,
- "maxLength": 100
+ "maxLength": 2048
}
}
}
diff --git a/app/views/abuse_reports/new.html.haml b/app/views/abuse_reports/new.html.haml
index aaa85e81bd4..d5dfddef837 100644
--- a/app/views/abuse_reports/new.html.haml
+++ b/app/views/abuse_reports/new.html.haml
@@ -1,8 +1,8 @@
-- page_title _("Report abuse to admin")
+- page_title _("Report abuse to administrator")
%h1.page-title.gl-font-size-h-display
- = _("Report abuse to admin")
+ = _("Report abuse to administrator")
%p
- = _("Please use this form to report to the admin users who create spam issues, comments or behave inappropriately.")
+ = _("Use this form to report to the administrator users who create spam issues, comments or behave inappropriately.")
%p
= _("A member of the abuse team will review your report as soon as possible.")
%hr
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 00e5650b551..eeedd58ec15 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -24,11 +24,13 @@
= markdown_field(abuse_report, :message)
%td
- if user
- = link_to _('Remove user & report'), admin_abuse_report_path(abuse_report, remove_user: true),
- data: { confirm: _("USER %{user} WILL BE REMOVED! Are you sure?") % { user: user.name }, confirm_btn_variant: "danger" }, aria: { label: _('Remove user & report') }, remote: true, method: :delete, class: "gl-button btn btn-block btn-danger js-remove-tr"
+ = 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 gl-mb-5" }) do
+ = _('Remove user & report')
- if user && !user.blocked?
- = link_to _('Block user'), block_admin_user_path(user), data: { confirm: _('USER WILL BE BLOCKED! Are you sure?') }, aria: { label: _('Block user') }, method: :put, class: "gl-button btn btn-default btn-block"
+ = 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 }, class: "gl-mb-5" }) do
+ = _('Block user')
- else
- .gl-button.btn.btn-default.disabled.btn-block
+ = 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 }, class: "gl-mb-5" }) do
= _('Already blocked')
- = link_to _('Remove report'), [:admin, abuse_report], remote: true, method: :delete, class: "gl-button btn btn-default btn-block btn-close js-remove-tr"
+ = 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/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 0f7b10f822d..21f69f6700f 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -46,7 +46,7 @@
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-form-input gl-mt-2'
.help-block
= _('Specify an email address regex pattern to identify default internal users.')
- = link_to _('Learn more.'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/admin_area/external_users', anchor: 'set-a-new-user-to-external'), target: '_blank', rel: 'noopener noreferrer'
- unless Gitlab.com?
.form-group
= f.label :deactivate_dormant_users, _('Dormant users'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index f6635ad17ef..8fafa52cd4c 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -1,6 +1,6 @@
.settings-content
= gitlab_ui_form_for @application_setting, url: ci_cd_admin_application_settings_path(anchor: 'js-ci-cd-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting )
+ = form_errors(@application_setting)
%fieldset
.form-group
@@ -61,7 +61,7 @@
%h4
= s_('AdminSettings|CI/CD limits')
%p
- = s_('AdminSettings|Set limit to 0 to disable it.')
+ = s_('AdminSettings|By default, set a limit to 0 to have no limit.')
.scrolling-tabs-container.inner-page-scroll-tabs
- if @plans.size > 1
%ul.nav-links.scrolling-tabs.mobile-separator.nav.nav-tabs.gl-mb-5
@@ -94,10 +94,14 @@
.form-group
= f.label :ci_needs_size_limit, s_('AdminSettings|Maximum number of DAG dependencies that a job can have')
= f.number_field :ci_needs_size_limit, class: 'form-control gl-form-input'
+ .form-text.text-muted= s_('AdminSettings|This limit cannot be disabled. Set to 0 to block all DAG dependencies.')
.form-group
= f.label :ci_registered_group_runners, s_('AdminSettings|Maximum number of runners registered per group')
= f.number_field :ci_registered_group_runners, class: 'form-control gl-form-input'
.form-group
= f.label :ci_registered_project_runners, s_('AdminSettings|Maximum number of runners registered per project')
= f.number_field :ci_registered_project_runners, class: 'form-control gl-form-input'
+ .form-group
+ = f.label :pipeline_hierarchy_size, s_("AdminSettings|Maximum number of downstream pipelines in a pipeline's hierarchy tree")
+ = f.number_field :pipeline_hierarchy_size, class: 'form-control gl-form-input'
= f.submit s_('AdminSettings|Save %{name} limits').html_safe % { name: plan.name.capitalize }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_default_branch.html.haml b/app/views/admin/application_settings/_default_branch.html.haml
index 7be4bac02fd..67de5ffb2b9 100644
--- a/app/views/admin/application_settings/_default_branch.html.haml
+++ b/app/views/admin/application_settings/_default_branch.html.haml
@@ -8,7 +8,7 @@
= f.label :default_branch_name, _('Initial default branch name'), class: 'label-light'
= f.text_field :default_branch_name, placeholder: Gitlab::DefaultBranch.value, class: 'form-control gl-form-input'
%span.form-text.text-muted
- = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name } ).html_safe
+ = (s_("AdminSettings|If not specified at the group or instance level, the default is %{default_initial_branch_name}. Does not affect existing repositories.") % { default_initial_branch_name: fallback_branch_name }).html_safe
= render 'shared/default_branch_protection', f: f
diff --git a/app/views/admin/application_settings/_error_tracking.html.haml b/app/views/admin/application_settings/_error_tracking.html.haml
index 5a8aba5784e..aa42cd99e89 100644
--- a/app/views/admin/application_settings/_error_tracking.html.haml
+++ b/app/views/admin/application_settings/_error_tracking.html.haml
@@ -37,4 +37,4 @@
= f.label :error_tracking_api_url, _('Opstrace endpoint for Error Tracking integration'), class: 'label-light'
= f.text_field :error_tracking_api_url, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_git_lfs_limits.html.haml b/app/views/admin/application_settings/_git_lfs_limits.html.haml
index b8970a5bcf1..638984ae97a 100644
--- a/app/views/admin/application_settings/_git_lfs_limits.html.haml
+++ b/app/views/admin/application_settings/_git_lfs_limits.html.haml
@@ -15,4 +15,4 @@
= f.label :throttle_authenticated_git_lfs_period_in_seconds, _('Authenticated Git LFS rate limit period in seconds'), class: 'gl-font-weight-bold'
= f.number_field :throttle_authenticated_git_lfs_period_in_seconds, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_grafana.html.haml b/app/views/admin/application_settings/_grafana.html.haml
index 7f305b9ad9c..e2a53106cec 100644
--- a/app/views/admin/application_settings/_grafana.html.haml
+++ b/app/views/admin/application_settings/_grafana.html.haml
@@ -11,4 +11,4 @@
= f.text_field :grafana_url, class: 'form-control gl-form-input', placeholder: '/-/grafana'
%span.form-text.text-muted#support_help_block= _('URL of the Grafana instance to link to from the Metrics Dashboard menu item.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_kroki.html.haml b/app/views/admin/application_settings/_kroki.html.haml
index 4f5a313d7b7..f1f6dd34401 100644
--- a/app/views/admin/application_settings/_kroki.html.haml
+++ b/app/views/admin/application_settings/_kroki.html.haml
@@ -24,7 +24,7 @@
- install_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: install_link_url }
= html_escape(_('Use the public cloud instance URL (%{kroki_public_url}) or %{install_link_start}install Kroki%{install_link_end} on your own infrastructure and use your own instance URL.')) % { kroki_public_url: '<code>https://kroki.io</code>'.html_safe, install_link_start: install_link_start, install_link_end: '</a>'.html_safe }
.form-group
- = f.label :kroki_formats, 'Additional diagram formats', class: 'label-bold'
+ = f.label :kroki_formats, _('Additional diagram formats'), class: 'label-bold'
.form-text.text-muted
- container_link_url = 'https://docs.kroki.io/kroki/setup/install/#images'
- container_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: container_link_url }
diff --git a/app/views/admin/application_settings/_localization.html.haml b/app/views/admin/application_settings/_localization.html.haml
index 90cb34395d8..9ec4afec484 100644
--- a/app/views/admin/application_settings/_localization.html.haml
+++ b/app/views/admin/application_settings/_localization.html.haml
@@ -15,5 +15,12 @@
- time_tracking_help_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: time_tracking_help_link }
= f.gitlab_ui_checkbox_component :time_tracking_limit_to_hours, _('Limit display of time tracking units to hours.'), help_text: _('Display time tracking in issues in total hours only. %{link_start}What is time tracking?%{link_end}').html_safe % { link_start: time_tracking_help_link_start, link_end: '</a>'.html_safe }
+ .form-group
+ = f.label :default_preferred_language, class: 'label-bold' do
+ = _('Default language')
+ = f.select :default_preferred_language, default_preferred_language_choices, {}, class: 'gl-form-select custom-select'
+ .form-text.text-muted
+ = s_('Default language for users who are not logged in.')
+
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml
index 1604419869c..fb15f6e79a5 100644
--- a/app/views/admin/application_settings/_mailgun.html.haml
+++ b/app/views/admin/application_settings/_mailgun.html.haml
@@ -19,4 +19,4 @@
= f.label :mailgun_signing_key, _('Mailgun HTTP webhook signing key'), class: 'label-light'
= f.text_field :mailgun_signing_key, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 3505a3bf3ee..1821c8ef4bb 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -20,6 +20,6 @@
.form-group
= f.gitlab_ui_checkbox_component :dns_rebinding_protection_enabled,
s_('OutboundRequests|Enforce DNS rebinding attack protection'),
- help_text: _('OutboundRequests|Resolve IP addresses once and uses them to submit requests.')
+ help_text: s_('OutboundRequests|Resolve IP addresses once and uses them to submit requests.')
= f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_performance_bar.html.haml b/app/views/admin/application_settings/_performance_bar.html.haml
index d4f6d84ea74..c09ba01b7ed 100644
--- a/app/views/admin/application_settings/_performance_bar.html.haml
+++ b/app/views/admin/application_settings/_performance_bar.html.haml
@@ -4,7 +4,7 @@
%fieldset
.form-group
= f.gitlab_ui_checkbox_component :performance_bar_enabled,
- s_("Allow non-administrators access to the performance bar"),
+ _("Allow non-administrators access to the performance bar"),
checkbox_options: { data: { qa_selector: 'enable_performance_bar_checkbox' } }
.form-group
= f.label :performance_bar_allowed_group_path, _('Allow access to members of the following group'), class: 'label-bold'
diff --git a/app/views/admin/application_settings/_plantuml.html.haml b/app/views/admin/application_settings/_plantuml.html.haml
index 5c86ce8dbfb..42f289d87b2 100644
--- a/app/views/admin/application_settings/_plantuml.html.haml
+++ b/app/views/admin/application_settings/_plantuml.html.haml
@@ -22,4 +22,4 @@
.form-text.text-muted
= _('The hostname of your PlantUML server.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_check.html.haml b/app/views/admin/application_settings/_repository_check.html.haml
index aaf76c5ff7a..332d3a94b92 100644
--- a/app/views/admin/application_settings/_repository_check.html.haml
+++ b/app/views/admin/application_settings/_repository_check.html.haml
@@ -19,28 +19,35 @@
%h4= _("Housekeeping")
.form-group
- help_text = _("Run housekeeping tasks to automatically optimize Git repositories. Disabling this option will cause performance to degenerate over time.")
- - help_link = link_to s_('Learn more.'), help_page_path('administration/housekeeping.md', anchor: 'configure-push-based-maintenance'), target: '_blank', rel: 'noopener noreferrer'
+ - help_link = link_to _('Learn more.'), help_page_path('administration/housekeeping.md', anchor: 'configure-push-based-maintenance'), target: '_blank', rel: 'noopener noreferrer'
= f.gitlab_ui_checkbox_component :housekeeping_enabled,
_("Enable automatic repository housekeeping"),
help_text: '%{help_text} %{help_link}'.html_safe % { help_text: help_text, help_link: help_link }
- .form-group
- = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-bold'
- = f.number_field :housekeeping_incremental_repack_period, class: 'form-control gl-form-input'
- .form-text.text-muted
- = html_escape(s_('Number of Git pushes after which an incremental %{code_start}git repack%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
- .form-group
- = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-bold'
- = f.number_field :housekeeping_full_repack_period, class: 'form-control gl-form-input'
- .form-text.text-muted
- = html_escape(s_('Number of Git pushes after which a full %{code_start}git repack%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
- .form-group
- = f.label :housekeeping_gc_period, _('Git GC period'), class: 'label-bold'
- = f.number_field :housekeeping_gc_period, class: 'form-control gl-form-input'
- .form-text.text-muted
- = html_escape(s_('Number of Git pushes after which %{code_start}git gc%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
+ - if Feature.enabled?(:optimized_housekeeping)
+ .form-group
+ = f.label :housekeeping_incremental_repack_period, _('Optimize repository period'), class: 'label-bold'
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ = _('Number of Git pushes after which Gitaly is asked to optimize a repository.')
+ - else
+ .form-group
+ = f.label :housekeeping_incremental_repack_period, 'Incremental repack period', class: 'label-bold'
+ = f.number_field :housekeeping_incremental_repack_period, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ = html_escape(s_('Number of Git pushes after which an incremental %{code_start}git repack%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
+ .form-group
+ = f.label :housekeeping_full_repack_period, 'Full repack period', class: 'label-bold'
+ = f.number_field :housekeeping_full_repack_period, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ = html_escape(s_('Number of Git pushes after which a full %{code_start}git repack%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
+ .form-group
+ = f.label :housekeeping_gc_period, _('Git GC period'), class: 'label-bold'
+ = f.number_field :housekeeping_gc_period, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ = html_escape(s_('Number of Git pushes after which %{code_start}git gc%{code_end} is run.')) % { code_start: '<code>'.html_safe, code_end: '</code>'.html_safe }
.sub-section
%h4= s_("AdminSettings|Inactive project deletion")
.js-inactive-project-deletion-form{ data: inactive_projects_deletion_data(@application_setting) }
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_static_objects.html.haml b/app/views/admin/application_settings/_repository_static_objects.html.haml
index d962d050ebc..b301ec15a0e 100644
--- a/app/views/admin/application_settings/_repository_static_objects.html.haml
+++ b/app/views/admin/application_settings/_repository_static_objects.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-static-objects-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: repository_admin_application_settings_path(anchor: 'js-repository-static-objects-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -15,4 +15,4 @@
%span.form-text.text-muted#static_objects_external_storage_auth_token_help_block
= _('Secure token that identifies an external storage request.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_repository_storage.html.haml b/app/views/admin/application_settings/_repository_storage.html.haml
index 12dd8816783..066d77c792b 100644
--- a/app/views/admin/application_settings/_repository_storage.html.haml
+++ b/app/views/admin/application_settings/_repository_storage.html.haml
@@ -20,7 +20,7 @@
- weights_link_url = help_page_path('administration/repository_storage_paths.md', anchor: 'configure-where-new-repositories-are-stored')
- weights_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: weights_link_url }
= html_escape(s_('Enter %{weights_link_start}weights%{weights_link_end} for storages for new repositories. Configured storages appear below.')) % { weights_link_start: weights_link_start, weights_link_end: '</a>'.html_safe }
- = link_to s_('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
.form-check
= f.fields_for :repository_storages_weighted, storage_weights do |storage_form|
- Gitlab.config.repositories.storages.each_key do |storage|
diff --git a/app/views/admin/application_settings/_runner_registrars_form.html.haml b/app/views/admin/application_settings/_runner_registrars_form.html.haml
index 1d6051a06ea..08486a808bf 100644
--- a/app/views/admin/application_settings/_runner_registrars_form.html.haml
+++ b/app/views/admin/application_settings/_runner_registrars_form.html.haml
@@ -5,7 +5,7 @@
.gl-form-group
%span.form-text.gl-mb-3.gl-mt-0
= _('If no options are selected, only administrators can register runners.')
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'prevent-users-from-registering-runners'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
= hidden_field_tag "application_setting[valid_runner_registrars][]", nil
- ApplicationSetting::VALID_RUNNER_REGISTRAR_TYPES.each do |type|
= f.gitlab_ui_checkbox_component :valid_runner_registrars, s_("Runners|Members of the %{type} can register runners") % { type: type },
@@ -13,4 +13,4 @@
checked_value: type,
unchecked_value: nil
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = 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 945c9397f0d..396c263dd5d 100644
--- a/app/views/admin/application_settings/_search_limits.html.haml
+++ b/app/views/admin/application_settings/_search_limits.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-search-limits-settings'), html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-search-limits-settings'), html: { class: 'fieldset-form' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -13,4 +13,4 @@
= f.number_field :search_rate_limit_unauthenticated, class: 'form-control gl-form-input'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index bb512940be2..96face44344 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -50,8 +50,7 @@
= f.label :akismet_api_key, _('Akismet API Key'), class: 'label-bold'
= f.text_field :akismet_api_key, class: 'form-control gl-form-input'
.form-text.text-muted
- Generate API key at
- %a{ href: 'http://www.akismet.com', target: 'blank', rel: 'noopener noreferrer' } http://www.akismet.com
+ = _("Generate API key at %{site}").html_safe % { site: link_to('http://www.akismet.com', 'http://www.akismet.com', target: 'blank', ref: 'noopener noreferrer') }
%h5
= _('IP address restrictions')
@@ -86,4 +85,4 @@
= f.text_field :spam_check_api_key, class: 'form-control gl-form-input'
.form-text.text-muted= _('The API key used by GitLab for accessing the Spam Check service endpoint.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index c53f63e124b..b07db09d06c 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -1,4 +1,4 @@
-= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f|
+= gitlab_ui_form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-terminal-settings'), html: { class: 'fieldset-form', id: 'terminal-settings' } do |f|
= form_errors(@application_setting)
%fieldset
@@ -7,4 +7,4 @@
= f.number_field :terminal_max_session_time, class: 'form-control gl-form-input'
.form-text.text-muted
= _('Maximum time, in seconds, for a web terminal websocket connection. 0 for unlimited.')
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/_terraform_limits.html.haml b/app/views/admin/application_settings/_terraform_limits.html.haml
new file mode 100644
index 00000000000..bdb0ba5cc85
--- /dev/null
+++ b/app/views/admin/application_settings/_terraform_limits.html.haml
@@ -0,0 +1,11 @@
+= gitlab_ui_form_for @application_setting, url: preferences_admin_application_settings_path(anchor: 'js-terraform-limits-settings'), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ .form-group
+ = f.label :max_terraform_state_size_bytes, s_('TerraformLimits|Terraform state size limit (bytes)'), class: 'label-bold'
+ = f.number_field :max_terraform_state_size_bytes, class: 'form-control gl-form-input'
+ .form-text.text-muted
+ = s_("TerraformLimits|Maximum file size (in bytes) of Terraform state files. Set to 0 for no limit.")
+
+ = 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 85bee72e863..9c8770b8998 100644
--- a/app/views/admin/application_settings/_visibility_and_access.html.haml
+++ b/app/views/admin/application_settings/_visibility_and_access.html.haml
@@ -36,10 +36,14 @@
= render_if_exists 'admin/application_settings/ldap_access_setting', form: f
- .form-group
+ .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|Enable 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')
@@ -67,4 +71,4 @@
-# This is added for Jihu edition in https://jihulab.com/gitlab-cn/gitlab/-/merge_requests/1112
= render_if_exists 'admin/application_settings/disable_download_button', f: f
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
index 415606c055d..d7bb3a85f3a 100644
--- a/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
+++ b/app/views/admin/application_settings/appearances/_system_header_footer_form.html.haml
@@ -20,7 +20,7 @@
label_options: { class: 'gl-font-weight-bold!' }
.form-group.js-toggle-colors-container
- %button.btn.gl-button.btn-link.js-toggle-colors-link{ type: 'button' }
+ = render Pajamas::ButtonComponent.new(variant: :link, button_options: { class: 'js-toggle-colors-link' }) do
= _('Customize colors')
.form-group.js-toggle-colors-container.hide
= form.label :message_background_color, _('Background Color'), class: 'col-form-label label-bold'
diff --git a/app/views/admin/application_settings/appearances/show.html.haml b/app/views/admin/application_settings/appearances/show.html.haml
index 77a08913666..1e55190d53b 100644
--- a/app/views/admin/application_settings/appearances/show.html.haml
+++ b/app/views/admin/application_settings/appearances/show.html.haml
@@ -1,4 +1,5 @@
- page_title _("Appearance")
- @content_class = "limit-container-width" unless fluid_layout
+- add_page_specific_style 'page_bundles/settings'
= render 'form'
diff --git a/app/views/admin/application_settings/ci/_header.html.haml b/app/views/admin/application_settings/ci/_header.html.haml
index 0adb6cbbcf0..79c07f491fc 100644
--- a/app/views/admin/application_settings/ci/_header.html.haml
+++ b/app/views/admin/application_settings/ci/_header.html.haml
@@ -8,12 +8,13 @@
%p
= _('Variables store information, like passwords and secret keys, that you can use in job scripts. All projects on the instance can use these variables.')
- = link_to s_('Learn more.'), help_page_path('ci/variables/index', anchor: 'add-a-cicd-variable-to-an-instance'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'add-a-cicd-variable-to-an-instance'), target: '_blank', rel: 'noopener noreferrer'
%p
= _('Variables can be:')
%ul
%li
= html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or protected tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
+ = link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'protected-cicd-variables'), target: '_blank', rel: 'noopener noreferrer'
%li
= html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
= link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/admin/application_settings/ci_cd.html.haml b/app/views/admin/application_settings/ci_cd.html.haml
index b7244c45871..0414382a108 100644
--- a/app/views/admin/application_settings/ci_cd.html.haml
+++ b/app/views/admin/application_settings/ci_cd.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("CI/CD")
- page_title _("CI/CD")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.no-animate#js-ci-cd-variables{ class: ('expanded' if expanded_by_default?) }
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 6d8428d1aa6..8c9d54cd5d8 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("General")
- page_title _("General")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-visibility-access.no-animate#js-visibility-settings{ class: ('expanded' if expanded_by_default?) }
@@ -124,4 +125,4 @@
= render 'admin/application_settings/eks'
= render 'admin/application_settings/floc'
= render_if_exists 'admin/application_settings/add_license'
-= render 'admin/application_settings/jira_connect' if Feature.enabled?(:jira_connect_oauth_self_managed_setting, current_user)
+= render 'admin/application_settings/jira_connect'
diff --git a/app/views/admin/application_settings/integrations.html.haml b/app/views/admin/application_settings/integrations.html.haml
index d818c587b79..fd1ad5cd304 100644
--- a/app/views/admin/application_settings/integrations.html.haml
+++ b/app/views/admin/application_settings/integrations.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title s_('Integrations|Instance-level integration management')
- page_title s_('Integrations|Instance-level integration management')
+- add_page_specific_style 'page_bundles/settings'
- @content_class = 'limit-container-width' unless fluid_layout
%h3= s_('Integrations|Instance-level integration management')
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index b79b189e9cf..b5981578866 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -2,6 +2,7 @@
- breadcrumb_title _("Metrics and profiling")
- page_title _("Metrics and profiling")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-prometheus.no-animate#js-prometheus-settings{ class: ('expanded' if expanded_by_default?) }
@@ -23,7 +24,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Link to your Grafana instance.')
- = link_to s_('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/grafana_configuration.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'grafana'
@@ -36,7 +37,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Enable access to the performance bar for non-administrators in a given group.')
- = link_to s_('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar.md', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/monitoring/performance/performance_bar.md', anchor: 'enable-the-performance-bar-for-non-administrators'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'performance_bar'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 485b3a9828b..779263b439f 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Network")
- page_title _("Network")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-performance.no-animate#js-performance-settings{ class: ('expanded' if expanded_by_default?) }
diff --git a/app/views/admin/application_settings/preferences.html.haml b/app/views/admin/application_settings/preferences.html.haml
index bd92f7d490c..dd6666542ca 100644
--- a/app/views/admin/application_settings/preferences.html.haml
+++ b/app/views/admin/application_settings/preferences.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Preferences")
- page_title _("Preferences")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-email.no-animate#js-email-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'email_content' } }
@@ -32,7 +33,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Additional text for the sign-in and Help page.')
- = link_to s_('Learn more.'), help_page_path('user/admin_area/settings/help_page.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/help_page.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'help_page'
@@ -79,7 +80,7 @@
= render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Configure the default first day of the week and time tracking units.')
+ = _('Configure the default first day of the week, time tracking units, and default language.')
.settings-content
= render 'localization'
@@ -96,3 +97,15 @@
.settings-content
= render 'sidekiq_job_limits'
+
+%section.settings.as-terraform-limits.no-animate#js-terraform-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
+ = s_('TerraformLimits|Terraform limits')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-settings-toggle' }) do
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = s_('TerraformLimits|Limits for Terraform features')
+ = link_to s_('TerraformLimits|Learn more about Terraform limits.'), help_page_path('user/admin_area/settings/terraform_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render 'terraform_limits'
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index af9145bf1e7..3d803e95cd0 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Reporting")
- page_title _("Reporting")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-spam.no-animate#js-spam-settings{ class: ('expanded' if expanded_by_default?) }
diff --git a/app/views/admin/application_settings/repository.html.haml b/app/views/admin/application_settings/repository.html.haml
index 12063ea700b..50798ad476c 100644
--- a/app/views/admin/application_settings/repository.html.haml
+++ b/app/views/admin/application_settings/repository.html.haml
@@ -1,5 +1,6 @@
- breadcrumb_title _("Repository")
- page_title _("Repository")
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
%section.settings.as-default-branch-name.no-animate#js-default-branch-name{ class: ('expanded' if expanded_by_default?) }
@@ -21,7 +22,7 @@
= expanded_by_default? ? 'Collapse' : 'Expand'
%p
= _('Configure repository mirroring.')
- = link_to s_('Learn more.'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render partial: 'repository_mirrors_form'
@@ -33,7 +34,7 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Configure repository storage.')
- = link_to s_('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/repository_storage_paths.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_storage'
@@ -60,6 +61,6 @@
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
= _('Serve repository static objects (for example, archives and blobs) from external storage.')
- = link_to s_('Learn more.'), help_page_path('administration/static_objects_external_storage.md'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('administration/static_objects_external_storage.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'repository_static_objects'
diff --git a/app/views/admin/application_settings/service_usage_data.html.haml b/app/views/admin/application_settings/service_usage_data.html.haml
index 82b627e1805..d6860cc08ac 100644
--- a/app/views/admin/application_settings/service_usage_data.html.haml
+++ b/app/views/admin/application_settings/service_usage_data.html.haml
@@ -2,6 +2,7 @@
- breadcrumb_title name
- page_title name
+- add_page_specific_style 'page_bundles/settings'
- @content_class = "limit-container-width" unless fluid_layout
- payload_class = 'js-service-ping-payload'
@@ -9,10 +10,10 @@
%h3= name
- if @service_ping_data_present
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-preview-trigger gl-mr-2', data: { payload_selector: ".#{payload_class}" } }) do
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
%span.js-text.gl-display-inline= _('Preview payload')
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-payload-download-trigger gl-mr-2', data: { endpoint: usage_data_admin_application_settings_path(format: :json) } }) do
= gl_loading_icon(css_class: 'js-spinner gl-display-none', inline: true)
%span.js-text.gl-display-inline= _('Download payload')
%pre.js-syntax-highlight.code.highlight.gl-mt-2.gl-display-none{ class: payload_class, data: { endpoint: usage_data_admin_application_settings_path(format: :html) } }
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index b603c7e5f49..a92bad5e601 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -14,11 +14,13 @@
.gl-max-w-full.gl-m-auto
%h1.h4.gl-font-size-h-display= s_('AdminArea|No applications found')
- = link_to _('New application'), new_admin_application_path, class: 'btn gl-button btn-confirm'
+ = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm) do
+ = s_('New application')
- else
%hr
- %p= link_to _('New application'), new_admin_application_path, class: 'gl-button btn btn-confirm'
+ = render Pajamas::ButtonComponent.new(href: new_admin_application_path, variant: :confirm) do
+ = s_('New application')
.table-responsive
%table.b-table.gl-table.gl-w-full{ role: 'table' }
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index dfd3b87c674..4e05eb31010 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -17,14 +17,14 @@
= f.label :broadcast_type, _('Type')
.col-sm-10
= f.select :broadcast_type, broadcast_type_options, {}, class: 'form-control js-broadcast-message-type'
- .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner? ) }
+ .form-group.row.js-broadcast-message-background-color-form-group{ class: ('hidden' unless @broadcast_message.banner?) }
.col-sm-2.col-form-label
= f.label :theme, _("Theme")
.col-sm-10
.input-group
= f.select :theme, broadcast_theme_options, {}, class: 'form-control js-broadcast-message-theme'
- .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner? ) }
+ .form-group.row.js-broadcast-message-dismissable-form-group{ class: ('hidden' unless @broadcast_message.banner?) }
.col-sm-2.col-form-label.pt-0
= f.label :starts_at, _("Dismissable")
.col-sm-10
@@ -62,6 +62,6 @@
= f.datetime_select :ends_at, {}, class: 'form-control form-control-inline'
.form-actions
- if @broadcast_message.persisted?
- = f.submit _("Update broadcast message"), class: "btn gl-button btn-confirm"
+ = f.submit _("Update broadcast message"), pajamas_button: true
- else
- = f.submit _("Add broadcast message"), class: "btn gl-button btn-confirm"
+ = f.submit _("Add broadcast message"), pajamas_button: true
diff --git a/app/views/admin/broadcast_messages/edit.html.haml b/app/views/admin/broadcast_messages/edit.html.haml
index 569aaa29cc4..28301833f7d 100644
--- a/app/views/admin/broadcast_messages/edit.html.haml
+++ b/app/views/admin/broadcast_messages/edit.html.haml
@@ -1,4 +1,19 @@
- breadcrumb_title _("Messages")
- page_title _("Broadcast Messages")
+- vue_app_enabled = Feature.enabled?(:vue_broadcast_messages, current_user)
-= render 'form'
+- if vue_app_enabled
+ #js-broadcast-message{ data: {
+ id: @broadcast_message.id,
+ message: @broadcast_message.message,
+ broadcast_type: @broadcast_message.broadcast_type,
+ theme: @broadcast_message.theme,
+ dismissable: @broadcast_message.dismissable.to_s,
+ target_access_levels: @broadcast_message.target_access_levels,
+ target_path: @broadcast_message.target_path,
+ starts_at: @broadcast_message.starts_at,
+ ends_at: @broadcast_message.ends_at,
+ target_access_level_options: target_access_level_options.to_json,
+ } }
+- else
+ = render 'form'
diff --git a/app/views/admin/broadcast_messages/index.html.haml b/app/views/admin/broadcast_messages/index.html.haml
index 7559365e49a..7a005f9c982 100644
--- a/app/views/admin/broadcast_messages/index.html.haml
+++ b/app/views/admin/broadcast_messages/index.html.haml
@@ -10,6 +10,7 @@
- if vue_app_enabled
#js-broadcast-messages{ data: {
page: params[:page] || 1,
+ target_access_level_options: target_access_level_options.to_json,
messages_count: @broadcast_messages.total_count,
messages: @broadcast_messages.map { |message| {
id: message.id,
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
index 76bfa347480..7495298936d 100644
--- a/app/views/admin/dashboard/_security_newsletter_callout.html.haml
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -10,5 +10,5 @@
= c.body do
= s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
= c.actions do
- = link_to 'https://about.gitlab.com/company/preference-center/', target: '_blank', rel: 'noreferrer noopener', class: 'deferred-link gl-alert-action btn-confirm btn-md gl-button' do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://about.gitlab.com/company/preference-center/', target: '_blank', button_options: { class: 'deferred-link gl-alert-action', rel: 'noreferrer noopener' }) do
= s_('AdminArea|Sign up for the GitLab newsletter')
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 886402139e9..27ae7d523b9 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -32,7 +32,8 @@
= sprite_icon('project', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Project)
.gl-mt-3.text-uppercase= s_('AdminArea|Projects')
- = link_to(s_('AdminArea|New project'), new_project_path, class: "btn gl-button btn-default")
+ = render Pajamas::ButtonComponent.new(href: new_project_path) do
+ = s_('AdminArea|New project')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest projects'), admin_projects_path(sort: 'created_desc'))
@@ -55,7 +56,8 @@
.gl-mt-3.text-uppercase
= s_('AdminArea|Users')
= link_to(s_('AdminArea|Users statistics'), admin_dashboard_stats_path, class: "text-capitalize gl-ml-2")
- = link_to(s_('AdminArea|New user'), new_admin_user_path, class: "btn gl-button btn-default")
+ = render Pajamas::ButtonComponent.new(href: new_admin_user_path) do
+ = s_('AdminArea|New user')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest users'), admin_users_path({ sort: 'created_desc' }))
@@ -68,7 +70,8 @@
= sprite_icon('group', size: 16, css_class: 'gl-text-gray-700')
%h3.gl-m-0.gl-ml-3= approximate_count_with_delimiters(@counts, Group)
.gl-mt-3.text-uppercase= s_('AdminArea|Groups')
- = link_to(s_('AdminArea|New group'), new_admin_group_path, class: "btn gl-button btn-default")
+ = render Pajamas::ButtonComponent.new(href: new_admin_group_path) do
+ = s_('AdminArea|New group')
= c.footer do
.d-flex.align-items-center
= link_to(s_('AdminArea|View latest groups'), admin_groups_path(sort: 'created_desc'))
@@ -122,7 +125,7 @@
= s_('AdminArea|Components')
- if show_version_check?
.float-right
- .js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true" } }
+ .js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true", "version": gitlab_version_check.to_json } }
= link_to(sprite_icon('question'), "https://gitlab.com/gitlab-org/gitlab/-/blob/master/CHANGELOG.md", class: 'gl-ml-2', target: '_blank', rel: 'noopener noreferrer')
%p
= link_to _('GitLab'), general_admin_application_settings_path
diff --git a/app/views/admin/deploy_keys/edit.html.haml b/app/views/admin/deploy_keys/edit.html.haml
index acdf503727d..b51ab3457d6 100644
--- a/app/views/admin/deploy_keys/edit.html.haml
+++ b/app/views/admin/deploy_keys/edit.html.haml
@@ -7,4 +7,5 @@
= render partial: 'shared/deploy_keys/form', locals: { form: f, deploy_key: @deploy_key }
.form-actions
= f.submit _('Save changes'), pajamas_button: true
- = link_to _('Cancel'), admin_deploy_keys_path, class: 'btn gl-button btn-default btn-cancel'
+ = render Pajamas::ButtonComponent.new(href: admin_deploy_keys_path) do
+ = _('Cancel')
diff --git a/app/views/admin/groups/_form.html.haml b/app/views/admin/groups/_form.html.haml
index 7adba0d023b..20ee8c9f310 100644
--- a/app/views/admin/groups/_form.html.haml
+++ b/app/views/admin/groups/_form.html.haml
@@ -36,9 +36,11 @@
= render 'shared/group_tips'
.gl-mt-5
= f.submit _('Create group'), pajamas_button: true
- = link_to _('Cancel'), admin_groups_path, class: "gl-button btn btn-default btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: admin_groups_path) do
+ = _('Cancel')
- else
.gl-mt-5
= f.submit _('Save changes'), data: { qa_selector: 'save_changes_button' }, pajamas_button: true
- = link_to _('Cancel'), admin_group_path(@group), class: "gl-button btn btn-cancel"
+ = 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 a1afb1ddbfa..f9ebda2bc21 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -31,5 +31,7 @@
= visibility_level_icon(group.visibility_level)
.controls.gl-flex-shrink-0.gl-ml-5
- = link_to _('Edit'), admin_group_edit_path(group), id: "edit_#{dom_id(group)}", class: 'btn gl-button btn-default'
- = link_to _('Delete'), [:admin, group], aria: { label: _('Remove') }, data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, confirm_btn_variant: 'danger' }, method: :delete, class: 'gl-button btn btn-danger'
+ = render Pajamas::ButtonComponent.new(href: admin_group_edit_path(group), button_options: { id: "edit_#{dom_id(group)}" }) do
+ = _('Edit')
+ = render Pajamas::ButtonComponent.new(href: [:admin, group], variant: :danger, button_options: { data: { confirm: _("Are you sure you want to remove %{group_name}?") % { group_name: group.name }, confirm_btn_variant: 'danger', method: :delete } }) do
+ = _('Delete')
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 2ea5890be2c..2a49b9c5ad8 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -1,4 +1,5 @@
- page_title _("Groups")
+- add_page_specific_style 'page_bundles/search'
.top-area
.gl-mt-3.gl-mb-3
@@ -9,7 +10,7 @@
= 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' }
= sprite_icon('search', css_class: 'search-icon')
= render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
- = link_to new_admin_group_path, class: "gl-button btn btn-confirm" do
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_group_path) do
= _('New group')
%ul.content-list
= render @groups
diff --git a/app/views/admin/hook_logs/show.html.haml b/app/views/admin/hook_logs/show.html.haml
index 6fcaf2ea152..0ccde159905 100644
--- a/app/views/admin/hook_logs/show.html.haml
+++ b/app/views/admin/hook_logs/show.html.haml
@@ -5,8 +5,11 @@
%hr
- if @hook_log.oversize?
- = button_tag _("Resend Request"), class: "btn gl-button btn-default float-right gl-ml-3 has-tooltip", disabled: true, title: _("Request data is too large")
+ - tooltip = _("Request data is too large")
+ = render Pajamas::ButtonComponent.new(disabled: true, button_options: { class: 'gl-float-right gl-ml-3 has-tooltip', title: tooltip }) do
+ = _("Resend Request")
- else
- = link_to _("Resend Request"), retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
+ = render Pajamas::ButtonComponent.new(href: retry_admin_hook_hook_log_path(@hook, @hook_log), method: :post, button_options: { class: 'gl-float-right gl-ml-3' }) do
+ = _("Resend Request")
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index ba7687db9c7..ad78c677da1 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -1,4 +1,4 @@
-= form_for [:admin, @user, @identity], html: { class: 'fieldset-form' } do |f|
+= gitlab_ui_form_for [:admin, @user, @identity], html: { class: 'fieldset-form' } do |f|
= form_errors(@identity)
.form-group.row
@@ -14,5 +14,5 @@
= f.text_field :extern_uid, class: 'form-control', required: true
.form-actions
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm"
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 3121cd2ae59..a24cd000464 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -14,11 +14,11 @@
icon: 'pencil',
button_options: { title: _('Edit'),
'aria-label' => _('Edit'),
- class: button_classes } )
+ class: button_classes })
= render Pajamas::ButtonComponent.new(category: :tertiary,
href: url_for([:admin, @user, identity]),
icon: 'remove',
button_options: { title: _('Delete'),
'aria-label' => _('Delete identity'),
class: button_classes,
- data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } } )
+ data: { method: :delete, confirm: _("Are you sure you want to remove this identity?") } })
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
index 21b19236683..d6f2898a383 100644
--- a/app/views/admin/labels/index.html.haml
+++ b/app/views/admin/labels/index.html.haml
@@ -27,6 +27,5 @@
%p
= s_('AdminLabels|They can be used to categorize issues and merge requests.')
.gl-display-flex.gl-flex-wrap.gl-justify-content-center
- = link_to new_admin_label_path, class: "btn gl-mb-3 btn-confirm btn-md gl-button gl-mx-2" do
- %span.gl-button-text
- = _('New label')
+ = render Pajamas::ButtonComponent.new(href: new_admin_label_path) do
+ = _('New label')
diff --git a/app/views/admin/projects/_projects.html.haml b/app/views/admin/projects/_projects.html.haml
index c7c30673d74..cf1bd2a8022 100644
--- a/app/views/admin/projects/_projects.html.haml
+++ b/app/views/admin/projects/_projects.html.haml
@@ -25,8 +25,8 @@
.controls.gl-flex-shrink-0.gl-ml-5
= render Pajamas::ButtonComponent.new(href: edit_project_path(project), button_options: { id: dom_id(project, :edit) }) do
- = s_('Edit')
- = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } } ) do
+ = _('Edit')
+ = render Pajamas::ButtonComponent.new(variant: :danger, button_options: { class: 'delete-project-button', data: { delete_project_url: admin_project_path(project), project_name: project.name } }) do
= s_('AdminProjects|Delete')
= paginate @projects, theme: 'gitlab'
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index f23a688dd48..18cd3400c60 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,4 +1,5 @@
- page_title _('Projects')
+- add_page_specific_style 'page_bundles/search'
- params[:visibility_level] ||= []
.top-area
@@ -20,10 +21,9 @@
- namespace = Namespace.find(params[:namespace_id])
- current_namespace = "#{namespace.kind}: #{namespace.full_path}"
%button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { show_any: 'true', field_name: 'namespace_id', placeholder: current_namespace, update_location: 'true' }, type: 'button' }
- %span.gl-new-dropdown-button-text
+ %span.gl-dropdown-button-text
= current_namespace
- = render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-confirm' do
= _('New Project')
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index a60c3996cf2..829e9f508e0 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -6,8 +6,7 @@
%h1.page-title.gl-font-size-h-display
= _('Project: %{name}') % { name: @project.full_name }
- = link_to edit_project_path(@project), class: "btn btn-default gl-button float-right" do
- = sprite_icon('pencil', css_class: 'gl-icon gl-mr-2')
+ = render Pajamas::ButtonComponent.new(href: edit_project_path(@project), icon: 'pencil', button_options: { class: 'gl-float-right' }) do
= _('Edit')
%hr
- if @project.last_repository_check_failed?
@@ -143,7 +142,7 @@
.col-sm-9
- placeholder = _('Search for Namespace')
%button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { field_name: 'new_namespace_id', placeholder: placeholder }, type: 'button' }
- %span.gl-new-dropdown-button-text
+ %span.gl-dropdown-button-text
= placeholder
.form-group.row
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
index 9b9d97950cc..544310e312c 100644
--- a/app/views/admin/topics/_form.html.haml
+++ b/app/views/admin/topics/_form.html.haml
@@ -38,10 +38,13 @@
- if @topic.new_record?
.form-actions
- = f.submit _('Create topic'), class: "gl-button btn btn-confirm"
- = link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-default btn-cancel"
+ = f.submit _('Create topic'), pajamas_button: true
+ = render Pajamas::ButtonComponent.new(href: admin_topics_path) do
+ = _('Cancel')
- else
.form-actions
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
- = link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-cancel"
+ = f.submit _('Save changes'), pajamas_button: true, data: { qa_selector: 'save_changes_button' }
+ = render Pajamas::ButtonComponent.new(href: admin_topics_path) do
+ = _('Cancel')
+
diff --git a/app/views/admin/topics/index.html.haml b/app/views/admin/topics/index.html.haml
index 77823ed7058..2f39f27208e 100644
--- a/app/views/admin/topics/index.html.haml
+++ b/app/views/admin/topics/index.html.haml
@@ -9,7 +9,7 @@
= sprite_icon('search', css_class: 'search-icon')
.gl-flex-grow-1
.js-merge-topics{ data: { path: merge_admin_topics_path } }
- = link_to new_admin_topic_path, class: "gl-button btn btn-confirm gl-w-full gl-md-w-auto" do
+ = render Pajamas::ButtonComponent.new(href: new_admin_topic_path, variant: 'confirm') do
= _('New topic')
%ul.content-list
= render partial: 'topic', collection: @topics
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 6809f147ef8..eb151b40a65 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -76,7 +76,9 @@
%div
- if @user.new_record?
= f.submit _('Create user'), pajamas_button: true
- = link_to _('Cancel'), admin_users_path, class: "gl-button btn btn-default btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: admin_users_path) do
+ = _('Cancel')
- else
= f.submit _('Save changes'), pajamas_button: true
- = link_to _('Cancel'), admin_user_path(@user), class: "gl-button btn btn-default btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: admin_user_path(@user)) do
+ = _('Cancel')
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 1fa7c9c8651..6b5ec62bc77 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -40,7 +40,8 @@
= render Pajamas::ButtonComponent.new(variant: :default, button_options: { class: 'js-confirm-modal-button', data: confirm_user_data(@user) }) do
= _('Confirm user')
.gl-p-2
- = link_to _('New identity'), new_admin_user_identity_path(@user), class: "btn btn-primary gl-button"
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: new_admin_user_identity_path(@user)) do
+ = _('New identity')
= gl_tabs_nav do
= gl_tab_link_to _("Account"), admin_user_path(@user)
= gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user)
diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml
index 6d85ff50fbe..96e6a264d8e 100644
--- a/app/views/admin/users/_users.html.haml
+++ b/app/views/admin/users/_users.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/search'
+
- if registration_features_can_be_prompted?
= render Pajamas::AlertComponent.new(variant: :tip,
alert_options: { class: 'gl-my-5' },
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 8f4cc41822b..cdf25a9348c 100644
--- a/app/views/ci/runner/_how_to_setup_runner.html.haml
+++ b/app/views/ci/runner/_how_to_setup_runner.html.haml
@@ -17,8 +17,7 @@
= clipboard_button(target: '#registration_token', title: _("Copy token"))
.gl-mt-3.gl-mb-3
-= button_to _("Reset registration token"), reset_token_url,
-method: :put, class: 'gl-button btn btn-default',
-data: { confirm: _("Are you sure you want to reset the registration token?") }
+= 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
+ = _('Reset registration token')
#js-install-runner
diff --git a/app/views/ci/variables/_content.html.haml b/app/views/ci/variables/_content.html.haml
index b597c2d442a..37043a207ff 100644
--- a/app/views/ci/variables/_content.html.haml
+++ b/app/views/ci/variables/_content.html.haml
@@ -1,10 +1,12 @@
-= _('Variables store information, like passwords and secret keys, that you can use in job scripts.')
-= link_to s_('Learn more.'), help_page_path('ci/variables/index'), target: '_blank', rel: 'noopener noreferrer'
+= format(s_('CiVariables|Variables store information, like passwords and secret keys, that you can use in job scripts. Each %{entity} can define a maximum of %{limit} variables.'), entity: entity, limit: variable_limit).html_safe
+= link_to _('Learn more.'), help_page_path('ci/variables/index'), target: '_blank', rel: 'noopener noreferrer'
%p
- = _('Variables can be:')
+ = _('Variables can have several attributes.')
+ = link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'add-a-cicd-variable-to-an-instance'), target: '_blank', rel: 'noopener noreferrer'
%ul
%li
= html_escape(_('%{code_open}Protected:%{code_close} Only exposed to protected branches or protected tags.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
%li
= html_escape(_('%{code_open}Masked:%{code_close} Hidden in job logs. Must match masking requirements.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
- = link_to _('Learn more.'), help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'), target: '_blank', rel: 'noopener noreferrer'
+ %li
+ = html_escape(_('%{code_open}Expanded:%{code_close} Variables with %{code_open}$%{code_close} will be treated as the start of a reference to another variable.')) % { code_open: '<code>'.html_safe, code_close: '</code>'.html_safe }
diff --git a/app/views/ci/variables/_header.html.haml b/app/views/ci/variables/_header.html.haml
index d6a9ce72d03..dfcf8f39533 100644
--- a/app/views/ci/variables/_header.html.haml
+++ b/app/views/ci/variables/_header.html.haml
@@ -7,4 +7,4 @@
= expanded ? _('Collapse') : _('Expand')
%p
- = render "ci/variables/content"
+ = render "ci/variables/content", entity: @entity, variable_limit: @variable_limit
diff --git a/app/views/ci/variables/_index.html.haml b/app/views/ci/variables/_index.html.haml
index 08865abbe86..fdbf5132d40 100644
--- a/app/views/ci/variables/_index.html.haml
+++ b/app/views/ci/variables/_index.html.haml
@@ -23,7 +23,7 @@
aws_tip_deploy_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'deploy-your-application-to-ecs'),
aws_tip_commands_link: help_page_path('ci/cloud_deployment/index.md', anchor: 'use-an-image-to-run-aws-commands'),
aws_tip_learn_link: help_page_path('ci/cloud_deployment/index.md'),
- contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'use-variables-in-other-variables'),
+ contains_variable_reference_link: help_page_path('ci/variables/index', anchor: 'expand-cicd-variables'),
protected_environment_variables_link: help_page_path('ci/variables/index', anchor: 'protected-cicd-variables'),
masked_environment_variables_link: help_page_path('ci/variables/index', anchor: 'mask-a-cicd-variable'),
environment_scope_link: help_page_path('ci/environments/index', anchor: 'scope-environments-with-specs') } }
diff --git a/app/views/ci/variables/_variable_row.html.haml b/app/views/ci/variables/_variable_row.html.haml
index c5e518d8526..a710655aa20 100644
--- a/app/views/ci/variables/_variable_row.html.haml
+++ b/app/views/ci/variables/_variable_row.html.haml
@@ -33,5 +33,4 @@
%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'
- %button.gl-button.btn.btn-default.btn-icon.js-row-remove-button.ci-variable-row-remove-button.table-section{ type: 'button', 'aria-label': s_('CiVariables|Remove variable row') }
- = sprite_icon('close')
+ = 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/_details.html.haml b/app/views/clusters/clusters/_details.html.haml
index 3079e024369..34408a9adeb 100644
--- a/app/views/clusters/clusters/_details.html.haml
+++ b/app/views/clusters/clusters/_details.html.haml
@@ -4,8 +4,6 @@
%section.settings.no-animate{ class: ('expanded' if expanded) }
.settings-header
%h4= s_('ClusterIntegration|Provider details')
- %button.btn.gl-button.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
%p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
.settings-content
= render 'provider_details_form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster)
diff --git a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
index c3b881df98d..af4c934fd72 100644
--- a/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
+++ b/app/views/clusters/clusters/_gcp_signup_offer_banner.html.haml
@@ -6,5 +6,5 @@
= c.body do
= s_('ClusterIntegration|Every new Google Cloud Platform (GCP) account receives $300 in credit upon %{sign_up_link}. In partnership with Google, GitLab is able to offer an additional $200 for both new and existing GCP accounts to get started with GitLab\'s Google Kubernetes Engine Integration.').html_safe % { sign_up_link: link }
= c.actions do
- %a.gl-button.btn-confirm.text-decoration-none{ href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', rel: 'noopener noreferrer' }
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: 'https://cloud.google.com/partners/partnercredit/?pcn_code=0014M00001h35gDQAQ#contact-form', target: '_blank', button_options: { rel: 'noopener noreferrer' }) do
= s_("ClusterIntegration|Apply for credit")
diff --git a/app/views/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 4edb0f324dc..8750b80ccfd 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,8 +1,7 @@
.nav-block.activities
= render 'shared/event_filter'
.controls
- = link_to dashboard_projects_path(rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip', title: 'Subscribe' do
- = sprite_icon('rss', css_class: 'gl-icon')
+ = render Pajamas::ButtonComponent.new(href: dashboard_projects_path(rss_url_options), icon: 'rss', button_options: { title: _('Subscribe'), aria: { label: _('Subscribe') }, class: 'gl-display-none gl-sm-display-inline-flex' })
.content_list
.loading
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 1c82b30ed8d..09e2e35c617 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -3,8 +3,8 @@
- if current_user.can_create_group?
.page-title-controls
- = link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { qa_selector: "new_group_button", testid: "new-group-button" }
-
+ = render Pajamas::ButtonComponent.new(href: new_group_path, variant: :confirm, button_options: { data: { qa_selector: "new_group_button", testid: "new-group-button" } }) do
+ = _("New group")
.top-area
= gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
= gl_tab_link_to _("Your groups"), dashboard_groups_path
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index 9c492a0da34..10e653fd427 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,6 +1,3 @@
-- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
-- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
-
= content_for :flash_message do
= render 'shared/project_limit'
@@ -9,17 +6,13 @@
- if current_user.can_create_project?
.page-title-controls
- = link_to _("New project"), new_project_path, class: "gl-button btn btn-confirm", data: { qa_selector: 'new_project_button' }
+ = render Pajamas::ButtonComponent.new(href: new_project_path, variant: :confirm, button_options: { data: { qa_selector: 'new_project_button' } }) do
+ = _("New project")
.top-area
- .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0.gl-w-full
+ .scrolling-tabs-container.inner-page-scroll-tabs.gl-flex-grow-1.gl-min-w-0
.fade-left= sprite_icon('chevron-lg-left', size: 12)
.fade-right= sprite_icon('chevron-lg-right', size: 12)
= render 'dashboard/projects_nav'
- - unless feature_project_list_filter_bar
- .nav-controls
- = render 'shared/projects/search_form'
- = render 'shared/projects/dropdown'
-- if feature_project_list_filter_bar
- .project-filters
- = render 'shared/projects/search_bar', project_tab_filter: project_tab_filter
+ .nav-controls
+ = render 'shared/projects/search_form'
diff --git a/app/views/dashboard/_projects_nav.html.haml b/app/views/dashboard/_projects_nav.html.haml
index 29c820ddc58..7cbd2fb14ec 100644
--- a/app/views/dashboard/_projects_nav.html.haml
+++ b/app/views/dashboard/_projects_nav.html.haml
@@ -3,11 +3,11 @@
= gl_tabs_nav({ class: 'scrolling-tabs nav-links gl-display-flex gl-flex-grow-1 gl-w-full nav gl-tabs-nav' }) do
= gl_tab_link_to dashboard_projects_path, { item_active: is_your_projects_path, class: 'shortcuts-activity', data: { placement: 'right' } } do
- = _("Your projects")
+ = s_("ProjectList|Yours")
= gl_tab_counter_badge(limited_counter_with_delimiter(@total_user_projects_count))
= gl_tab_link_to starred_dashboard_projects_path, { data: { placement: 'right' } } do
- = _("Starred projects")
+ = s_("ProjectList|Starred")
= gl_tab_counter_badge(limited_counter_with_delimiter(@total_starred_projects_count))
- = gl_tab_link_to _("Explore projects"), explore_root_path, { item_active: is_explore_projects_path, data: { placement: 'right' } }
- = gl_tab_link_to _("Explore topics"), topics_explore_projects_path, { data: { placement: 'right' } }
+ = gl_tab_link_to s_("ProjectList|Explore"), explore_root_path, { item_active: is_explore_projects_path, data: { placement: 'right' } }
+ = gl_tab_link_to s_("ProjectList|Topics"), topics_explore_projects_path, { data: { placement: 'right' } }
= render_if_exists "dashboard/removed_projects_tab"
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
index be2124fdd7e..5a798c249d1 100644
--- a/app/views/dashboard/_snippets_head.html.haml
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -4,7 +4,8 @@
- if current_user && current_user.snippets.any? || @snippets.any?
.page-title-controls
- if can?(current_user, :create_snippet)
- = link_to _("New snippet"), new_snippet_path, class: "gl-button btn btn-confirm", title: _("New snippet")
+ = render Pajamas::ButtonComponent.new(href: new_snippet_path, variant: :confirm, button_options: { title: _("New snippet") }) do
+ = _("New snippet")
.top-area
= gl_tabs_nav({ class: 'gl-border-0' }) do
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 79f6bfc866a..5293f685d06 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,6 +1,7 @@
- @hide_top_links = true
- page_title _("Issues")
- @breadcrumb_link = issues_dashboard_path(assignee_username: current_user.username)
+- add_page_specific_style 'page_bundles/issuable_list'
- add_page_specific_style 'page_bundles/dashboard'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{current_user.name} issues")
@@ -15,10 +16,7 @@
= render 'shared/new_project_item_select', path: 'issues/new', label: _("issue"), with_feature_enabled: 'issues', type: :issues
- if ::Feature.enabled?(:vue_issues_dashboard)
- .js-issues-dashboard{ data: { calendar_path: url_for(safe_params.merge(calendar_url_options)),
- empty_state_svg_path: image_path('illustrations/issue-dashboard_results-without-filter.svg'),
- is_signed_in: current_user.present?.to_s,
- rss_path: url_for(safe_params.merge(rss_url_options)) } }
+ .js-issues-dashboard{ data: dashboard_issues_list_data(current_user) }
- else
.top-area
= render 'shared/issuable/nav', type: :issues, display_count: !@no_filters_set
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 8a639d08a27..c921375edd1 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,6 +1,7 @@
- @hide_top_links = true
- page_title _("Merge requests")
- @breadcrumb_link = merge_requests_dashboard_path(assignee_username: current_user.username)
+- add_page_specific_style 'page_bundles/issuable_list'
= render_dashboard_ultimate_trial(current_user)
diff --git a/app/views/dashboard/projects/_nav.html.haml b/app/views/dashboard/projects/_nav.html.haml
index 3e39872902d..45e3267813f 100644
--- a/app/views/dashboard/projects/_nav.html.haml
+++ b/app/views/dashboard/projects/_nav.html.haml
@@ -1,19 +1,4 @@
-- inactive_class = 'btn p-2'
-- active_class = 'btn p-2 active'
-- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
-- is_explore_trending = project_tab_filter == :explore_trending
-- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
-
-.nav-block{ class: ("w-100" if feature_project_list_filter_bar) }
- - if feature_project_list_filter_bar
- .btn-group.button-filter-group.d-flex.m-0.p-0
- - if project_tab_filter == :explore || is_explore_trending
- = link_to s_('DashboardProjects|Trending'), trending_explore_projects_path, class: is_explore_trending ? active_class : inactive_class
- = link_to s_('DashboardProjects|All'), explore_projects_path, class: is_explore_trending ? inactive_class : active_class
- - else
- = link_to s_('DashboardProjects|All'), dashboard_projects_path, class: params[:personal].present? ? inactive_class : active_class
- = link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), class: params[:personal].present? ? active_class : inactive_class
- - else
- = gl_tabs_nav do
- = gl_tab_link_to s_('DashboardProjects|All'), dashboard_projects_path, { item_active: params[:personal].blank? }
- = gl_tab_link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), { item_active: params[:personal].present? }
+.nav-block
+ = gl_tabs_nav do
+ = gl_tab_link_to s_('DashboardProjects|All'), dashboard_projects_path, { item_active: params[:personal].blank? }
+ = gl_tab_link_to s_('DashboardProjects|Personal'), filter_projects_path(personal: true), { item_active: params[:personal].present? }
diff --git a/app/views/dashboard/projects/index.html.haml b/app/views/dashboard/projects/index.html.haml
index 0d9257e659a..f427c347dd3 100644
--- a/app/views/dashboard/projects/index.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -12,7 +12,7 @@
= render "projects/last_push"
- if show_projects?(@projects, params)
= render 'dashboard/projects_head'
- = render 'nav' unless Feature.enabled?(:project_list_filter_bar)
+ = render 'nav'
= render 'projects'
- else
= render "zero_authorized_projects"
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index 47bc8f5c95b..9dfeaa3d07d 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -1,60 +1,66 @@
-%li.todo.gl-hover-border-blue-200.gl-hover-bg-blue-50.gl-hover-cursor-pointer{ class: "todo-#{todo.done? ? 'done' : 'pending'}", id: dom_id(todo), data: { url: todo_target_path(todo) } }
- .gl-display-flex.gl-flex-direction-row
- .todo-avatar.gl-display-none.gl-sm-display-inline-block
- = author_avatar(todo, size: 40)
-
- .todo-item.flex-fill.gl-overflow-hidden.gl-overflow-x-auto.gl-align-self-center{ data: { qa_selector: "todo_item_container" } }
- .todo-title.gl-mb-3.gl-md-mb-0
- - if todo_author_display?(todo)
- = todo_target_state_pill(todo)
-
- %span.title-item.author-name.bold
- - if todo.author
- = link_to_author(todo, self_added: todo.self_added?)
- - else
- = _('(removed)')
-
- %span.title-item.action-name{ data: { qa_selector: "todo_action_name_content" } }
- = todo_action_name(todo)
-
- %span.title-item.todo-label.todo-target-link
+%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-title.gl-pt-2.gl-pb-3.gl-px-2.gl-md-mb-1.gl-font-sm.gl-text-gray-500
+
+ = todo_target_state_pill(todo)
+
+ %span.todo-target-title{ data: { qa_selector: "todo_target_title_content" }, :id => dom_id(todo) + "_describer" }
+ = todo_target_title(todo)
+
+ - if !todo.for_design? && !todo.member_access_requested?
+ &middot;
+
+ %span
+ = todo_parent_path(todo)
+
+ %span.todo-label
- if todo.target
- = todo_target_link(todo)
+ = link_to todo_target_name(todo), todo_target_path(todo), class: 'todo-target-link gl-text-gray-500! gl-text-decoration-none!', :'aria-describedby' => dom_id(todo) + "_describer", :'aria-label' => todo_target_aria_label(todo)
- else
= _("(removed)")
- %span.title-item.todo-target-title{ data: { qa_selector: "todo_target_title_content" } }
- = todo_target_title(todo)
-
- %span.title-item.todo-project.todo-label
- = s_('Todo|at %{todo_parent_path}').html_safe % { todo_parent_path: todo_parent_path(todo) }
+ .todo-body.gl-mb-2.gl-px-2.gl-display-flex.gl-align-items-flex-start.gl-lg-align-items-center
+ .todo-avatar.gl-display-none.gl-sm-display-inline-block
+ = author_avatar(todo, size: 24)
+ .todo-note
+ - if todo_author_display?(todo)
+ .author-name.bold.gl-display-inline
+ - if todo.author
+ = link_to_author(todo, self_added: todo.self_added?)
+ - else
+ = _('(removed)')
- - if todo.self_assigned?
- %span.title-item.action-name
- = todo_self_addressing(todo)
+ %span.action-name{ data: { qa_selector: "todo_action_name_content" } }<
+ = todo_action_name(todo)
+ - if todo.note.present?
+ \:
+ - unless todo.note.present? || todo.self_assigned?
+ \.
- %span.title-item
- &middot;
+ - if todo.self_assigned?
+ %span.action-name<
+ = todo_self_addressing(todo)
+ \.
+ - if todo.note.present?
+ %span.action-description.gl-font-style-italic<
+ = first_line_in_markdown(todo, :body, 100, is_todo: true, project: todo.project, group: todo.group)
- %span.title-item.todo-timestamp
- #{time_ago_with_tooltip(todo.created_at)}
- = todo_due_date(todo)
+ .todo-timestamp.gl-white-space-nowrap.gl-sm-ml-3.gl-mt-2.gl-mb-2.gl-sm-my-0.gl-px-2.gl-sm-px-0
+ %span.todo-timestamp.gl-font-sm.gl-text-gray-500
+ = todo_due_date(todo)
+ #{time_ago_with_tooltip(todo.created_at)}
- - if todo.note.present?
- .todo-body
- .todo-note.break-word
- .md
- = first_line_in_markdown(todo, :body, 150, project: todo.project, group: todo.group)
- .todo-actions.gl-ml-3
+ .todo-actions.gl-mr-4.gl-px-2.gl-sm-px-0.gl-sm-mx-0
- if todo.pending?
- = link_to dashboard_todo_path(todo), method: :delete, class: 'gl-button gl-bg-gray-50 btn btn-default btn-loading d-flex align-items-center js-done-todo', data: { href: dashboard_todo_path(todo) } do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-loading btn-icon gl-display-flex js-done-todo has-tooltip', title: _('Mark as done')}, method: :delete, href: dashboard_todo_path(todo)), 'aria-label' => _('Mark as done') do
= gl_loading_icon(inline: true)
- = _('Done')
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button gl-bg-gray-50 btn btn-default btn-loading d-flex align-items-center js-undo-todo hidden', data: { href: restore_dashboard_todo_path(todo) } do
+ = sprite_icon('check', css_class: 'js-todo-button-icon')
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-loading btn-icon gl-display-flex js-undo-todo hidden has-tooltip', title: _('Undo')}, method: :patch, href: restore_dashboard_todo_path(todo)), 'aria-label' => _('Undo') do
= gl_loading_icon(inline: true)
- = _('Undo')
+ = sprite_icon('redo', css_class: 'js-todo-button-icon')
- else
- = link_to restore_dashboard_todo_path(todo), method: :patch, class: 'gl-button gl-bg-gray-50 btn btn-default btn-loading d-flex align-items-center js-add-todo', data: { href: restore_dashboard_todo_path(todo) } do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'btn-loading btn-icon gl-display-flex js-add-todo has-tooltip', title: _('Add a to do')}, method: :patch, href: restore_dashboard_todo_path(todo)), 'aria-label' => _('Add a to do') do
= gl_loading_icon(inline: true)
- = _('Add a to do')
+ = sprite_icon('todo-add', css_class: 'js-todo-button-icon')
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index deb1ac9e360..c0bd3ee3f0d 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -5,6 +5,7 @@
= render_two_factor_auth_recovery_settings_check
= render_dashboard_ultimate_trial(current_user)
- add_page_specific_style 'page_bundles/todos'
+- add_page_specific_style 'page_bundles/issuable'
.page-title-holder.d-flex.align-items-center
%h1.page-title.gl-font-size-h-display= _("To-Do List")
diff --git a/app/views/devise/shared/_footer.html.haml b/app/views/devise/shared/_footer.html.haml
index 10cfc07a719..0744faa148c 100644
--- a/app/views/devise/shared/_footer.html.haml
+++ b/app/views/devise/shared/_footer.html.haml
@@ -1,9 +1,10 @@
%hr.footer-fixed
-.container.footer-container
+.container.footer-container.gl-display-flex.gl-justify-content-space-between
.footer-links
- unless public_visibility_restricted?
= link_to _("Explore"), explore_root_path
= link_to _("Help"), help_path
= link_to _("About GitLab"), "https://#{ApplicationHelper.promo_host}"
= link_to _("Community forum"), ApplicationHelper.community_forum, target: '_blank', class: 'text-nowrap', rel: 'noopener noreferrer'
+ = render 'devise/shared/language_switcher'
= footer_message
diff --git a/app/views/devise/shared/_language_switcher.html.haml b/app/views/devise/shared/_language_switcher.html.haml
new file mode 100644
index 00000000000..4c47e3efd0f
--- /dev/null
+++ b/app/views/devise/shared/_language_switcher.html.haml
@@ -0,0 +1,3 @@
+- return unless ::Feature.enabled?(:preferred_language_switcher)
+
+.js-language-switcher{ data: { locales: ordered_selectable_locales.to_json } }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index b9c9c99bf1a..a3a5fe690a7 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -44,7 +44,6 @@
.form-group
= f.label :email, class: "label-bold #{'gl-mb-1' if Feature.enabled?(:restyle_login_page, @project)}"
= f.email_field :email,
- value: @invite_email,
class: 'form-control gl-form-input middle js-validate-email',
data: { qa_selector: 'new_user_email_field' },
required: true,
diff --git a/app/views/devise/unlocks/new.html.haml b/app/views/devise/unlocks/new.html.haml
index abaf169afd5..b9d50e48d05 100644
--- a/app/views/devise/unlocks/new.html.haml
+++ b/app/views/devise/unlocks/new.html.haml
@@ -1,14 +1,14 @@
= render 'devise/shared/tab_single', tab_title: _('Resend unlock instructions')
.login-box
.login-body
- = form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
+ = gitlab_ui_form_for(resource, as: resource_name, url: unlock_path(resource_name), html: { method: :post, class: 'gl-show-field-errors' }) do |f|
.devise-errors
= render "devise/shared/error_messages", resource: resource
.form-group.gl-mb-6
= f.label :email
= f.email_field :email, class: 'form-control', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', title: _('Please provide a valid email address.')
.clearfix
- = f.submit _('Resend unlock instructions'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Resend unlock instructions'), pajamas_button: true, class: 'gl-w-full'
.clearfix.prepend-top-20
= render 'devise/shared/sign_in_link'
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index f02d30081b6..67a88f3d623 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -1,9 +1,7 @@
- has_label = local_assigns.fetch(:has_label, false)
-- feature_project_list_filter_bar = Feature.enabled?(:project_list_filter_bar)
-- klass = feature_project_list_filter_bar ? 'gl-ml-3 gl-display-flex gl-flex-grow-1 gl-flex-shrink-1' : 'gl-ml-3'
- selected = projects_filter_selected(params[:visibility_level])
- if current_user
- unless has_label
%span.gl-float-left= _("Visibility:")
- = gl_redirect_listbox_tag(projects_filter_items, selected, class: klass, data: { right: true })
+ = gl_redirect_listbox_tag(projects_filter_items, selected, class: 'gl-ml-3', data: { right: true })
diff --git a/app/views/explore/projects/_nav.html.haml b/app/views/explore/projects/_nav.html.haml
index 9d7a6f1ccfb..9119026320a 100644
--- a/app/views/explore/projects/_nav.html.haml
+++ b/app/views/explore/projects/_nav.html.haml
@@ -7,5 +7,4 @@
.nav-controls
- unless current_user
= render 'shared/projects/search_form'
- = render 'shared/projects/dropdown'
= render 'filter'
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index ae59d9c728b..9585eb76912 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -10,5 +10,5 @@
- else
= render 'explore/head'
-= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
+= render 'explore/projects/nav'
= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/page_out_of_bounds.html.haml b/app/views/explore/projects/page_out_of_bounds.html.haml
index c554cce3dc6..ef5ee2c679e 100644
--- a/app/views/explore/projects/page_out_of_bounds.html.haml
+++ b/app/views/explore/projects/page_out_of_bounds.html.haml
@@ -9,7 +9,7 @@
- else
= render 'explore/head'
-= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
+= render 'explore/projects/nav'
.nothing-here-block
.svg-content
@@ -18,4 +18,5 @@
%h5= _("Maximum page reached")
%p= _("Sorry, you have exceeded the maximum browsable page number. Please use the API to explore further.")
- = link_to _("Back to page %{number}") % { number: @max_page_number }, request.params.merge(page: @max_page_number), class: 'gl-button btn btn-inverted'
+ = render Pajamas::ButtonComponent.new(href: request.params.merge(page: @max_page_number)) do
+ = _("Back to page %{number}") % { number: @max_page_number }
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index a1f2fea5134..ec7eefea264 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -9,5 +9,5 @@
- else
= render 'explore/head'
-= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
+= render 'explore/projects/nav'
= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/topic.html.haml b/app/views/explore/projects/topic.html.haml
index 76e59a49ed1..7b2c5683482 100644
--- a/app/views/explore/projects/topic.html.haml
+++ b/app/views/explore/projects/topic.html.haml
@@ -25,7 +25,6 @@
.top-area.gl-pt-2.gl-pb-2
.nav-controls
= render 'shared/projects/search_form'
- = render 'shared/projects/dropdown'
= render 'filter'
= render 'projects', projects: @projects
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index e23f63b0064..8a92ec31b22 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -9,5 +9,5 @@
- else
= render 'explore/head'
-= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
+= render 'explore/projects/nav'
= render 'projects', projects: @projects
diff --git a/app/views/groups/_activities.html.haml b/app/views/groups/_activities.html.haml
index 0d6c3e74ce8..b62076c23f3 100644
--- a/app/views/groups/_activities.html.haml
+++ b/app/views/groups/_activities.html.haml
@@ -1,8 +1,7 @@
.nav-block.activities
= render 'shared/event_filter', show_group_events: @group.supports_events?
.controls
- = link_to group_path(@group, rss_url_options), class: 'btn gl-button btn-default btn-icon d-none d-sm-inline-flex has-tooltip' , title: _('Subscribe') do
- = sprite_icon('rss', css_class: 'gl-icon')
+ = render Pajamas::ButtonComponent.new(href: group_path(@group, rss_url_options), icon: 'rss', button_options: { class: 'd-none d-sm-inline-flex has-tooltip', title: _('Subscribe') })
.content_list
.loading
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index 687a1fb32bf..0b26db64ffa 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -27,3 +27,13 @@
= f.text_field :two_factor_grace_period, class: 'form-control gl-form-input gl-form-input-sm'
%small.form-text.text-gl-muted
= _("Time (in hours) that users are allowed to skip forced configuration of two-factor authentication.")
+
+- if @group.namespace_settings.present?
+ .form-group.gl-form-group
+ %legend.col-form-label.col-form-label
+ = s_('Runners|Runner Registration')
+ - parent_disabled = Gitlab::CurrentSettings.valid_runner_registrars.exclude?('group') || !@group.all_ancestors_have_runner_registration_enabled?
+ = f.gitlab_ui_checkbox_component :runner_registration_enabled,
+ s_('Runners|New group runners can be registered'),
+ checkbox_options: { checked: @group.runner_registration_enabled && !parent_disabled, disabled: parent_disabled },
+ help_text: s_('Runners|Existing runners are not affected. To permit runner registration for all groups, enable this setting in the Admin Area in Settings &gt CI/CD.').html_safe
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index a82a2e41508..1494990e427 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -3,16 +3,15 @@
- emails_disabled = @group.emails_disabled?
.group-home-panel
- .row.my-3
- .home-panel-title-row.col-md-12.col-lg-6.d-flex
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-gap-3.gl-my-5
+ .home-panel-title-row.gl-display-flex.gl-align-items-center
.avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.float-none{ class: 'gl-mr-3!' }
= group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'logo')
- .d-flex.flex-column.flex-wrap.align-items-baseline
- .d-inline-flex.align-items-baseline
- %h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2{ itemprop: 'name' }
- = @group.name
- %span.visibility-icon.gl-text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
- = visibility_level_icon(@group.visibility_level, options: {class: 'icon'})
+ %div
+ %h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex{ itemprop: 'name' }
+ = @group.name
+ %span.visibility-icon.gl-text-secondary.has-tooltip.gl-ml-2{ data: { container: 'body' }, title: visibility_icon_description(@group) }
+ = visibility_level_icon(@group.visibility_level, options: {class: 'icon'})
.home-panel-metadata.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { qa_selector: 'group_id_content' }, itemprop: 'identifier' }
- if can?(current_user, :read_group, @group)
%span.gl-display-inline-block.gl-vertical-align-middle
@@ -22,24 +21,23 @@
%span.gl-ml-3.gl-mb-3
= render 'shared/members/access_request_links', source: @group
- .home-panel-buttons.col-md-12.col-lg-6
- - if current_user
- .gl-display-flex.gl-flex-wrap.gl-lg-justify-content-end.gl-mx-n2{ data: { testid: 'group-buttons' } }
- - if current_user.admin?
- = link_to [:admin, @group], class: 'btn btn-default gl-button btn-icon gl-mt-3 gl-mr-2', title: _('View group in admin area'),
- data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = sprite_icon('admin')
- - if @notification_setting
- .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-mx-2 gl-mt-3 gl-vertical-align-top', no_flip: 'true' } }
- - if can_create_subgroups
- .gl-px-2.gl-sm-w-auto.gl-w-full
- = link_to _("New subgroup"),
- new_group_path(parent_id: @group.id, anchor: 'create-group-pane'),
- class: "btn btn-default gl-button gl-mt-3 gl-sm-w-auto gl-w-full",
- data: { qa_selector: 'new_subgroup_button' }
- - if can_create_projects
- .gl-px-2.gl-sm-w-auto.gl-w-full
- = link_to _("New project"), new_project_path(namespace_id: @group.id), class: "btn btn-confirm gl-button gl-mt-3 gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_project_button' }
+ - if current_user
+ .home-panel-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-center.gl-flex-wrap.gl-gap-3{ data: { testid: 'group-buttons' } }
+ - if current_user.admin?
+ = link_to [:admin, @group], class: 'btn btn-default gl-button btn-icon', title: _('View group in admin area'),
+ data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
+ = sprite_icon('admin')
+ - if @notification_setting
+ .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), group_id: @group.id, container_class: 'gl-vertical-align-top', no_flip: 'true' } }
+ - if can_create_subgroups
+ .gl-sm-w-auto.gl-w-full
+ = link_to _("New subgroup"),
+ new_group_path(parent_id: @group.id, anchor: 'create-group-pane'),
+ class: "btn btn-default gl-button gl-sm-w-auto gl-w-full",
+ data: { qa_selector: 'new_subgroup_button' }
+ - if can_create_projects
+ .gl-sm-w-auto.gl-w-full
+ = link_to _("New project"), new_project_path(namespace_id: @group.id), class: "btn btn-confirm gl-button gl-sm-w-auto gl-w-full", data: { qa_selector: 'new_project_button' }
- if @group.description.present?
.group-home-desc.mt-1
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 d48bf0173a4..4a4bdfc6714 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
@@ -1,20 +1,39 @@
+- bulk_imports_disabled = !Gitlab::CurrentSettings.bulk_import_enabled?
+
= gitlab_ui_form_with url: configure_import_bulk_imports_path(namespace_id: params[:parent_id]), class: 'gl-show-field-errors' do |f|
.gl-border-l-solid.gl-border-r-solid.gl-border-t-solid.gl-border-gray-100.gl-border-1.gl-p-5.gl-mt-4
.gl-display-flex.gl-align-items-center
%h4.gl-display-flex
= s_('GroupsNew|Import groups from another instance of GitLab')
= link_to _('History'), history_import_bulk_imports_path, class: 'gl-link gl-ml-auto'
- = render Pajamas::AlertComponent.new(dismissible: false,
- variant: :warning) do |c|
- = c.body do
- - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
- - docs_link_end = '</a>'.html_safe
- = s_('GroupsNew|Not all related objects are migrated. %{docs_link_start}More info%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+
+ - if bulk_imports_disabled
+ = render Pajamas::AlertComponent.new(dismissible: false, variant: :tip) do |c|
+ = c.body do
+ = 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
+
+ = 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 }
+ - else
+ = s_('GroupsNew|Please ask your Administrator to enable it in the Admin settings.')
+
+ = s_('GroupsNew|Remember to enable it also on the instance you are migrating from.')
+ - else
+ = render Pajamas::AlertComponent.new(dismissible: false,
+ variant: :warning) do |c|
+ = c.body do
+ - docs_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: help_page_path('user/group/import/index.md') }
+ - docs_link_end = '</a>'.html_safe
+ = s_('GroupsNew|Not all related objects are migrated. %{docs_link_start}More info%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
+
%p.gl-mt-3
- = s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.')
+ = s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.')
.form-group.gl-display-flex.gl-flex-direction-column
= f.label :bulk_import_gitlab_url, s_('GroupsNew|GitLab source URL'), for: 'import_gitlab_url'
- = f.text_field :bulk_import_gitlab_url, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8',
+ = f.text_field :bulk_import_gitlab_url, disabled: bulk_imports_disabled, placeholder: 'https://gitlab.example.com', class: 'gl-form-input col-xs-12 col-sm-8',
required: true,
title: s_('GroupsNew|Please fill in GitLab source URL.'),
id: 'import_gitlab_url',
@@ -24,12 +43,13 @@
.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 this 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}, use a short expiration date when creating the token.').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 }
+ = 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 }
= 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,
autocomplete: 'off',
title: s_('GroupsNew|Please fill in your personal access token.'),
id: 'import_gitlab_token',
data: { qa_selector: 'import_gitlab_token' }
.gl-border-gray-100.gl-border-solid.gl-border-1.gl-bg-gray-10.gl-p-5
- = f.submit s_('GroupsNew|Connect instance'), pajamas_button: true, data: { qa_selector: 'connect_instance_button' }
+ = f.submit s_('GroupsNew|Connect instance'), disabled: bulk_imports_disabled, pajamas_button: true, data: { qa_selector: 'connect_instance_button' }
diff --git a/app/views/groups/_invite_groups_modal.html.haml b/app/views/groups/_invite_groups_modal.html.haml
index 2e11f6cee4f..b8e40460a92 100644
--- a/app/views/groups/_invite_groups_modal.html.haml
+++ b/app/views/groups/_invite_groups_modal.html.haml
@@ -1,3 +1,3 @@
- return unless can_admin_group_member?(group)
-.js-invite-groups-modal{ data: common_invite_group_modal_data(group, GroupMember, 'false') }
+.js-invite-groups-modal{ data: { reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s }.merge(common_invite_group_modal_data(group, GroupMember, 'false')) }
diff --git a/app/views/groups/_invite_members_modal.html.haml b/app/views/groups/_invite_members_modal.html.haml
index 786034fd2e7..bf0e8b627fd 100644
--- a/app/views/groups/_invite_members_modal.html.haml
+++ b/app/views/groups/_invite_members_modal.html.haml
@@ -2,4 +2,5 @@
.js-invite-members-modal{ data: { is_project: 'false',
access_levels: GroupMember.access_level_roles.to_json,
+ reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s,
help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(group)).merge(users_filter_data(group)) }
diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml
index 94b0b018084..95990e8937c 100644
--- a/app/views/groups/_new_group_fields.html.haml
+++ b/app/views/groups/_new_group_fields.html.haml
@@ -30,5 +30,7 @@
= recaptcha_tags nonce: content_security_policy_nonce
.row
.col-sm-12
- = f.submit submit_label, class: "btn gl-button btn-confirm", data: { qa_selector: 'create_group_button' }
- = link_to _('Cancel'), dashboard_groups_path, class: 'btn gl-button btn-default btn-cancel'
+ = f.submit submit_label, pajamas_button: true, data: { qa_selector: 'create_group_button' }
+ = render Pajamas::ButtonComponent.new(href: dashboard_groups_path) do
+ = _('Cancel')
+
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index bca1c874cc6..8763912438b 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -13,7 +13,7 @@
= _('Collapse')
%p
= _('Update your group name, description, avatar, and visibility.')
- = link_to s_('Learn more about groups.'), help_page_path('user/group/index')
+ = link_to _('Learn more about groups.'), help_page_path('user/group/index')
.settings-content
= render 'groups/settings/general'
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 4da70c8bf5d..298ed2c0806 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -15,8 +15,8 @@
classes: 'gl-md-w-auto gl-w-full gl-md-ml-3 gl-md-mt-0 gl-mt-3',
trigger_source: 'group-members-page',
display_text: _('Invite members') } }
- = render 'groups/invite_groups_modal', group: @group
- = render 'groups/invite_members_modal', group: @group
+ = render 'groups/invite_groups_modal', group: @group, reload_page_on_submit: true
+ = render 'groups/invite_members_modal', group: @group, reload_page_on_submit: true
= render_if_exists 'groups/group_members/ldap_sync'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 925e7d46f14..6faa4758d66 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,8 +1,9 @@
- page_title _('Issues')
+- add_page_specific_style 'page_bundles/issuable_list'
- add_page_specific_style 'page_bundles/issues_list'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
-.js-issues-list{ data: group_issues_list_data(@group, current_user) }
+.js-issues-list-root{ data: group_issues_list_data(@group, current_user) }
- if can?(current_user, :admin_issue, @group) && @group.licensed_feature_available?(:group_bulk_edit)
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 8187dda5471..a03c406acc6 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -11,7 +11,7 @@
.labels-container.gl-mt-5
- if @labels.any?
.text-muted.gl-mb-5
- = _('Labels can be applied to %{features}. Group labels are available for any project within the group.') % { features: issuable_types.to_sentence }
+ = labels_function_introduction
.other-labels
%h4= _('Labels')
%ul.manage-labels-list.js-other-labels
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 6c4a8b53764..92f6c896e7b 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,6 +1,7 @@
- @can_bulk_update = can?(current_user, :admin_merge_request, @group) && @group.licensed_feature_available?(:group_bulk_edit) && issuables_count_for_state(:merge_requests, :all) > 0
- page_title _("Merge requests")
+- add_page_specific_style 'page_bundles/issuable_list'
.top-area
= render 'shared/issuable/nav', type: :merge_requests
diff --git a/app/views/groups/milestones/_form.html.haml b/app/views/groups/milestones/_form.html.haml
index d4b1c3c27f1..a99d76f99a7 100644
--- a/app/views/groups/milestones/_form.html.haml
+++ b/app/views/groups/milestones/_form.html.haml
@@ -22,7 +22,9 @@
.form-actions
- if @milestone.new_record?
= f.submit _('Create milestone'), data: { qa_selector: "create_milestone_button" }, pajamas_button: true
- = link_to _("Cancel"), group_milestones_path(@group), class: "btn gl-button btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: group_milestones_path(@group)) do
+ = _("Cancel")
- else
= f.submit _('Update milestone'), pajamas_button: true
- = link_to _("Cancel"), group_milestone_path(@group, @milestone), class: "btn gl-button btn-cancel"
+ = render Pajamas::ButtonComponent.new(href: group_milestone_path(@group, @milestone)) do
+ = _("Cancel")
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 50a1b474504..f49b69f821d 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -9,13 +9,14 @@
= render 'shared/milestones/search_form'
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @group)
- = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm gl-ml-3", data: { qa_selector: "new_group_milestone_link" }
-
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }, class: "gl-ml-3" }) do
+ = _('New milestone')
- if @milestones.blank?
= render 'shared/empty_states/milestones_tab', learn_more_path: help_page_path('user/project/milestones/index') do
- if can?(current_user, :admin_milestone, @group)
.text-center
- = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }}) do
+ = _('New milestone')
- else
.milestones
%ul.content-list
@@ -29,4 +30,5 @@
= render 'shared/empty_states/milestones', learn_more_path: help_page_path('user/project/milestones/index') do
- if can?(current_user, :admin_milestone, @group)
.text-center
- = link_to _('New milestone'), new_group_milestone_path(@group), class: "btn gl-button btn-confirm", data: { qa_selector: "new_group_milestone_link" }
+ = render Pajamas::ButtonComponent.new(href: new_group_milestone_path(@group), variant: :confirm, button_options: { data: { qa_selector: "new_group_milestone_link" }}) do
+ = _('New milestone')
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index e7ae54a8879..cae347630ee 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -12,19 +12,19 @@
= _("New project")
- c.body do
%ul.content-list
- - @projects.each do |project|
- %li.project-row.gl-align-items-center{ class: 'gl-display-flex!' }
+ - @projects.each_with_index do |project, idx|
+ %li.project-row.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'project_row_container', qa_index: idx } }
.avatar-container.rect-avatar.s40.gl-flex-shrink-0
= project_icon(project, alt: '', class: 'avatar project-avatar s40', width: 40, height: 40)
.gl-min-w-0.gl-flex-grow-1
.title
= link_to project_path(project), class: 'js-prefetch-document' do
- %span.project-full-name
- %span.namespace-name
+ %span.project-full-name{ data: { qa_selector: 'project_fullname_content' } }
+ %span.namespace-name{ data: { qa_selector: 'project_namespace_content' } }
- if project.namespace
= project.namespace.human_name
\/
- %span.project-name
+ %span.project-name{ data: { qa_selector: 'project_name_content', qa_project_name: project.name } }
= project.name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
@@ -38,9 +38,9 @@
= render 'project_badges', project: project
.controls.gl-flex-shrink-0.gl-ml-5
- = link_to _('Members'), project_project_members_path(project), id: dom_id(project, :edit), class: "btn gl-button"
- = link_to _('Edit'), edit_project_path(project), id: dom_id(project, :edit), class: "btn gl-button"
- = render 'delete_project_button', project: project
+ = link_to _('Members'), project_project_members_path(project), id: dom_id(project, :edit), class: "btn gl-button", data: { qa_selector: 'project_members_button' }
+ = link_to _('Edit'), edit_project_path(project), id: dom_id(project, :edit), class: "btn gl-button", data: { qa_selector: 'project_edit_button' }
+ = render 'delete_project_button', project: project, data: { qa_selector: 'project_delete_button' }
- if @projects.blank?
.nothing-here-block= _("This group has no projects yet")
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index 6060d697f52..efd2e53e100 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @group.full_path, first: 10, name: nil, isGroupPage: true, sort: nil})
%section
#js-container-registry{ data: { endpoint: group_container_registries_path(@group),
@@ -12,7 +12,6 @@
"registry_host_url_with_port" => escape_once(registry_config.host_port),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
- "container_registry_importing_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'),
"is_admin": current_user&.admin.to_s,
is_group_page: "true",
"group_path": @group.full_path,
diff --git a/app/views/groups/runners/_settings.html.haml b/app/views/groups/runners/_settings.html.haml
index 3e5ec3c26e2..24b2469c501 100644
--- a/app/views/groups/runners/_settings.html.haml
+++ b/app/views/groups/runners/_settings.html.haml
@@ -3,10 +3,3 @@
- if @group.licensed_feature_available?(:stale_runner_cleanup_for_namespace)
.gl-mb-5
#stale-runner-cleanup-form{ data: { group_full_path: @group.full_path, stale_timeout_secs: ::Ci::Runner::STALE_TIMEOUT.to_i } }
-= render Pajamas::BannerComponent.new(button_text: s_('Runners|Take me there!'),
- button_link: group_runners_path(@group),
- svg_path: 'illustrations/rocket-launch-md.svg',
- close_options: { class: 'gl-display-none' }) do |c|
- - c.title do
- = s_('Runners|New group runners view')
- %p= s_('Runners|The new view gives you more space and better visibility into your fleet of runners.')
diff --git a/app/views/groups/runners/index.html.haml b/app/views/groups/runners/index.html.haml
index 1146063969b..9ea83397348 100644
--- a/app/views/groups/runners/index.html.haml
+++ b/app/views/groups/runners/index.html.haml
@@ -1,3 +1,3 @@
- page_title s_('Runners|Runners')
-#js-group-runners{ data: group_runners_data_attributes(@group).merge( { group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token } ) }
+#js-group-runners{ data: group_runners_data_attributes(@group).merge({ group_runners_limited_count: @group_runners_limited_count, registration_token: @group_runner_registration_token }) }
diff --git a/app/views/groups/settings/_export.html.haml b/app/views/groups/settings/_export.html.haml
index 66c1341fb15..5d79d0f8e79 100644
--- a/app/views/groups/settings/_export.html.haml
+++ b/app/views/groups/settings/_export.html.haml
@@ -25,10 +25,10 @@
%li= _('Runner tokens')
%li= _('SAML discovery tokens')
- if group.export_file_exists?
- = link_to _('Download export'), download_export_group_path(group),
- rel: 'nofollow', method: :get, class: 'btn gl-button btn-default', data: { qa_selector: 'download_export_link' }
- = link_to _('Regenerate export'), export_group_path(group),
- method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'regenerate_export_group_link' }
+ = render Pajamas::ButtonComponent.new(href: download_export_group_path(group), button_options: { rel: 'nofollow', data: { method: :get, qa_selector: 'download_export_link' } }) do
+ = _('Download export')
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'regenerate_export_group_link' } }) do
+ = _('Regenerate export')
- else
- = link_to _('Export group'), export_group_path(group),
- method: :post, class: 'btn gl-button btn-default', data: { qa_selector: 'export_group_link' }
+ = render Pajamas::ButtonComponent.new(href: export_group_path(group), button_options: { data: { method: :post, qa_selector: 'export_group_link' } }) do
+ = _('Export group')
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index be9d2c45885..658109fde64 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -32,4 +32,4 @@
= link_to s_('Groups|Remove avatar'), group_avatar_path(@group.to_param), aria: { label: s_('Groups|Remove avatar') }, data: { confirm: s_('Groups|Avatar will be removed. Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :delete, class: 'gl-button btn btn-danger-secondary'
.form-group.gl-form-group
= render 'shared/visibility_level', f: f, visibility_level: @group.visibility_level, can_change_visibility_level: can_change_group_visibility_level?(@group), form_model: @group
- = f.submit s_('Groups|Save changes'), class: 'btn gl-button btn-confirm js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
+ = f.submit s_('Groups|Save changes'), pajamas_button: true, class: 'js-dirty-submit', data: { qa_selector: 'save_name_visibility_settings_button' }
diff --git a/app/views/groups/settings/_git_access_protocols.html.haml b/app/views/groups/settings/_git_access_protocols.html.haml
index df798db79ad..01e8536c7ad 100644
--- a/app/views/groups/settings/_git_access_protocols.html.haml
+++ b/app/views/groups/settings/_git_access_protocols.html.haml
@@ -1,6 +1,6 @@
- if group.root? && Feature.enabled?(:group_level_git_protocol_control, group)
.form-group
- = f.label s_('Enabled Git access protocols'), class: 'label-bold'
+ = f.label _('Enabled Git access protocols'), class: 'label-bold'
= f.select :enabled_git_access_protocol, options_for_select(enabled_git_access_protocol_options_for_group, group.enabled_git_access_protocol), {}, class: 'form-control', data: { qa_selector: 'enabled_git_access_protocol_dropdown' }, disabled: !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
- if !::Gitlab::CurrentSettings.enabled_git_access_protocol.blank?
.form-text.text-muted
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index e35c0341ec0..a18789b52a3 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -52,4 +52,4 @@
checkbox_options: { checked: @group.crm_enabled? },
help_text: s_('GroupSettings|Organizations and contacts can be created and associated with issues.')
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
+ = f.submit _('Save changes'), pajamas_button: true, class: 'gl-mt-3 js-dirty-submit', data: { qa_selector: 'save_permissions_changes_button' }
diff --git a/app/views/groups/settings/_remove_button.html.haml b/app/views/groups/settings/_remove_button.html.haml
index df978f3cb96..cb05076b39d 100644
--- a/app/views/groups/settings/_remove_button.html.haml
+++ b/app/views/groups/settings/_remove_button.html.haml
@@ -1,8 +1,8 @@
- remove_form_id = local_assigns.fetch(:remove_form_id, nil)
-- if group.paid?
+- if group.prevent_delete?
= render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5', data: { testid: 'group-has-linked-subscription-alert' }}) do |c|
= c.body do
- = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
+ = html_escape(_("This group can't be removed because it is linked to a subscription. To remove this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/gitlab_com/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-confirm-danger{ data: group_settings_confirm_modal_data(group, remove_form_id) }
diff --git a/app/views/groups/settings/_transfer.html.haml b/app/views/groups/settings/_transfer.html.haml
index e01d703206c..3c76e8a864a 100644
--- a/app/views/groups/settings/_transfer.html.haml
+++ b/app/views/groups/settings/_transfer.html.haml
@@ -1,7 +1,7 @@
- form_id = "transfer-group-form"
- initial_data = { button_text: s_('GroupSettings|Transfer group'), group_name: @group.name, group_id: @group.id, target_form_id: form_id, is_paid_group: group.paid?.to_s }
-.sub-section
+.sub-section{ data: { qa_selector: 'transfer_group_content' } }
%h4.warning-title= s_('GroupSettings|Transfer group')
%p= _('Transfer group to another parent group.')
= form_for group, url: transfer_group_path(group), method: :put, html: { id: form_id, class: 'js-group-transfer-form' } do |f|
@@ -15,5 +15,5 @@
- if group.paid?
= render Pajamas::AlertComponent.new(dismissible: false, alert_options: { class: 'gl-mb-5' }) do |c|
= c.body do
- = html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
+ = html_escape(_("This group can't be transferred because it is linked to a subscription. To transfer this group, %{linkStart}link the subscription%{linkEnd} with a different group.")) % { linkStart: "<a href=\"#{help_page_path('subscriptions/gitlab_com/index', anchor: 'change-the-linked-namespace')}\">".html_safe, linkEnd: '</a>'.html_safe }
.js-transfer-group-form{ data: initial_data }
diff --git a/app/views/groups/settings/applications/index.html.haml b/app/views/groups/settings/applications/index.html.haml
index 96f834bd271..95bf2151bda 100644
--- a/app/views/groups/settings/applications/index.html.haml
+++ b/app/views/groups/settings/applications/index.html.haml
@@ -1,4 +1,5 @@
- page_title _("Group applications")
+- add_page_specific_style 'page_bundles/settings'
= render 'shared/doorkeeper/applications/index',
oauth_applications_enabled: user_oauth_applications?,
diff --git a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
index a55ccd94974..06cb9893196 100644
--- a/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_auto_devops_form.html.haml
@@ -13,4 +13,4 @@
help_text: '%{help_text} %{learn_more_link}'.html_safe % { help_text: help_text, learn_more_link: learn_more_link },
checkbox_options: { checked: group.auto_devops_enabled? }
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm gl-mt-5'
+ = f.submit _('Save changes'), class: 'gl-mt-5', pajamas_button: true
diff --git a/app/views/groups/settings/ci_cd/_form.html.haml b/app/views/groups/settings/ci_cd/_form.html.haml
index 89e353b94b0..d31d22c61be 100644
--- a/app/views/groups/settings/ci_cd/_form.html.haml
+++ b/app/views/groups/settings/ci_cd/_form.html.haml
@@ -7,5 +7,5 @@
= f.number_field :max_artifacts_size, class: 'form-control'
%p.form-text.text-muted
= _("The maximum file size in megabytes for individual job artifacts.")
- = link_to s_('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'maximum-artifacts-size'), target: '_blank', rel: 'noopener noreferrer'
= f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/groups/settings/repository/_default_branch.html.haml b/app/views/groups/settings/repository/_default_branch.html.haml
index 844a5f890a4..e8aa809a6ca 100644
--- a/app/views/groups/settings/repository/_default_branch.html.haml
+++ b/app/views/groups/settings/repository/_default_branch.html.haml
@@ -21,4 +21,4 @@
= render 'groups/settings/default_branch_protection', f: f, group: @group
= f.hidden_field :redirect_target, value: "repository_settings"
- = f.submit _('Save changes'), class: 'btn gl-button btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/groups/usage_quotas/index.html.haml b/app/views/groups/usage_quotas/index.html.haml
new file mode 100644
index 00000000000..a8c1071b876
--- /dev/null
+++ b/app/views/groups/usage_quotas/index.html.haml
@@ -0,0 +1,7 @@
+- page_title s_("UsageQuota|Usage")
+
+.gl-alert.gl-alert-no-icon.gl-alert-info.gl-mt-6
+ %h2.gl-alert-title
+ Development
+ .gl-alert-content
+ Placeholder for usage quotas Vue app
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 8c74aac5ef5..b3f9d538e83 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -11,7 +11,7 @@
%span= link_to_version
- if show_version_check?
%span.gl-mt-5.gl-mb-3.gl-ml-3
- .js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true" } }
+ .js-gitlab-version-check-badge{ data: { "size": "lg", "actionable": "true", "version": gitlab_version_check.to_json } }
%hr
- unless Gitlab::CurrentSettings.help_page_hide_commercial_content?
diff --git a/app/views/ide/_show.html.haml b/app/views/ide/_show.html.haml
index 95c15612adf..b18b5f1574b 100644
--- a/app/views/ide/_show.html.haml
+++ b/app/views/ide/_show.html.haml
@@ -1,13 +1,10 @@
-- @body_class = 'ide-layout'
- page_title _('IDE')
-- add_page_specific_style 'page_bundles/build'
-- add_page_specific_style 'page_bundles/ide'
+- unless use_new_web_ide?
+ - add_page_specific_style 'page_bundles/build'
+ - add_page_specific_style 'page_bundles/ide'
-- content_for :prefetch_asset_tags do
- - webpack_preload_asset_tag('monaco')
+ - content_for :prefetch_asset_tags do
+ - webpack_preload_asset_tag('monaco')
-#ide.ide-loading{ data: ide_data }
- .text-center
- = gl_loading_icon(size: 'md')
- %h2.clgray= _('Loading the GitLab IDE...')
+= render partial: 'shared/ide_root', locals: { data: ide_data, loading_text: _('Loading the GitLab IDE...') }
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index e92db09aaf1..4d2186a1352 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -4,6 +4,7 @@
- filterable = local_assigns.fetch(:filterable, true)
- paginatable = local_assigns.fetch(:paginatable, false)
- default_namespace_path = (local_assigns[:default_namespace] || current_user.namespace).full_path
+- cancel_path = local_assigns.fetch(:cancel_path, nil)
- provider_title = Gitlab::ImportSources.title(local_assigns.fetch(:provider))
- optional_stages = local_assigns.fetch(:optional_stages, [])
@@ -13,11 +14,11 @@
#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
can_select_namespace: current_user.can_select_namespace?.to_s,
ci_cd_only: has_ci_cd_only_params?.to_s,
- namespaces_path: import_available_namespaces_path,
repos_path: url_for([:status, :import, provider, { format: :json }]),
jobs_path: url_for([:realtime_changes, :import, provider, { format: :json }]),
default_target_namespace: default_namespace_path,
import_path: url_for([:import, provider, { format: :json }]),
+ cancel_path: cancel_path,
filterable: filterable.to_s,
paginatable: paginatable.to_s,
optional_stages: optional_stages.to_json }.merge(extra_data) }
diff --git a/app/views/import/bulk_imports/status.html.haml b/app/views/import/bulk_imports/status.html.haml
index 1c8de23f28f..e1547920708 100644
--- a/app/views/import/bulk_imports/status.html.haml
+++ b/app/views/import/bulk_imports/status.html.haml
@@ -3,7 +3,6 @@
- page_title _('Import groups')
#import-groups-mount-element{ data: { status_path: status_import_bulk_imports_path(format: :json),
- available_namespaces_path: import_available_namespaces_path(format: :json),
default_target_namespace: @namespace&.id,
create_bulk_import_path: import_bulk_imports_path(format: :json),
jobs_path: realtime_changes_import_bulk_imports_path(format: :json),
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 25afe9a7b1b..4a9f8be35c3 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -10,4 +10,5 @@
= render 'import/githubish_status',
provider: 'github', paginatable: paginatable,
default_namespace: @namespace,
+ cancel_path: cancel_import_github_path,
optional_stages: Gitlab::GithubImport::Settings.stages_array
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 42a9d2c3136..079123e989e 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -21,5 +21,8 @@
= file_field_tag :file, class: ''
.row
.form-actions.col-sm-12
- = submit_tag _('Import project'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'import_project_button' }
- = link_to _('Cancel'), new_project_path, class: 'gl-button btn btn-default btn-cancel'
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, button_options: { class: 'gl-mr-2', data: { qa_selector: 'import_project_button' }}) do
+ = _('Import project')
+ = render Pajamas::ButtonComponent.new(href: new_project_path) do
+ = _('Cancel')
+
diff --git a/app/views/import/manifest/_form.html.haml b/app/views/import/manifest/_form.html.haml
index 096d2543502..a35e9ea3fcf 100644
--- a/app/views/import/manifest/_form.html.haml
+++ b/app/views/import/manifest/_form.html.haml
@@ -2,11 +2,14 @@
.form-group
= label_tag :group_id, nil, class: 'label-bold' do
= _('Group')
- .input-group
- .input-group-prepend.has-tooltip{ title: root_url }
- .input-group-text
- = root_url
- = select_tag :group_id, namespaces_options(params[:namespace_id], display_path: true, groups_only: true), { class: 'select2 js-select-namespace' }
+ .input-group.gl-max-w-62
+ - namespace_id = namespace_id_from(params) || current_user.manageable_groups(include_groups_with_developer_maintainer_access: true)&.first&.id
+ - namespace_full_path = GroupFinder.new(current_user).execute(id: namespace_id)&.full_path
+ .js-vue-new-project-url-select{ data: { namespace_full_path: namespace_full_path,
+ namespace_id: namespace_id ,
+ input_id: 'group_id',
+ input_name: 'group_id',
+ root_url: root_url } }
.form-text.text-muted
= _('Choose the top-level group for your repository imports.')
@@ -19,5 +22,8 @@
= link_to sprite_icon('question-o'), help_page_path('user/project/import/manifest')
.gl-mb-3
- = submit_tag _('List available repositories'), class: 'gl-button btn btn-confirm'
- = link_to _('Cancel'), new_project_path, class: 'gl-button btn btn-default btn-cancel'
+ = render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm) do
+ = _('List available repositories')
+
+ = render Pajamas::ButtonComponent.new(href: new_project_path) do
+ = _('Cancel')
diff --git a/app/views/import/shared/_new_project_form.html.haml b/app/views/import/shared/_new_project_form.html.haml
index 16526382f42..6000612a285 100644
--- a/app/views/import/shared/_new_project_form.html.haml
+++ b/app/views/import/shared/_new_project_form.html.haml
@@ -2,20 +2,23 @@
.form-group.project-name.col-sm-12
= label_tag :name, _('Project name'), class: 'label-bold'
= text_field_tag :name, @name, placeholder: "My awesome project", class: "js-project-name form-control gl-form-input input-lg", autofocus: true, required: true, aria: { required: true }, data: { qa_selector: 'project_name_field' }
- .form-group.col-12.col-sm-6
+ .form-group.col-12.col-sm-6.gl-pr-0
= label_tag :namespace_id, _('Project URL'), class: 'label-bold'
- .form-group
- .input-group.gl-flex-nowrap
- - if current_user.can_select_namespace?
- .input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
- .input-group-text
- = root_url
- = select_tag :namespace_id, namespaces_options(namespace_id_from(params) || :current_user, display_path: true, extra_group: namespace_id_from(params)), class: 'select2 js-select-namespace block-truncated'
- - else
- .input-group-prepend.static-namespace.has-tooltip{ title: user_url(current_user.username) + '/' }
- .input-group-text.border-0
- #{user_url(current_user.username)}/
- = hidden_field_tag :namespace_id, current_user.namespace_id
+ .input-group.gl-flex-nowrap
+ - if current_user.can_select_namespace?
+ - namespace_id = namespace_id_from(params)
+ .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || current_user.namespace.full_path,
+ namespace_id: namespace_id || current_user.namespace_id,
+ input_id: 'namespace_id',
+ input_name: 'namespace_id',
+ root_url: root_url,
+ user_namespace_id: current_user.namespace_id } }
+ - else
+ .input-group-prepend.static-namespace.flex-shrink-0.has-tooltip{ title: user_url(current_user.username) + '/' }
+ .input-group-text.border-0
+ #{user_url(current_user.username)}/
+ = hidden_field_tag :namespace_id, current_user.namespace_id
+ .gl-align-self-center.gl-pl-5 /
.form-group.col-12.col-sm-6.project-path
= label_tag :path, _('Project slug'), class: 'label-bold'
= text_field_tag :path, @path, placeholder: "my-awesome-project", class: "js-path-name form-control gl-form-input", required: true, aria: { required: true }, data: { qa_selector: 'project_slug_field' }
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index c1ee12bb6c8..5f65405c8bc 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -17,8 +17,10 @@
= html_escape(_("You have been invited by %{link_to_inviter} to join %{source_name} %{strong_open}%{link_to_source}%{strong_close} as %{role}")) % { link_to_inviter: link_to_inviter, source_name: @invite_details[:title], strong_open: '<strong>'.html_safe, link_to_source: link_to_source, strong_close: '</strong>'.html_safe, role: @member.human_access }
.actions
- = link_to _("Accept invitation"), accept_invite_url(@token), method: :post, class: "btn gl-button btn-confirm"
- = link_to _("Decline"), decline_invite_url(@token), method: :post, class: "btn gl-button btn-danger gl-ml-3"
+ = render Pajamas::ButtonComponent.new(variant: :confirm, method: :post, href: accept_invite_url(@token)) do
+ = _("Accept invitation")
+ = render Pajamas::ButtonComponent.new(variant: :danger, method: :post, href: decline_invite_url(@token), button_options: { class: 'gl-ml-3' }) do
+ = _("Decline")
- else
%p
diff --git a/app/views/jira_connect/users/show.html.haml b/app/views/jira_connect/users/show.html.haml
index 569c4587f14..5db6cb44ff6 100644
--- a/app/views/jira_connect/users/show.html.haml
+++ b/app/views/jira_connect/users/show.html.haml
@@ -11,7 +11,10 @@
= s_('JiraService|You can now close this window and%{br}return to the GitLab for Jira application.').html_safe % { br: '<br>'.html_safe }
- if @jira_app_link
- %p= link_to s_('Integrations|Return to GitLab for Jira'), @jira_app_link, class: 'gl-button btn btn-confirm'
+ %p
+ = render Pajamas::ButtonComponent.new(href: @jira_app_link, variant: :confirm) do
+ = s_('Integrations|Return to GitLab for Jira')
+
%p= link_to _('Sign out'), destroy_user_session_path, method: :post
diff --git a/app/views/layouts/_google_tag_manager_head.html.haml b/app/views/layouts/_google_tag_manager_head.html.haml
index 97e118aba93..21b9a604a35 100644
--- a/app/views/layouts/_google_tag_manager_head.html.haml
+++ b/app/views/layouts/_google_tag_manager_head.html.haml
@@ -20,6 +20,17 @@
'wait_for_update': 500
});
+ window.geofeed = (options) => {
+ dataLayer.push({
+ 'event' : 'OneTrustCountryLoad',
+ 'oneTrustCountryId': options.country.toString()
+ })
+ }
+
+ const json = document.createElement('script');
+ json.setAttribute('src', 'https://geolocation.onetrust.com/cookieconsentpub/v1/geo/location/geofeed');
+ document.head.appendChild(json);
+
- if Feature.enabled?(:gtm_nonce, type: :ops)
= javascript_tag nonce: content_security_policy_nonce do
:plain
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 2ac926a7fc3..ea2f452b9e2 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -18,7 +18,13 @@
= favicon_link_tag favicon, id: 'favicon', data: { original_href: favicon }, type: 'image/png'
- = render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) }
+ - if startup_css_enabled?
+ = render 'layouts/startup_css', { startup_filename: local_assigns.fetch(:startup_filename, nil) }
+ - else
+ - diffs_colors = user_diffs_colors
+ = stylesheet_link_tag "themes/#{user_application_theme_css_filename}" if user_application_theme_css_filename
+ = render 'layouts/diffs_colors_css', diffs_colors if diffs_colors.present? || request.path == profile_preferences_path
+
- if user_application_theme == 'gl-dark'
%meta{ name: 'color-scheme', content: 'dark light' }
= stylesheet_link_tag_defer "application_dark"
@@ -31,16 +37,22 @@
= stylesheet_link_tag "disable_animations", media: "all" if Rails.env.test? || Gitlab.config.gitlab['disable_animations']
= stylesheet_link_tag "test_environment", media: "all" if Rails.env.test?
+ = stylesheet_link_tag_defer "fonts" if use_new_fonts?
+
= stylesheet_link_tag_defer "highlight/themes/#{user_color_scheme}"
- = render 'layouts/startup_css_activation'
+ - if startup_css_enabled?
+ = render 'layouts/startup_css_activation'
= stylesheet_link_tag 'performance_bar' if performance_bar_enabled?
= Gon::Base.render_data(nonce: content_security_policy_nonce)
= render_if_exists 'layouts/header/translations'
- = webpack_bundle_tag "sentry" if Gitlab.config.sentry.enabled
+ - if Feature.enabled?(:enable_new_sentry_clientside_integration, current_user) && Gitlab::CurrentSettings.sentry_enabled
+ = webpack_bundle_tag 'sentry'
+ - elsif Gitlab.config.sentry.enabled
+ = webpack_bundle_tag 'legacy_sentry'
= webpack_bundle_tag 'performance_bar' if performance_bar_enabled?
= yield :page_specific_javascripts
diff --git a/app/views/layouts/_loading_hints.html.haml b/app/views/layouts/_loading_hints.html.haml
index b3bb474ea43..b1d1447ae2a 100644
--- a/app/views/layouts/_loading_hints.html.haml
+++ b/app/views/layouts/_loading_hints.html.haml
@@ -13,3 +13,9 @@
= preload_link_tag(path_to_stylesheet("highlight/themes/#{user_color_scheme}"), crossorigin: css_crossorigin)
- if Gitlab::Tracking.enabled? && Gitlab::Tracking.collector_hostname
%link{ rel: 'preconnect', href: "https://#{Gitlab::Tracking.collector_hostname}", crossorigin: '' }
+ - if use_new_fonts?
+ -# Do not use preload_link_tag for fonts, to work around Firefox double-fetch bug.
+ -# See https://github.com/web-platform-tests/wpt/pull/36930
+ %link{ rel: 'preload', href: font_path('gitlab-sans/GitLabSans.woff2'), as: 'font', crossorigin: css_crossorigin }
+ %link{ rel: 'preload', href: font_path('jetbrains-mono/JetBrainsMono.woff2'), as: 'font', crossorigin: css_crossorigin }
+ = preload_link_tag(path_to_stylesheet('fonts'), crossorigin: css_crossorigin)
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index d668399b408..bb1d051f71f 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -14,6 +14,7 @@
= dispensable_render "layouts/header/registration_enabled_callout"
= dispensable_render "layouts/nav/classification_level_banner"
= yield :flash_message
+ = dispensable_render "shared/gitlab_version/security_patch_upgrade_alert"
= dispensable_render "shared/service_ping_consent"
= dispensable_render_if_exists "layouts/header/ee_subscribable_banner"
= dispensable_render_if_exists "layouts/header/seat_count_alert"
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index 0350dc82e46..daf2c582de2 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -29,7 +29,7 @@
= hidden_field_tag :scope, search_context.scope
= hidden_field_tag :search_code, search_context.code_search?
- - ref = search_context.ref if can?(current_user, :download_code, search_context.project)
+ - ref = search_context.ref if can?(current_user, :read_code, search_context.project)
= hidden_field_tag :snippets, search_context.for_snippets?
= hidden_field_tag :repository_ref, ref
= hidden_field_tag :nav_source, 'navbar'
diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml
index c4e5e811280..60eeb9a4602 100644
--- a/app/views/layouts/group_settings.html.haml
+++ b/app/views/layouts/group_settings.html.haml
@@ -1,5 +1,6 @@
- page_title _("Settings")
- nav "group"
+- add_page_specific_style 'page_bundles/settings'
- enable_search_settings locals: { container_class: 'gl-my-5' }
= render template: "layouts/group"
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 00e7a0567da..8363d424c1b 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -46,6 +46,10 @@
%li.d-md-none
= link_to _("Switch to GitLab Next"), Gitlab::Saas.canary_toggle_com_url
+ - if Feature.enabled?(:super_sidebar_nav, current_user)
+ %li.divider
+ .js-new-nav-toggle{ data: { enabled: current_user.use_new_navigation.to_s, endpoint: profile_preferences_url} }
+
- if current_user_menu?(:sign_out)
%li.divider
%li
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 47d8f5a447f..558af352ae9 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -54,7 +54,7 @@
= sprite_icon('issues')
- issues_count = assigned_issuables_count(:issues)
= gl_badge_tag({ size: :sm, variant: :success }, { class: "gl-ml-n2 #{'gl-display-none' if issues_count == 0}", "aria-label": n_("%d assigned issue", "%d assigned issues", issues_count) % issues_count }) do
- = number_with_delimiter(issues_count)
+ = assigned_open_issues_count_text
- if header_link?(:merge_requests)
= nav_link(path: 'dashboard#merge_requests', html_options: { class: "user-counter dropdown" }) do
- top_level_link = assigned_mrs_dashboard_path
@@ -119,12 +119,12 @@
= render 'layouts/header/current_user_dropdown'
- if has_impersonation_link
%li.nav-item.impersonation.ml-0
- = link_to admin_impersonation_path, class: 'nav-link impersonation-btn', method: :delete, title: _('Stop impersonation'), aria: { label: _('Stop impersonation') }, data: { toggle: 'tooltip', placement: 'bottom', container: 'body', qa_selector: 'stop_impersonation_link' } do
- = sprite_icon('incognito', size: 18)
+ = render Pajamas::ButtonComponent.new(href: admin_impersonation_path, icon: 'incognito', button_options: { title: _('Stop impersonation'), class: 'impersonation-btn', aria: { label: _('Stop impersonation') }, data: { method: :delete, toggle: 'tooltip', placement: 'bottom', container: 'body', qa_selector: 'stop_impersonation_link' } })
- if header_link?(:sign_in)
- if Gitlab.com?
%li.nav-item.gl-display-none.gl-sm-display-block
- = link_to _('Sign up now'), new_user_registration_path, class: 'gl-button btn btn-default btn-sign-in'
+ = render Pajamas::ButtonComponent.new(href: new_user_registration_path) do
+ = _('Sign up now')
%li.nav-item.gl-display-none.gl-sm-display-block
= link_to _('Login'), new_session_path(:user, redirect_to_referer: 'yes')
= render 'layouts/header/sign_in_register_button', class: 'gl-sm-display-none'
diff --git a/app/views/layouts/header/_gitlab_version.html.haml b/app/views/layouts/header/_gitlab_version.html.haml
index 2315caa5fe8..581d4d498e1 100644
--- a/app/views/layouts/header/_gitlab_version.html.haml
+++ b/app/views/layouts/header/_gitlab_version.html.haml
@@ -17,4 +17,4 @@
%span.gl-font-sm.gl-text-gray-500
#{Gitlab.version_info.major}.#{Gitlab.version_info.minor}
%span.gl-ml-2
- .js-gitlab-version-check-badge{ data: { "size": "sm" } }
+ .js-gitlab-version-check-badge{ data: { "size": "sm", "version": gitlab_version_check.to_json } }
diff --git a/app/views/layouts/header/_marketing_links.html.haml b/app/views/layouts/header/_marketing_links.html.haml
index 24069de394d..c33229e4ec4 100644
--- a/app/views/layouts/header/_marketing_links.html.haml
+++ b/app/views/layouts/header/_marketing_links.html.haml
@@ -6,29 +6,29 @@
.dropdown-menu
%ul
%li
- = link_to 'https://about.gitlab.com/stages-devops-lifecycle/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'stages-devops-lifecycle') do
= s_('LoggedOutMarketingHeader|GitLab: the DevOps platform')
%li
= link_to explore_root_path do
= s_('LoggedOutMarketingHeader|Explore GitLab')
%li
- = link_to 'https://about.gitlab.com/install/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'install') do
= s_('LoggedOutMarketingHeader|Install GitLab')
%li
- = link_to 'https://about.gitlab.com/is-it-any-good/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'is-it-any-good') do
= s_('LoggedOutMarketingHeader|How GitLab compares')
%li
- = link_to 'https://about.gitlab.com/get-started/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'get-started') do
= s_('LoggedOutMarketingHeader|Get started')
%li
- = link_to 'https://docs.gitlab.com/' do
+ = link_to Gitlab::Saas::doc_url do
= s_('LoggedOutMarketingHeader|GitLab docs')
%li
- = link_to 'https://about.gitlab.com/learn/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'learn') do
= s_('LoggedOutMarketingHeader|GitLab Learn')
%li.gl-mr-3
- = link_to 'https://about.gitlab.com/pricing/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'pricing') do
= s_('LoggedOutMarketingHeader|Pricing')
%li.gl-mr-3
- = link_to 'https://about.gitlab.com/sales/' do
+ = link_to Gitlab::Utils.append_path(promo_url, 'sales') do
= s_('LoggedOutMarketingHeader|Talk to an expert')
diff --git a/app/views/layouts/header/_registration_enabled_callout.html.haml b/app/views/layouts/header/_registration_enabled_callout.html.haml
index dd3d14a5678..52c39fce961 100644
--- a/app/views/layouts/header/_registration_enabled_callout.html.haml
+++ b/app/views/layouts/header/_registration_enabled_callout.html.haml
@@ -1,17 +1,17 @@
- return unless show_registration_enabled_user_callout?
-= render Pajamas::AlertComponent.new(title: _('Anyone can register for an account.'),
+= render Pajamas::AlertComponent.new(title: _('Check your sign-up restrictions'),
variant: :warning,
alert_options: { class: 'js-registration-enabled-callout',
data: { feature_id: Users::CalloutsHelper::REGISTRATION_ENABLED_CALLOUT,
dismiss_endpoint: callouts_path }},
close_button_options: { data: { testid: 'close-registration-enabled-callout' }}) do |c|
= c.body do
- = _('Only allow anyone to register for accounts on GitLab instances that you intend to be used by anyone. Allowing anyone to register makes GitLab instances more vulnerable.')
+ = _("Your GitLab instance allows anyone to register for an account, which is a security risk on public-facing GitLab instances. You should deactivate new sign ups if public users aren't expected to register for an account.")
= c.actions do
= link_to general_admin_application_settings_path(anchor: 'js-signup-settings'), class: 'btn gl-alert-action btn-confirm btn-md gl-button' do
%span.gl-button-text
- = _('Turn off')
+ = _('Deactivate')
%button.btn.gl-alert-action.btn-default.btn-md.gl-button.js-close
%span.gl-button-text
= _('Acknowledge')
diff --git a/app/views/layouts/header/_sign_in_register_button.html.haml b/app/views/layouts/header/_sign_in_register_button.html.haml
index 992e8785251..cadb7cfe683 100644
--- a/app/views/layouts/header/_sign_in_register_button.html.haml
+++ b/app/views/layouts/header/_sign_in_register_button.html.haml
@@ -3,4 +3,5 @@
%li.nav-item{ class: top_class }
%div
- sign_in_text = allow_signup? ? _('Sign in / Register') : _('Sign in')
- = link_to sign_in_text, new_session_path(:user, redirect_to_referer: 'yes'), class: 'gl-button btn btn-default btn-sign-in'
+ = render Pajamas::ButtonComponent.new(href: new_session_path(:user, redirect_to_referer: 'yes'), button_options: { class: 'btn-sign-in'}) do
+ = sign_in_text
diff --git a/app/views/layouts/jira_connect.html.haml b/app/views/layouts/jira_connect.html.haml
index 6acd7799875..80bbe578510 100644
--- a/app/views/layouts/jira_connect.html.haml
+++ b/app/views/layouts/jira_connect.html.haml
@@ -5,7 +5,7 @@
GitLab
= yield :page_specific_styles
- = javascript_include_tag 'https://connect-cdn.atl-paas.net/all.js'
+ = javascript_include_tag Gitlab.config.jira_connect.atlassian_js_url
= Gon::Base.render_data(nonce: content_security_policy_nonce)
= yield :head
%body
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 8815dec5a6b..717175e8eb3 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -14,7 +14,7 @@
%span.nav-item-name
= _('Overview')
%ul.sidebar-sub-level-items
- = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: %w[dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts], html_options: { class: "fly-out-top-item" }) do
= link_to admin_root_path do
%strong.fly-out-top-item-name
= _('Overview')
@@ -82,7 +82,7 @@
= _('Monitoring')
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_monitoring_submenu_content' } }
- = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: admin_monitoring_nav_links, html_options: { class: "fly-out-top-item" }) do
= link_to admin_system_info_path do
%strong.fly-out-top-item-name
= _('Monitoring')
@@ -117,7 +117,7 @@
%span.nav-item-name
= _('Messages')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :broadcast_messages, html_options: { class: "fly-out-top-item" }) do
= link_to admin_broadcast_messages_path do
%strong.fly-out-top-item-name
= _('Messages')
@@ -129,7 +129,7 @@
%span.nav-item-name
= _('System Hooks')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:hooks, :hook_logs], html_options: { class: "fly-out-top-item" }) do
= link_to admin_hooks_path do
%strong.fly-out-top-item-name
= _('System Hooks')
@@ -141,7 +141,7 @@
%span.nav-item-name
= _('Applications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :applications, html_options: { class: "fly-out-top-item" }) do
= link_to admin_applications_path do
%strong.fly-out-top-item-name
= _('Applications')
@@ -154,7 +154,7 @@
= _('Abuse Reports')
= gl_badge_tag number_with_delimiter(AbuseReport.count(:all)), variant: :info, size: :sm
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :abuse_reports, html_options: { class: "fly-out-top-item" }) do
= link_to admin_abuse_reports_path do
%strong.fly-out-top-item-name
= _('Abuse Reports')
@@ -170,7 +170,7 @@
%span.nav-item-name
= _('Kubernetes')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :clusters, html_options: { class: "fly-out-top-item" }) do
= link_to admin_clusters_path do
%strong.fly-out-top-item-name
= _('Kubernetes')
@@ -183,7 +183,7 @@
%span.nav-item-name
= _('Spam Logs')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :spam_logs, html_options: { class: "fly-out-top-item" }) do
= link_to admin_spam_logs_path do
%strong.fly-out-top-item-name
= _('Spam Logs')
@@ -199,7 +199,7 @@
%span.nav-item-name
= _('Deploy Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :deploy_keys, html_options: { class: "fly-out-top-item" }) do
= link_to admin_deploy_keys_path do
%strong.fly-out-top-item-name
= _('Deploy Keys')
@@ -213,7 +213,7 @@
%span.nav-item-name
= _('Labels')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :labels, html_options: { class: "fly-out-top-item" }) do
= link_to admin_labels_path do
%strong.fly-out-top-item-name
= _('Labels')
@@ -227,7 +227,7 @@
%ul.sidebar-sub-level-items{ data: { qa_selector: 'admin_settings_submenu_content' } }
-# This active_nav_link check is also used in `app/views/layouts/admin.html.haml`
- = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:application_settings, :integrations, :appearances], html_options: { class: "fly-out-top-item" }) do
= link_to general_admin_application_settings_path do
%strong.fly-out-top-item-name
= _('Settings')
@@ -273,7 +273,7 @@
= link_to network_admin_application_settings_path, title: _('Network'), data: { qa_selector: 'admin_settings_network_link' } do
%span
= _('Network')
- = nav_link(controller: :appearances ) do
+ = nav_link(controller: :appearances) do
= link_to admin_application_settings_appearances_path do
%span
= _('Appearance')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 0e3327935ca..e1978009114 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -12,7 +12,7 @@
%span.nav-item-name
= _('Profile')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: 'profiles#show', html_options: { class: "fly-out-top-item" }) do
= link_to profile_path do
%strong.fly-out-top-item-name
= _('Profile')
@@ -23,7 +23,7 @@
%span.nav-item-name
= _('Account')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: [:accounts, :two_factor_auths], html_options: { class: "fly-out-top-item" }) do
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
@@ -36,7 +36,7 @@
%span.nav-item-name
= _('Applications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: 'oauth/applications', html_options: { class: "fly-out-top-item" }) do
= link_to applications_profile_path do
%strong.fly-out-top-item-name
= _('Applications')
@@ -47,7 +47,7 @@
%span.nav-item-name
= _('Chat')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :chat_names, html_options: { class: "fly-out-top-item" }) do
= link_to profile_chat_names_path do
%strong.fly-out-top-item-name
= _('Chat')
@@ -59,7 +59,7 @@
%span.nav-item-name
= _('Access Tokens')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :personal_access_tokens, html_options: { class: "fly-out-top-item" }) do
= link_to profile_personal_access_tokens_path do
%strong.fly-out-top-item-name
= _('Access Tokens')
@@ -70,7 +70,7 @@
%span.nav-item-name
= _('Emails')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :emails, html_options: { class: "fly-out-top-item" }) do
= link_to profile_emails_path do
%strong.fly-out-top-item-name
= _('Emails')
@@ -82,7 +82,7 @@
%span.nav-item-name
= _('Password')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :passwords, html_options: { class: "fly-out-top-item" }) do
= link_to edit_profile_password_path do
%strong.fly-out-top-item-name
= _('Password')
@@ -93,7 +93,7 @@
%span.nav-item-name
= _('Notifications')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :notifications, html_options: { class: "fly-out-top-item" }) do
= link_to profile_notifications_path do
%strong.fly-out-top-item-name
= _('Notifications')
@@ -104,7 +104,7 @@
%span.nav-item-name
= _('SSH Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_keys_path do
%strong.fly-out-top-item-name
= _('SSH Keys')
@@ -115,7 +115,7 @@
%span.nav-item-name
= _('GPG Keys')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :gpg_keys, html_options: { class: "fly-out-top-item" }) do
= link_to profile_gpg_keys_path do
%strong.fly-out-top-item-name
= _('GPG Keys')
@@ -126,7 +126,7 @@
%span.nav-item-name
= _('Preferences')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :preferences, html_options: { class: "fly-out-top-item" }) do
= link_to profile_preferences_path do
%strong.fly-out-top-item-name
= _('Preferences')
@@ -137,7 +137,7 @@
%span.nav-item-name
= _('Active Sessions')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(controller: :active_sessions, html_options: { class: "fly-out-top-item" }) do
= link_to profile_active_sessions_path do
%strong.fly-out-top-item-name
= _('Active Sessions')
@@ -148,7 +148,7 @@
%span.nav-item-name
= _('Authentication log')
%ul.sidebar-sub-level-items.is-fly-out-only
- = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" } ) do
+ = nav_link(path: 'profiles#audit_log', html_options: { class: "fly-out-top-item" }) do
= link_to audit_log_profile_path do
%strong.fly-out-top-item-name
= _('Authentication Log')
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index a06f9f8d6ef..67c3cd9cc54 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -1 +1 @@
-= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref))
+= render partial: 'shared/nav/sidebar', object: Sidebars::Projects::Panel.new(project_sidebar_context(@project, current_user, current_ref, ref_type: @ref_type))
diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml
index 97d9f2fbc78..29e30c4434f 100644
--- a/app/views/layouts/project_settings.html.haml
+++ b/app/views/layouts/project_settings.html.haml
@@ -1,5 +1,6 @@
- page_title _("Settings")
- nav "project"
+- add_page_specific_style 'page_bundles/settings'
- enable_search_settings locals: { container_class: 'gl-my-5' }
diff --git a/app/views/layouts/search.html.haml b/app/views/layouts/search.html.haml
index dd4b9e45207..44c4b14e90d 100644
--- a/app/views/layouts/search.html.haml
+++ b/app/views/layouts/search.html.haml
@@ -1,4 +1,5 @@
- page_title _("Search")
- header_title _("Search"), search_path
+- add_page_specific_style 'page_bundles/search'
= render template: "layouts/application"
diff --git a/app/views/notify/_reassigned_issuable_email.html.haml b/app/views/notify/_reassigned_issuable_email.html.haml
index 54e51e07c86..ead8e5d0a7e 100644
--- a/app/views/notify/_reassigned_issuable_email.html.haml
+++ b/app/views/notify/_reassigned_issuable_email.html.haml
@@ -1,4 +1,4 @@
-- to_names = content_tag(:strong, issuable.assignees.any? ? sanitize_name(issuable.assignee_list) : s_('Unassigned'))
+- to_names = content_tag(:strong, issuable.assignees.any? ? sanitize_name(issuable.assignee_list) : _('Unassigned'))
%p
- if previous_assignees.any?
diff --git a/app/views/notify/access_token_revoked_email.html.haml b/app/views/notify/access_token_revoked_email.html.haml
index 4d9b9e14d14..ecd2b3e84b2 100644
--- a/app/views/notify/access_token_revoked_email.html.haml
+++ b/app/views/notify/access_token_revoked_email.html.haml
@@ -2,6 +2,8 @@
= _('Hi %{username}!') % { username: sanitize_name(@user.name) }
%p
= html_escape(_('A personal access token, named %{code_start}%{token_name}%{code_end}, has been revoked.')) % { code_start: '<code>'.html_safe, token_name: @token_name, code_end: '</code>'.html_safe }
+- if @source == 'secret_detection'
+ = _('We found your token in a public project and have automatically revoked it to protect your account.')
%p
- pat_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: @target_url }
= html_escape(_('You can check your tokens or create a new one in your %{pat_link_start}personal access tokens settings%{pat_link_end}.')) % { pat_link_start: pat_link_start, pat_link_end: '</a>'.html_safe }
diff --git a/app/views/notify/access_token_revoked_email.text.erb b/app/views/notify/access_token_revoked_email.text.erb
index 17dd628d76c..a0623f96488 100644
--- a/app/views/notify/access_token_revoked_email.text.erb
+++ b/app/views/notify/access_token_revoked_email.text.erb
@@ -1,5 +1,9 @@
<%= _('Hi %{username}!') % { username: sanitize_name(@user.name) } %>
<%= _('A personal access token, named %{token_name}, has been revoked.') % { token_name: @token_name } %>
+<% if @source == 'secret_detection' %>
+
+<%= _('We found your token in a public project and have automatically revoked it to protect your account.') %>
+<% end %>
<%= _('You can check your tokens or create a new one in your personal access tokens settings %{pat_link}.') % { pat_link: @target_url } %>
diff --git a/app/views/notify/autodevops_disabled_email.html.haml b/app/views/notify/autodevops_disabled_email.html.haml
index bdf2a1136d3..d6812821966 100644
--- a/app/views/notify/autodevops_disabled_email.html.haml
+++ b/app/views/notify/autodevops_disabled_email.html.haml
@@ -11,7 +11,7 @@
- link_style = "color: #1b69b6; text-decoration:none;"
- pipeline_link = link_to("\##{@pipeline.iid}", pipeline_url(@pipeline), style: link_style).html_safe
- project_link = link_to(@project.name, project_url(@project), style: link_style).html_safe
- - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style ).html_safe
+ - supported_langs_link = link_to(s_('Notify|currently supported languages'), 'https://docs.gitlab.com/ee/topics/autodevops/#currently-supported-languages', style: link_style).html_safe
- settings_link = link_to(s_('Notify|CI/CD project settings'), project_settings_ci_cd_url(@project), style: link_style).html_safe
= s_('Notify|The Auto DevOps pipeline failed for pipeline %{pipeline_link} and has been disabled for %{project_link}. In order to use the Auto DevOps pipeline with your project, please review the %{supported_langs_link}, adjust your project accordingly, and turn on the Auto DevOps pipeline within your %{settings_link}.').html_safe % { pipeline_link: pipeline_link, project_link: project_link, supported_langs_link: supported_langs_link, settings_link: settings_link }
diff --git a/app/views/notify/issue_moved_email.html.haml b/app/views/notify/issue_moved_email.html.haml
index c77a863d1a4..666aa45540e 100644
--- a/app/views/notify/issue_moved_email.html.haml
+++ b/app/views/notify/issue_moved_email.html.haml
@@ -2,6 +2,6 @@
= s_('Notify|Issue was moved to another project.')
- if @can_access_project
%p
- = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) } ).html_safe
+ = sprintf(s_('Notify|New issue: %{project_issue_url}'), { project_issue_url: link_to(@new_issue.title, project_issue_url(@new_project, @new_issue)) }).html_safe
- else
= s_("Notify|You don't have access to the project.")
diff --git a/app/views/notify/repository_push_email.html.haml b/app/views/notify/repository_push_email.html.haml
index ee219914513..d493f9d5d98 100644
--- a/app/views/notify/repository_push_email.html.haml
+++ b/app/views/notify/repository_push_email.html.haml
@@ -60,7 +60,7 @@
- if diff_file.deleted_file?
%strong<
= diff_file.old_path
- = s_('deleted')
+ = _('deleted')
- elsif diff_file.renamed_file?
%strong<
= diff_file.old_path
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index cdd5a9ae7a1..bc0d615bb64 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -24,10 +24,12 @@
%p
#{_('Status')}: #{current_user.two_factor_enabled? ? _('Enabled') : _('Disabled')}
- if current_user.two_factor_enabled?
- = link_to _('Manage two-factor authentication'), profile_two_factor_auth_path, class: 'gl-button btn btn-confirm'
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: profile_two_factor_auth_path) do
+ = _('Manage two-factor authentication')
- else
.gl-mb-3
- = link_to _('Enable two-factor authentication'), profile_two_factor_auth_path, class: 'gl-button btn btn-confirm', data: { qa_selector: 'enable_2fa_button' }
+ = render Pajamas::ButtonComponent.new(variant: :confirm, href: profile_two_factor_auth_path, button_options: { data: { qa_selector: 'enable_2fa_button' }}) do
+ = _('Enable two-factor authentication')
.col-lg-12
%hr
diff --git a/app/views/profiles/keys/_form.html.haml b/app/views/profiles/keys/_form.html.haml
index e6d91543585..5c4ea7b2ecb 100644
--- a/app/views/profiles/keys/_form.html.haml
+++ b/app/views/profiles/keys/_form.html.haml
@@ -12,7 +12,11 @@
= f.label :title, s_('Profiles|Title'), class: 'label-bold'
= f.text_field :title, class: "form-control gl-form-input input-lg", required: true, placeholder: s_('Profiles|Example: MacBook key'), data: { qa_selector: 'key_title_field' }
%p.form-text.text-muted= s_('Profiles|Key titles are publicly visible.')
-
+ .form-row
+ .col.form-group
+ = f.label :usage_type, s_('Profiles|Usage type')
+ .gl-md-form-input-lg
+ = f.select :usage_type, options_for_select(ssh_key_usage_types, :auth_and_signing), {}, { class: 'gl-form-select custom-select' }
.form-row
.col.form-group
.js-access-tokens-expires-at{ data: {min_date: Date.tomorrow, max_date: max_date, default_date_offset: 365, description: ssh_key_expires_field_description } }
diff --git a/app/views/profiles/keys/_key.html.haml b/app/views/profiles/keys/_key.html.haml
index de4a19bdad7..219e7c4d2fe 100644
--- a/app/views/profiles/keys/_key.html.haml
+++ b/app/views/profiles/keys/_key.html.haml
@@ -25,7 +25,10 @@
%span.expires.gl-mr-3
= key.expired? ? s_('Profiles|Expired:') : s_('Profiles|Expires:')
= key.expires_at ? key.expires_at.to_date : _('Never')
+ %span.last-used-at.gl-mr-3
+ = s_('Profiles|Usage type:')
+ = ssh_key_usage_types.invert[key.usage_type]
%span.key-created-at.gl-display-flex.gl-align-items-center
- if key.can_delete?
.gl-ml-3
- = render 'shared/ssh_keys/key_delete', html_class: "btn gl-button btn-icon btn-default js-confirm-modal-button", button_data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin))
+ = render 'shared/ssh_keys/key_delete', icon: true, button_data: ssh_key_delete_modal_data(key, path_to_key(key, is_admin))
diff --git a/app/views/profiles/keys/_key_details.html.haml b/app/views/profiles/keys/_key_details.html.haml
index 04fa1d96204..3c05502be57 100644
--- a/app/views/profiles/keys/_key_details.html.haml
+++ b/app/views/profiles/keys/_key_details.html.haml
@@ -10,6 +10,9 @@
%span.light= _('Title:')
%strong= @key.title
%li
+ %span.light= s_('Profiles|Usage type:')
+ %strong= ssh_key_usage_types.invert[@key.usage_type]
+ %li
%span.light= _('Created on:')
%strong= @key.created_at.to_s(:medium)
%li
@@ -39,4 +42,4 @@
.col-md-12
.float-right
- if @key.can_delete?
- = render 'shared/ssh_keys/key_delete', text: _('Delete'), html_class: "btn btn-danger gl-button delete-key js-confirm-modal-button", button_data: ssh_key_delete_modal_data(@key, path_to_key(@key, is_admin))
+ = render 'shared/ssh_keys/key_delete', button_data: ssh_key_delete_modal_data(@key, path_to_key(@key, is_admin))
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index a1d6ef3fec5..24ef9cf4dec 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -73,7 +73,7 @@
.form-group
= f.label :layout, class: 'label-bold' do
= s_('Preferences|Layout width')
- = f.select :layout, layout_choices, {}, class: 'select2'
+ = f.select :layout, layout_choices, {}, class: 'gl-form-select custom-select'
.form-text.text-muted
= s_('Preferences|Choose between fixed (max. 1280px) and fluid (%{percentage}) application layout.').html_safe % { percentage: '100%' }
.form-group
@@ -88,7 +88,7 @@
.form-group
= f.label :project_view, class: 'label-bold' do
= s_('Preferences|Project overview content')
- = f.select :project_view, project_view_choices, {}, class: 'select2'
+ = f.select :project_view, project_view_choices, {}, class: 'gl-form-select custom-select'
.form-text.text-muted
= s_('Preferences|Choose what content you want to see on a project’s overview page.')
.form-group
@@ -103,7 +103,7 @@
- supported_characters = %w(" ' ` &#40; [ { < * _).map { |char| "<code>#{char}</code>" }.join(', ')
= f.gitlab_ui_checkbox_component :markdown_surround_selection,
s_('Preferences|Surround text selection when typing quotes or brackets'),
- help_text: sprintf(s_( "Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe
+ help_text: sprintf(s_("Preferences|When you type in a description or comment box, selected text is surrounded by the corresponding character after typing one of the following characters: %{supported_characters}."), { supported_characters: supported_characters }).html_safe
.form-group
= f.gitlab_ui_checkbox_component :markdown_automatic_lists,
s_('Preferences|Automatically add new list items'),
@@ -144,9 +144,24 @@
.form-group
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
- = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'select2'
+ = f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'gl-form-select custom-select'
.col-sm-12
%hr
+ - if Feature.enabled?(:vscode_web_ide, current_user)
+ .row.js-preferences-form.js-search-settings-section
+ .col-lg-4.profile-settings-sidebar#web-ide
+ %h4.gl-mt-0
+ = s_('Preferences|Web IDE')
+ %p
+ = s_('Preferences|The Web IDE Beta is the default Web IDE experience.')
+ = link_to _('Learn more'), help_page_path('user/project/web_ide_beta/index.md'), target: '_blank', rel: 'noopener noreferrer'
+ .col-lg-8
+ .form-group
+ = f.gitlab_ui_checkbox_component :use_legacy_web_ide,
+ s_('Preferences|Opt out of the Web IDE Beta'),
+ help_text: s_('Preferences|The Web IDE remains available alongside the Beta.')
+ .col-sm-12
+ %hr
.row.js-preferences-form.js-search-settings-section
.col-lg-4.profile-settings-sidebar#time-preferences
%h4.gl-mt-0
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 51222784847..712d6fabf82 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -1,20 +1,23 @@
+- show_auto_devops_callout = show_auto_devops_callout?(@project)
- is_project_overview = local_assigns.fetch(:is_project_overview, false)
- ref = local_assigns.fetch(:ref) { current_ref }
- project = local_assigns.fetch(:project) { @project }
-- show_auto_devops_callout = show_auto_devops_callout?(@project)
- add_page_startup_api_call logs_file_project_ref_path(@project, ref, @path, format: "json", offset: 0)
- if readme_path = @project.repository.readme_path
- add_page_startup_api_call project_blob_path(@project, tree_join(@ref, readme_path), viewer: "rich", format: "json")
#tree-holder.tree-holder.clearfix.js-per-page{ data: { blame_per_page: Projects::BlameService::PER_PAGE } }
- .nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch
- = render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview
-
.info-well.gl-display-none.gl-sm-display-flex.project-last-commit.gl-flex-direction-column
#js-last-commit.gl-m-auto
= gl_loading_icon(size: 'md')
#js-code-owners
+ .nav-block.gl-display-flex.gl-xs-flex-direction-column.gl-align-items-stretch
+ = render 'projects/tree/tree_header', tree: @tree, is_project_overview: is_project_overview
+
+ - if project.forked? && Feature.enabled?(:fork_divergence_counts, @project.fork_source)
+ = render 'projects/fork_info'
+
- if is_project_overview
.project-buttons.gl-mb-5.js-show-on-project-root{ data: { qa_selector: 'project_buttons' } }
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index 7395495b537..2d9f7e49ddc 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -2,11 +2,13 @@
= content_for :flash_message do
= render partial: 'deletion_failed', locals: { project: project }
- - if current_user && can?(current_user, :download_code, project)
+ - if current_user && can?(current_user, :read_code, project)
= render 'shared/no_ssh'
= render 'shared/no_password'
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
= render_if_exists 'projects/above_size_limit_warning', project: project
= render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
= render_if_exists 'projects/terraform_banner', project: project
diff --git a/app/views/projects/_fork_info.html.haml b/app/views/projects/_fork_info.html.haml
new file mode 100644
index 00000000000..7fe30214e97
--- /dev/null
+++ b/app/views/projects/_fork_info.html.haml
@@ -0,0 +1,14 @@
+.info-well.gl-sm-display-flex.gl-flex-direction-column
+ .well-segment.gl-p-5.gl-w-full.gl-display-flex
+ .gl-icon.s32.gl-mt-4.gl-mr-4.gl-text-center
+ = sprite_icon('fork')
+ - source = visible_fork_source(@project)
+ - if source
+ %div
+ #{ s_('ForkedFromProjectPath|Forked from') }
+ = link_to source.full_name, project_path(source), data: { qa_selector: 'forked_from_link' }
+ .gl-text-secondary
+ = fork_divergence_message(::Projects::Forks::DivergenceCounts.new(@project, @ref).counts)
+ - else
+ .gl-py-4
+ = s_('ForkedFromProjectPath|Forked from an inaccessible project')
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index a862b841008..dc426f2f6b7 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -4,17 +4,16 @@
- cache_enabled = Feature.enabled?(:cache_home_panel, @project, type: :development)
.project-home-panel.js-show-on-project-root.gl-my-5{ class: [("empty-project" if empty_repo)] }
- .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3
- .home-panel-title-row.gl-display-flex
+ .gl-display-flex.gl-justify-content-space-between.gl-flex-wrap.gl-sm-flex-direction-column.gl-mb-3.gl-gap-5
+ .home-panel-title-row.gl-display-flex.gl-align-items-center
%div{ class: 'avatar-container rect-avatar s64 home-panel-avatar gl-flex-shrink-0 gl-w-11 gl-h-11 gl-mr-3! float-none' }
= project_icon(@project, alt: @project.name, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'image')
- .d-flex.flex-column.flex-wrap.align-items-baseline
- .d-inline-flex.align-items-baseline
- %h1.home-panel-title.gl-mt-3.gl-mb-2.gl-font-size-h1{ data: { qa_selector: 'project_name_content' }, itemprop: 'name' }
- = @project.name
- %span.visibility-icon.gl-text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@project) }
- = visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
- = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project
+ %div
+ %h1.home-panel-title.gl-font-size-h1.gl-mt-3.gl-mb-2.gl-display-flex{ data: { qa_selector: 'project_name_content' }, itemprop: 'name' }
+ = @project.name
+ %span.visibility-icon.gl-text-secondary.has-tooltip.gl-ml-2{ data: { container: 'body' }, title: visibility_icon_description(@project) }
+ = visibility_level_icon(@project.visibility_level, options: { class: 'icon' })
+ = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: @project, additional_classes: 'gl-align-self-center gl-ml-2'
.home-panel-metadata.gl-font-sm.gl-text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal{ data: { qa_selector: 'project_id_content' }, itemprop: 'identifier' }
- if can?(current_user, :read_project, @project)
%span.gl-display-inline-block.gl-vertical-align-middle
@@ -25,22 +24,20 @@
= render 'shared/members/access_request_links', source: @project
= cache_if(cache_enabled, [@project, @project.star_count, @project.forks_count, :buttons, current_user, @notification_setting], expires_in: 1.day) do
- .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-start.gl-flex-wrap.gl-mt-5
+ .project-repo-buttons.gl-display-flex.gl-justify-content-md-end.gl-align-items-center.gl-flex-wrap.gl-gap-3
- if current_user
- if current_user.admin?
- = link_to [:admin, @project], class: 'btn gl-button btn-icon gl-align-self-start gl-py-2! gl-mr-3', title: _('View project in admin area'),
+ = link_to [:admin, @project], class: 'btn btn-default gl-button btn-icon', title: _('View project in admin area'),
data: {toggle: 'tooltip', placement: 'top', container: 'body'} do
= sprite_icon('admin')
- .gl-display-flex.gl-align-items-start.gl-mr-3
- - if @notification_setting
- .js-vue-notification-dropdown{ data: { button_size: "small", disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } }
+ - if @notification_setting
+ .js-vue-notification-dropdown{ data: { disabled: emails_disabled.to_s, dropdown_items: notification_dropdown_items(@notification_setting).to_json, notification_level: @notification_setting.level, help_page_path: help_page_path('user/profile/notifications'), project_id: @project.id, no_flip: 'true' } }
- .count-buttons.gl-display-flex.gl-align-items-flex-start
- = render 'projects/buttons/star'
- = render 'projects/buttons/fork'
+ = render 'projects/buttons/star'
+ = render 'projects/buttons/fork'
- - if can?(current_user, :download_code, @project)
- = cache_if(cache_enabled, [@project, :download_code], expires_in: 1.minute) do
+ - if can?(current_user, :read_code, @project)
+ = cache_if(cache_enabled, [@project, :read_code], expires_in: 1.minute) do
%nav.project-stats
- if @project.empty_repo?
= render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
@@ -56,7 +53,7 @@
%button.btn.gl-button.btn-blank.btn-link.js-read-more-trigger.d-lg-none{ type: "button" }
= _("Read more")
- - if @project.forked?
+ - if @project.forked? && Feature.disabled?(:fork_divergence_counts, @project.fork_source)
%p
- source = visible_fork_source(@project)
- if source
diff --git a/app/views/projects/_invite_groups_modal.html.haml b/app/views/projects/_invite_groups_modal.html.haml
index 40dc0009b24..101acd9149e 100644
--- a/app/views/projects/_invite_groups_modal.html.haml
+++ b/app/views/projects/_invite_groups_modal.html.haml
@@ -1,3 +1,3 @@
- return unless can_invite_members_for_project?(project)
-.js-invite-groups-modal{ data: common_invite_group_modal_data(project, ProjectMember, 'true') }
+.js-invite-groups-modal{ data: { reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s }.merge(common_invite_group_modal_data(project, ProjectMember, 'true')) }
diff --git a/app/views/projects/_invite_members_modal.html.haml b/app/views/projects/_invite_members_modal.html.haml
index 16288f4357a..53f74a0f270 100644
--- a/app/views/projects/_invite_members_modal.html.haml
+++ b/app/views/projects/_invite_members_modal.html.haml
@@ -2,4 +2,5 @@
.js-invite-members-modal{ data: { is_project: 'true',
access_levels: ProjectMember.permissible_access_level_roles(current_user, project).to_json,
+ reload_page_on_submit: local_assigns.fetch(:reload_page_on_submit, false).to_s,
help_link: help_page_url('user/permissions') }.merge(common_invite_modal_dataset(project)).merge(users_filter_data(project.group)) }
diff --git a/app/views/projects/_merge_request_merge_checks_settings.html.haml b/app/views/projects/_merge_request_merge_checks_settings.html.haml
index 8c12399fdbb..bb7a7731067 100644
--- a/app/views/projects/_merge_request_merge_checks_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_checks_settings.html.haml
@@ -3,17 +3,6 @@
.form-group
%b= s_('ProjectSettings|Merge checks')
%p.text-secondary= s_('ProjectSettings|These checks must pass before merge requests can be merged.')
- .builds-feature
- = form.gitlab_ui_checkbox_component :only_allow_merge_if_pipeline_succeeds,
- s_('ProjectSettings|Pipelines must succeed'),
- help_text: s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
- .gl-pl-6
- = form.gitlab_ui_checkbox_component :allow_merge_on_skipped_pipeline,
- s_('ProjectSettings|Skipped pipelines are considered successful'),
- help_text: s_('ProjectSettings|Introduces the risk of merging changes that do not pass the pipeline.'),
- checkbox_options: { class: 'gl-pl-6' }
+ = 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
- = form.gitlab_ui_checkbox_component :only_allow_merge_if_all_discussions_are_resolved,
- s_('ProjectSettings|All threads must be resolved'),
- checkbox_options: { data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' } }
= render_if_exists 'projects/merge_request_merge_checks_jira_enforcement', form: form, project: @project
diff --git a/app/views/projects/_merge_request_pipelines_and_threads_options.html.haml b/app/views/projects/_merge_request_pipelines_and_threads_options.html.haml
new file mode 100644
index 00000000000..94f8d3cc4a3
--- /dev/null
+++ b/app/views/projects/_merge_request_pipelines_and_threads_options.html.haml
@@ -0,0 +1,13 @@
+- form = local_assigns.fetch(:form)
+
+= form.gitlab_ui_checkbox_component :only_allow_merge_if_pipeline_succeeds,
+ s_('ProjectSettings|Pipelines must succeed'),
+ help_text: s_("ProjectSettings|Merge requests can't be merged if the latest pipeline did not succeed or is still running.")
+.gl-pl-6
+ = form.gitlab_ui_checkbox_component :allow_merge_on_skipped_pipeline,
+ s_('ProjectSettings|Skipped pipelines are considered successful'),
+ help_text: s_('ProjectSettings|Introduces the risk of merging changes that do not pass the pipeline.'),
+ checkbox_options: { class: 'gl-pl-6' }
+= form.gitlab_ui_checkbox_component :only_allow_merge_if_all_discussions_are_resolved,
+ s_('ProjectSettings|All threads must be resolved'),
+ checkbox_options: { data: { qa_selector: 'allow_merge_if_all_discussions_are_resolved_checkbox' } }
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index 0699e39b420..ec83782985b 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -10,6 +10,7 @@
= f.label :name, class: 'label-bold' do
%span= _("Project name")
= f.text_field :name, placeholder: "My awesome project", class: "form-control gl-form-input input-lg", data: { qa_selector: 'project_name', track_label: "#{track_label}", track_action: "activate_form_input", track_property: "project_name", track_value: "" }, required: true, aria: { required: true }
+ #project_name_error.gl-field-error.hidden
.form-group.project-path.col-sm-6.gl-pr-0
= f.label :namespace_id, class: 'label-bold' do
%span= _('Project URL')
@@ -18,6 +19,8 @@
- namespace_id = namespace_id_from(params)
.js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path || @current_user_group&.full_path,
namespace_id: namespace_id || @current_user_group&.id,
+ input_id: 'project_namespace_id',
+ input_name: 'project[namespace_id]',
root_url: root_url,
track_label: track_label,
user_namespace_id: current_user.namespace.id } }
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index a907e175443..87a6b54d697 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -28,7 +28,8 @@
.file-buttons.gl-display-flex.gl-align-items-center.gl-justify-content-end
- if is_markdown
- = render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false
+ - unless Feature.enabled?(:source_editor_toolbar, current_user)
+ = render 'shared/blob/markdown_buttons', show_fullscreen_button: false, supports_file_upload: false
%span.soft-wrap-toggle
= render Pajamas::ButtonComponent.new(icon: 'soft-unwrap', button_options: { class: 'no-wrap' }) do
= _("No wrap")
diff --git a/app/views/projects/blob/_template_selectors.html.haml b/app/views/projects/blob/_template_selectors.html.haml
index 249c474587c..4fe68c1ce1a 100644
--- a/app/views/projects/blob/_template_selectors.html.haml
+++ b/app/views/projects/blob/_template_selectors.html.haml
@@ -4,12 +4,12 @@
- toggle_text = should_suggest_gitlab_ci_yml? ? '.gitlab-ci.yml' : 'Select a template type'
= dropdown_tag(_(toggle_text), options: { toggle_class: 'js-template-type-selector', dropdown_class: 'dropdown-menu-selectable', data: { qa_selector: 'template_type_dropdown' } })
.license-selector.js-license-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-license-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: licenses_for_select(@project), project: @project.name, fullname: @project.namespace.human_name, qa_selector: 'license_dropdown' } })
.gitignore-selector.js-gitignore-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitignore-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitignore_names(@project), qa_selector: 'gitignore_dropdown' } })
.metrics-dashboard-selector.js-metrics-dashboard-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-metrics-dashboard-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: metrics_dashboard_ymls(@project), qa_selector: 'metrics_dashboard_dropdown' } })
#gitlab-ci-yml-selector.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-gitlab-ci-yml-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls(@project), selected: params[:template], qa_selector: 'gitlab_ci_yml_dropdown' } })
.dockerfile-selector.js-dockerfile-selector-wrap.js-template-selector-wrap.hidden
- = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } } )
+ = dropdown_tag(_("Apply a template"), options: { toggle_class: 'js-dockerfile-selector', dropdown_class: 'dropdown-menu-selectable', filter: true, placeholder: "Filter", data: { data: dockerfile_names(@project), qa_selector: 'dockerfile_dropdown' } })
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 63d0cf7145d..91efd5ef048 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -17,18 +17,12 @@
.form-text.text-muted.text-danger.js-branch-name-error
.form-group.row
= label_tag :ref, _('Create from'), class: 'col-form-label col-sm-2'
- .col-sm-10.create-from
- .dropdown
- = hidden_field_tag :ref, default_ref
- = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select monospace', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
- .text-left.dropdown-toggle-text= default_ref
- = sprite_icon('chevron-down', css_class: "dropdown-menu-toggle-icon gl-top-3")
- = render 'shared/ref_dropdown', dropdown_class: 'wide'
+ .col-sm-auto.create-from
+ .js-new-branch-ref-selector{ data: { project_id: @project.id, default_branch_name: default_ref, hidden_input_name: 'ref' } }
.form-text.text-muted
= _('Existing branch name, tag, or commit SHA')
.form-actions
= render Pajamas::ButtonComponent.new(variant: :confirm, button_options: { type: 'submit', class: 'gl-mr-3' }) do
= _('Create branch')
= link_to _('Cancel'), project_branches_path(@project), class: 'gl-button btn btn-default btn-cancel'
--# haml-lint:disable InlineJavaScript
-%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
+
diff --git a/app/views/projects/buttons/_clone.html.haml b/app/views/projects/buttons/_clone.html.haml
index 34aecd31c57..a755cb9f5b0 100644
--- a/app/views/projects/buttons/_clone.html.haml
+++ b/app/views/projects/buttons/_clone.html.haml
@@ -27,28 +27,28 @@
= render_if_exists 'projects/buttons/geo'
= render_if_exists 'projects/buttons/kerberos_clone_field'
%li.divider.mt-2
- %li.pt-2.gl-new-dropdown-item
+ %li.pt-2.gl-dropdown-item
%label.label-bold{ class: 'gl-px-4!' }
= _('Open in your IDE')
- if ssh_enabled?
- escaped_ssh_url_to_repo = CGI.escape(project.ssh_url_to_repo)
%a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_ssh_url_to_repo }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Visual Studio Code (SSH)')
- if http_enabled?
- escaped_http_url_to_repo = CGI.escape(project.http_url_to_repo)
%a.dropdown-item.open-with-link{ href: 'vscode://vscode.git/clone?url=' + escaped_http_url_to_repo }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Visual Studio Code (HTTPS)')
- if ssh_enabled?
%a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_ssh_url_to_repo }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('IntelliJ IDEA (SSH)')
- if http_enabled?
%a.dropdown-item.open-with-link{ href: 'jetbrains://idea/checkout/git?idea.required.plugins.id=Git4Idea&checkout.repo=' + escaped_http_url_to_repo }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('IntelliJ IDEA (HTTPS)')
- if show_xcode_link?(@project)
%a.dropdown-item.open-with-link{ href: xcode_uri_to_repo(@project) }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _("Xcode")
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 23dcb7f41e1..1fbc399c3ff 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -4,7 +4,7 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- archive_prefix = "#{project.path}-#{ref.tr('/', '-')}"
- .project-action-button.dropdown.gl-new-dropdown.inline>
+ .project-action-button.dropdown.gl-dropdown.inline>
%button.gl-button.btn.btn-default.dropdown-toggle.gl-dropdown-toggle.dropdown-icon-only.has-tooltip{ title: s_('DownloadSource|Download'), 'data-toggle' => 'dropdown', 'aria-label' => s_('DownloadSource|Download'), 'data-display' => 'static', data: { qa_selector: 'download_source_code_button' } }
= sprite_icon('download', css_class: 'gl-icon dropdown-icon')
%span.sr-only= _('Select Archive Format')
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 3621853430d..97186149a9d 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -2,17 +2,17 @@
- if current_user
.count-badge.btn-group
- if current_user.already_forked?(@project) && current_user.forkable_namespaces.size < 2
- = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'gl-button btn btn-default btn-sm has-tooltip fork-btn' do
+ = link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: s_('ProjectOverview|Go to your fork'), class: 'gl-button btn btn-default has-tooltip fork-btn' do
= sprite_icon('fork', css_class: 'icon')
%span= s_('ProjectOverview|Fork')
- else
- disabled_tooltip = fork_button_disabled_tooltip(@project)
- - count_class = 'disabled' unless can?(current_user, :download_code, @project)
+ - count_class = 'disabled' unless can?(current_user, :read_code, @project)
- button_class = 'disabled' if disabled_tooltip
%span.btn-group{ class: ('has-tooltip' if disabled_tooltip), title: disabled_tooltip }
- = link_to new_project_fork_path(@project), class: "gl-button btn btn-default btn-sm fork-btn #{button_class}" do
+ = link_to new_project_fork_path(@project), class: "gl-button btn btn-default fork-btn #{button_class}" do
= sprite_icon('fork', css_class: 'icon')
%span= s_('ProjectOverview|Fork')
- = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Forks'), s_('ProjectOverview|Forks'), @project.forks_count), class: "gl-button btn btn-default btn-sm count has-tooltip fork-count #{count_class}" do
+ = link_to project_forks_path(@project), title: n_(s_('ProjectOverview|Forks'), s_('ProjectOverview|Forks'), @project.forks_count), class: "gl-button btn btn-default count has-tooltip fork-count #{count_class}" do
= @project.forks_count
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index eaf906ad89f..d4dcfbdff54 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -3,15 +3,15 @@
- icon = starred ? 'star' : 'star-o'
- button_text = starred ? s_('ProjectOverview|Unstar') : s_('ProjectOverview|Star')
- button_text_classes = starred ? 'starred' : ''
- .count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group
- = render Pajamas::ButtonComponent.new(size: :small, icon: icon, button_text_classes: button_text_classes, button_options: { class: 'star-btn toggle-star', data: { endpoint: toggle_star_project_path(@project, :json) } }) do
+ .count-badge.d-inline-flex.align-item-stretch.btn-group
+ = render Pajamas::ButtonComponent.new(size: :medium, icon: icon, button_text_classes: button_text_classes, button_options: { class: 'star-btn toggle-star', data: { endpoint: toggle_star_project_path(@project, :json) } }) do
- button_text
- = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
+ = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default has-tooltip star-count count' do
= @project.star_count
- else
- .count-badge.d-inline-flex.align-item-stretch.gl-mr-3.btn-group
- = link_to new_user_session_path, class: 'gl-button btn btn-default btn-sm has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
+ .count-badge.d-inline-flex.align-item-stretch.btn-group
+ = link_to new_user_session_path, class: 'gl-button btn btn-default has-tooltip star-btn', title: s_('ProjectOverview|You must sign in to star a project') do
= sprite_icon('star-o', css_class: 'icon')
%span= s_('ProjectOverview|Star')
- = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default btn-sm has-tooltip star-count count' do
+ = link_to project_starrers_path(@project), title: n_(s_('ProjectOverview|Starrer'), s_('ProjectOverview|Starrers'), @project.star_count), class: 'gl-button btn btn-default has-tooltip star-count count' do
= @project.star_count
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 6e202063900..079e24c6389 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -53,9 +53,7 @@
= ci_label_for_status(@last_pipeline.status)
- if @last_pipeline.stages_count.nonzero?
#{ n_(s_('Pipeline|with stage'), s_('Pipeline|with stages'), @last_pipeline.stages_count) }
- .mr-widget-pipeline-graph
- .stage-cell
- .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) } }
+ .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) } }
- if @last_pipeline.duration
in
= time_interval_in_words @last_pipeline.duration
diff --git a/app/views/projects/commit/_signature.html.haml b/app/views/projects/commit/_signature.html.haml
index 978d83bf2b4..c6f1e51049e 100644
--- a/app/views/projects/commit/_signature.html.haml
+++ b/app/views/projects/commit/_signature.html.haml
@@ -1,3 +1,3 @@
- if signature
- - uri = "projects/commit/#{'x509/' if x509_signature?(signature)}"
+ - uri = "projects/commit/#{'x509/' if signature.x509?}"
= render partial: "#{uri}#{signature.verification_status}_signature_badge", locals: { signature: signature }
diff --git a/app/views/projects/commit/_signature_badge.html.haml b/app/views/projects/commit/_signature_badge.html.haml
index fb30bfc2953..ad6b524c01b 100644
--- a/app/views/projects/commit/_signature_badge.html.haml
+++ b/app/views/projects/commit/_signature_badge.html.haml
@@ -17,18 +17,23 @@
- content = capture do
- if show_user
.clearfix
- - uri_signature_badge_user = "projects/commit/#{'x509/' if x509_signature?(signature)}signature_badge_user"
+ - uri_signature_badge_user = "projects/commit/#{'x509/' if signature.x509?}signature_badge_user"
= render partial: "#{uri_signature_badge_user}", locals: { signature: signature }
- - if x509_signature?(signature)
+ - 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: 'gpg-popover-help-link')
+ = 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')
+ - elsif ::Feature.enabled?(:ssh_commit_signatures, signature.project) && 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')
- else
= _('GPG Key ID:')
- %span.monospace= signature.gpg_key_primary_keyid
+ %span.gl-font-monospace= signature.gpg_key_primary_keyid
- = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gpg-popover-help-link gl-display-block')
+ = link_to(_('Learn more about signing commits'), help_page_path('user/project/repository/gpg_signed_commits/index.md'), class: 'gl-link gl-display-block')
%a{ role: 'button', tabindex: 0, class: css_classes, data: { toggle: 'popover', html: 'true', placement: 'top', title: title, content: content } }
= label
diff --git a/app/views/projects/commit/_signature_badge_user.html.haml b/app/views/projects/commit/_signature_badge_user.html.haml
index b20198e76db..656adef6a72 100644
--- a/app/views/projects/commit/_signature_badge_user.html.haml
+++ b/app/views/projects/commit/_signature_badge_user.html.haml
@@ -1,7 +1,4 @@
-- gpg_key = signature.gpg_key
-- user = gpg_key&.user
-- user_name = signature.gpg_key_user_name
-- user_email = signature.gpg_key_user_email
+- user = signature.signed_by_user
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
@@ -11,11 +8,14 @@
%div
%strong= user.name
%div= user.to_reference
-- else
- = mail_to user_email do
- %div
- = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
+- elsif signature.gpg? # SSH signatures do not have an email embedded in them
+ - user_name = signature.gpg_key_user_name
+ - user_email = signature.gpg_key_user_email
+ - if user_name && user_email
+ = mail_to user_email do
+ %div
+ = user_avatar_without_link(user_name: user_name, user_email: user_email, size: 32)
- %div
- %strong= user_name
- %div= user_email
+ %div
+ %strong= user_name
+ %div= user_email
diff --git a/app/views/projects/commit/x509/_signature_badge_user.html.haml b/app/views/projects/commit/x509/_signature_badge_user.html.haml
index f3d39b21ec2..da749172369 100644
--- a/app/views/projects/commit/x509/_signature_badge_user.html.haml
+++ b/app/views/projects/commit/x509/_signature_badge_user.html.haml
@@ -1,5 +1,5 @@
- user_email = signature.x509_certificate.email
-- user = signature.user
+- user = signature.signed_by_user
- if user
= link_to user_path(user), class: 'gpg-popover-user-link' do
diff --git a/app/views/projects/commits/_commits.html.haml b/app/views/projects/commits/_commits.html.haml
index b5ecc9b0193..b79f17ae7b3 100644
--- a/app/views/projects/commits/_commits.html.haml
+++ b/app/views/projects/commits/_commits.html.haml
@@ -15,7 +15,7 @@
%li.commits-row{ data: { day: day } }
%ul.content-list.commit-list.flex-list
- if Feature.enabled?(:cached_commits, project)
- = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ = render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: ->(commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
- else
= render partial: 'projects/commits/commit', collection: daily_commits, locals: { project: project, ref: ref, merge_request: merge_request }
@@ -29,7 +29,7 @@
%li.commits-row
%ul.content-list.commit-list.flex-list
- if Feature.enabled?(:cached_commits, project)
- = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: -> (commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
+ = render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }, cached: ->(commit) { commit_partial_cache_key(commit, ref: ref, merge_request: merge_request, request: request) }
- else
= render partial: 'projects/commits/commit', collection: context_commits, locals: { project: project, ref: ref, merge_request: merge_request }
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index ae68a13929e..c129d978e7e 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,6 +1,7 @@
- breadcrumb_title _("Commits")
- add_page_specific_style 'page_bundles/tree'
- page_title _("Commits"), @ref
+
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, project_commits_path(@project, @ref, rss_url_options), title: "#{@project.name}:#{@ref} commits")
@@ -9,7 +10,7 @@
.nav-block
.tree-ref-container
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'commits'
+ #js-project-commits-ref-switcher{ data: { "project-id" => @project.id, "ref" => @ref, "commits_path": project_commits_path(@project) } }
%ul.breadcrumb.repo-breadcrumb
= commits_breadcrumbs
@@ -24,7 +25,7 @@
= _("Create merge request")
.control
- = form_tag(project_commits_path(@project, @id), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path }) do
+ = form_tag(project_commits_path(@project, @id, ref_type: @ref_type), method: :get, class: 'commits-search-form js-signature-container', data: { 'signatures-path' => namespace_project_signatures_path(ref_type: @ref_type)}) do
= search_field_tag :search, params[:search], { placeholder: _('Search by message'), id: 'commits-search', class: 'form-control gl-form-input input-short gl-mt-3 gl-sm-mt-0 gl-min-w-full', spellcheck: false }
.control.d-none.d-md-block
= link_to project_commits_path(@project, @id, rss_url_options), title: _("Commits feed"), class: 'btn gl-button btn-default btn-icon' do
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 11984a9d6f6..8ff6d348d95 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -34,7 +34,7 @@
- if load_diff_files_async
- url = url_for(safe_params.merge(action: 'diff_files'))
.js-diffs-batch{ data: { diff_files_path: url } }
- = gl_loading_icon( size: "md", css_class: "gl-mt-4" )
+ = gl_loading_icon(size: "md", css_class: "gl-mt-4")
- else
= render partial: 'projects/diffs/file', collection: diff_files, as: :diff_file, locals: { project: diffs.project, environment: environment, diff_page_context: diff_page_context }
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 6d60ef92d86..53b2af88511 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -8,28 +8,31 @@
#environments-detail-view{ data: { details: environments_detail_data_json(current_user, @project, @environment) } }
#environments-detail-view-header
- .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
- = link_to _("Read more"), help_page_path("ci/environments/index.md"), class: "gl-button btn btn-confirm"
- - 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')
+ - 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
+ = link_to _("Read more"), help_page_path("ci/environments/index.md"), class: "gl-button btn btn-confirm"
+ - 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
+ = render @deployments
- = paginate @deployments, theme: 'gitlab'
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index c7639eec75d..a27f076d5dd 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,8 +1,14 @@
- page_title _('Contributors')
+- if Feature.enabled?(:use_ref_type_parameter, @project)
+ - graph_path = project_graph_path(@project, current_ref, ref_type: @ref_type, format: :json)
+ - commits_path = project_commits_path(@project, current_ref, ref_type: @ref_type)
+- else
+ - graph_path = project_graph_path(@project, current_ref, format: :json)
+ - commits_path = project_commits_path(@project, current_ref)
.sub-header-block.gl-bg-gray-10.gl-p-5
.tree-ref-holder.gl-display-inline-block.gl-vertical-align-middle.gl-mr-3>
= render 'shared/ref_switcher', destination: 'graphs'
- = link_to s_('Commits|History'), project_commits_path(@project, current_ref), class: 'btn gl-button btn-default'
+ = link_to s_('Commits|History'), commits_path, class: 'btn gl-button btn-default'
-.js-contributors-graph{ class: container_class, data: { project_graph_path: project_graph_path(@project, current_ref, format: :json), project_branch: current_ref, default_branch: @project.default_branch } }
+.js-contributors-graph{ class: container_class, data: { project_graph_path: graph_path, project_branch: current_ref, default_branch: @project.default_branch } }
diff --git a/app/views/projects/issuable/_show.html.haml b/app/views/projects/issuable/_show.html.haml
index 0898e0ae52d..ec233bc9aff 100644
--- a/app/views/projects/issuable/_show.html.haml
+++ b/app/views/projects/issuable/_show.html.haml
@@ -3,6 +3,7 @@
- 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
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index b730eb5072e..f8f57934303 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,13 +1,14 @@
- page_title _('Issues')
+- add_page_specific_style 'page_bundles/issuable_list'
- add_page_specific_style 'page_bundles/issues_list'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} issues")
-.js-jira-issues-import-status{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
+.js-jira-issues-import-status-root{ data: { can_edit: can?(current_user, :admin_project, @project).to_s,
is_jira_configured: @project.jira_integration.present?.to_s,
issues_path: project_issues_path(@project),
project_path: @project.full_path } }
-.js-issues-list{ data: project_issues_list_data(@project, current_user) }
+.js-issues-list-root{ data: project_issues_list_data(@project, current_user) }
- if can?(current_user, :admin_issue, @project)
= render 'shared/issuable/bulk_update_sidebar', type: :issues
diff --git a/app/views/projects/issues/service_desk.html.haml b/app/views/projects/issues/service_desk.html.haml
index 93cb5ddd7e2..3cc419716e5 100644
--- a/app/views/projects/issues/service_desk.html.haml
+++ b/app/views/projects/issues/service_desk.html.haml
@@ -1,7 +1,7 @@
- @can_bulk_update = false
- page_title _("Service Desk")
-- add_page_specific_style 'page_bundles/issues_list'
+- add_page_specific_style 'page_bundles/issuable_list'
- content_for :breadcrumbs_extra do
= render "projects/issues/service_desk/nav_btns", show_export_button: false, show_rss_button: false
diff --git a/app/views/projects/jobs/_table.html.haml b/app/views/projects/jobs/_table.html.haml
index cd59eae1fb7..954c77a21f3 100644
--- a/app/views/projects/jobs/_table.html.haml
+++ b/app/views/projects/jobs/_table.html.haml
@@ -20,16 +20,16 @@
%table.table.ci-table.builds-page
%thead
%tr
- %th Status
- %th Name
- %th Job
- %th Pipeline
+ %th= _('Status')
+ %th= _('Name')
+ %th= _('Job')
+ %th= _('Pipeline')
- if admin
- %th Project
- %th Runner
- %th Stage
- %th Duration
- %th Coverage
+ %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 }
diff --git a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
index 78fce3f7087..fb950611f81 100644
--- a/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
+++ b/app/views/projects/merge_requests/_close_reopen_draft_report_toggle.html.haml
@@ -1,53 +1,53 @@
- display_issuable_type = issuable_display_type(@merge_request)
-.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-new-dropdown.gl-md-w-auto.gl-w-full
+.btn-group.gl-md-ml-3.gl-display-flex.dropdown.gl-dropdown.gl-md-w-auto.gl-w-full
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret has-tooltip gl-display-none! gl-md-display-inline-flex!", data: { toggle: 'dropdown', title: _('Merge request actions'), testid: 'merge-request-actions', 'aria-label': _('Merge request actions') } do
= sprite_icon "ellipsis_v", size: 16, css_class: "dropdown-icon gl-icon"
= button_tag type: 'button', class: "btn dropdown-toggle btn-default btn-md btn-block gl-button gl-dropdown-toggle gl-md-display-none!", data: { 'toggle' => 'dropdown' } do
- %span.gl-new-dropdown-button-text= _('Merge request actions')
+ %span.gl-dropdown-button-text= _('Merge request actions')
= sprite_icon "chevron-down", size: 16, css_class: "dropdown-icon gl-icon"
.dropdown-menu.dropdown-menu-right
- .gl-new-dropdown-inner
- .gl-new-dropdown-contents
+ .gl-dropdown-inner
+ .gl-dropdown-contents
%ul
- if current_user && moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-item.js-sidebar-subscriptions-widget-root
- %li.gl-new-dropdown-divider
+ %li.gl-dropdown-item.js-sidebar-subscriptions-widget-root
+ %li.gl-dropdown-divider
%hr.dropdown-divider
- if can?(current_user, :update_merge_request, @merge_request)
- %li.gl-new-dropdown-item{ class: "gl-md-display-none!" }
+ %li.gl-dropdown-item{ class: "gl-md-display-none!" }
= link_to edit_project_merge_request_path(@project, @merge_request), class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Edit')
- if @merge_request.open?
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
= link_to toggle_draft_merge_request_path(@merge_request), method: :put, class: 'dropdown-item js-draft-toggle-button' do
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= @merge_request.draft? ? _('Mark as ready') : _('Mark as draft')
- %li.gl-new-dropdown-item.js-close-item
+ %li.gl-dropdown-item.js-close-item
= link_to close_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Close')
= display_issuable_type
- elsif !@merge_request.source_project_missing? && @merge_request.closed?
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
= link_to reopen_issuable_path(@merge_request), method: :put, class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Reopen')
= display_issuable_type
- if moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-item.js-sidebar-lock-root
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item.js-sidebar-lock-root
+ %li.gl-dropdown-item
%button.dropdown-item.js-copy-reference{ type: "button", data: { 'clipboard-text': @merge_request.to_reference(full: true) } }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Copy reference')
- unless current_controller?('conflicts')
- unless issuable_author_is_current_user(@merge_request)
- if moved_mr_sidebar_enabled?
- %li.gl-new-dropdown-divider
+ %li.gl-dropdown-divider
%hr.dropdown-divider
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
= link_to new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request)), class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
- = _('Report abuse')
+ .gl-dropdown-item-text-wrapper
+ = _('Report abuse to administrator')
diff --git a/app/views/projects/merge_requests/_code_dropdown.html.haml b/app/views/projects/merge_requests/_code_dropdown.html.haml
index 5c7fe56095c..2ef89a7bf04 100644
--- a/app/views/projects/merge_requests/_code_dropdown.html.haml
+++ b/app/views/projects/merge_requests/_code_dropdown.html.haml
@@ -1,39 +1,39 @@
-.gl-md-ml-3.dropdown.gl-new-dropdown{ class: "gl-display-none! gl-md-display-flex!" }
+.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
- %span.gl-new-dropdown-button-text= _('Code')
+ %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
- .gl-new-dropdown-inner
- .gl-new-dropdown-contents
+ .gl-dropdown-inner
+ .gl-dropdown-contents
%ul
- %li.gl-new-dropdown-section-header
+ %li.gl-dropdown-section-header
%header.dropdown-header
= _('Review changes')
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
%button.dropdown-item.js-check-out-modal-trigger{ type: 'button' }
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Check out branch')
- if current_user
- %li.gl-new-dropdown-item
+ %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
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Open in Web IDE')
- if Gitlab::CurrentSettings.gitpod_enabled && current_user&.gitpod_enabled
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
= link_to "#{Gitlab::CurrentSettings.gitpod_url}##{merge_request_url(@merge_request)}", target: '_blank', class: 'dropdown-item' do
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Open in Gitpod')
- %li.gl-new-dropdown-divider
+ %li.gl-dropdown-divider
%hr.dropdown-divider
- %li.gl-new-dropdown-section-header
+ %li.gl-dropdown-section-header
%header.dropdown-header
= _('Download')
- %li.gl-new-dropdown-item
+ %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
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Email patches')
- %li.gl-new-dropdown-item
+ %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
- .gl-new-dropdown-item-text-wrapper
+ .gl-dropdown-item-text-wrapper
= _('Plain diff')
diff --git a/app/views/projects/merge_requests/_page.html.haml b/app/views/projects/merge_requests/_page.html.haml
new file mode 100644
index 00000000000..9d79352659c
--- /dev/null
+++ b/app/views/projects/merge_requests/_page.html.haml
@@ -0,0 +1,114 @@
+- @gfm_form = true
+- unless moved_mr_sidebar_enabled?
+ - @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
+- add_to_breadcrumbs _("Merge requests"), project_merge_requests_path(@project)
+- breadcrumb_title @merge_request.to_reference
+- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
+- page_description @merge_request.description_html
+- page_card_attributes @merge_request.card_attributes
+- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions.md')
+- mr_action = j(params[:tab].presence || 'show')
+- add_page_specific_style 'page_bundles/issuable'
+- add_page_specific_style 'page_bundles/design_management'
+- add_page_specific_style 'page_bundles/merge_requests'
+- add_page_specific_style 'page_bundles/pipelines'
+- add_page_specific_style 'page_bundles/reports'
+- add_page_specific_style 'page_bundles/ci_status'
+
+- add_page_startup_api_call @endpoint_metadata_url
+- if mr_action == 'diffs'
+ - add_page_startup_api_call @endpoint_diff_batch_url
+
+.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } }
+ - if moved_mr_sidebar_enabled?
+ #js-merge-sticky-header{ data: { data: sticky_header_data.to_json } }
+ = render "projects/merge_requests/mr_title"
+
+ .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
+ = render "projects/merge_requests/mr_box"
+ .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
+ = 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
+ = tab_link_for @merge_request, :commits do
+ = _("Commits")
+ = gl_badge_tag @commits_count, { size: :sm }
+ - if @project.builds_enabled?
+ = render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do
+ = 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
+ = tab_link_for @merge_request, :diffs do
+ = _("Changes")
+ = gl_badge_tag @diffs_count, { size: :sm }
+ .d-flex.flex-wrap.align-items-center.justify-content-lg-end
+ #js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
+ - if moved_mr_sidebar_enabled?
+ - if !!@issuable_sidebar.dig(:current_user, :id)
+ .js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
+ .gl-ml-auto.gl-align-items-center.gl-display-none.gl-md-display-flex.gl-ml-3.js-expand-sidebar.gl-absolute.gl-right-5{ class: "gl-lg-display-none!" }
+ = render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left',
+ button_options: { class: 'js-sidebar-toggle' }) do
+ = _('Expand')
+ .tab-content#diff-notes-app
+ #js-diff-file-finder
+ #js-code-navigation
+ = render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
+ %div{ class: "#{'merge-request-overview' if moved_mr_sidebar_enabled?}" }
+ %section
+ .issuable-discussion.js-vue-notes-event
+ - if @merge_request.description.present?
+ .detail-page-description.gl-pb-0
+ = render "projects/merge_requests/description"
+ = render "projects/merge_requests/awards_block"
+ = render "projects/merge_requests/widget"
+ - if mr_action === "show"
+ - add_page_startup_api_call Feature.enabled?(:paginated_mr_discussions, @project) ? discussions_path(@merge_request, per_page: 20) : discussions_path(@merge_request)
+ - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json)
+ - add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json)
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
+ endpoint_metadata: @endpoint_metadata_url,
+ noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
+ noteable_type: 'MergeRequest',
+ notes_filters: UserPreference.notes_filters.to_json,
+ notes_filter_value: current_user&.notes_filter_for(@merge_request),
+ target_type: 'merge_request',
+ help_page_path: suggest_changes_help_path,
+ current_user_data: @current_user_data,
+ is_locked: @merge_request.discussion_locked.to_s } }
+ - if moved_mr_sidebar_enabled?
+ = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
+
+ = render "projects/merge_requests/tabs/pane", name: "commits", id: "commits", class: "commits" do
+ -# This tab is always loaded via AJAX
+ = render "projects/merge_requests/tabs/pane", name: "pipelines", id: "pipelines", class: "pipelines" do
+ - if @project.builds_enabled?
+ = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
+ - params = request.query_parameters.merge(diff_head: true)
+ = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: diffs_tab_pane_data(@project, @merge_request, params)
+
+ .mr-loading-status
+ .loading.hide
+ = gl_loading_icon(size: 'lg')
+
+- unless moved_mr_sidebar_enabled?
+ = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
+
+- if @merge_request.can_be_reverted?(current_user)
+ = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit
+- if @merge_request.can_be_cherry_picked?
+ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit
+
+#js-review-bar
+
+- if current_user && Feature.enabled?(:mr_experience_survey, current_user)
+ #js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } }
+
+= render 'projects/invite_members_modal', project: @project
+= render 'shared/web_ide_path'
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 17b1e5a757c..48334023cf0 100644
--- a/app/views/projects/merge_requests/creations/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/creations/_new_compare.html.haml
@@ -1,7 +1,7 @@
%h1.page-title.gl-font-size-h-display
= _('New merge request')
-= form_for [@project, @merge_request], url: project_new_merge_request_path(@project), method: :get, html: { class: "merge-request-form js-requires-input" } do |f|
+= 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) }
@@ -40,17 +40,20 @@
%h2.gl-font-size-h2
= _('Target branch')
.clearfix
- - projects = target_projects(@project)
.merge-request-select.dropdown
- = f.hidden_field :target_project_id
- = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
- .dropdown-menu.dropdown-menu-selectable.dropdown-target-project
- = dropdown_title(_("Select target project"))
- = dropdown_filter(_("Search projects"))
- = dropdown_content do
- = render 'projects/merge_requests/dropdowns/project',
- projects: projects,
- selected: f.object.target_project_id
+ - if Feature.enabled?(:mr_compare_dropdowns, @project)
+ #js-target-project-dropdown{ data: { target_projects_path: project_new_merge_request_json_target_projects_path(@project), current_project: { value: f.object.target_project_id.to_s, text: f.object.target_project.full_path }.to_json } }
+ - else
+ - projects = target_projects(@project)
+ = f.hidden_field :target_project_id
+ = dropdown_toggle f.object.target_project.full_path, { toggle: "dropdown", 'field-name': "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted?, default_text: _("Select target project") }, { toggle_class: "js-compare-dropdown js-target-project" }
+ .dropdown-menu.dropdown-menu-selectable.dropdown-target-project
+ = dropdown_title(_("Select target project"))
+ = dropdown_filter(_("Search projects"))
+ = dropdown_content do
+ = render 'projects/merge_requests/dropdowns/project',
+ projects: projects,
+ selected: f.object.target_project_id
.merge-request-select.dropdown
= f.hidden_field :target_branch
= dropdown_toggle f.object.target_branch.presence || _("Select target branch"), { toggle: "dropdown", 'field-name': "#{f.object_name}[target_branch]", 'refs-url': refs_project_path(f.object.target_project), selected: f.object.target_branch, default_text: _("Select target branch") }, { toggle_class: "js-compare-dropdown js-target-branch monospace" }
@@ -68,4 +71,4 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- = f.submit _('Compare branches and continue'), class: "gl-button btn btn-confirm mr-compare-btn gl-mt-4", data: { qa_selector: "compare_branches_button" }
+ = f.submit _('Compare branches and continue'), data: { qa_selector: 'compare_branches_button' }, pajamas_button: true
diff --git a/app/views/projects/merge_requests/diffs.html.haml b/app/views/projects/merge_requests/diffs.html.haml
new file mode 100644
index 00000000000..1ef212ee5ce
--- /dev/null
+++ b/app/views/projects/merge_requests/diffs.html.haml
@@ -0,0 +1 @@
+= render 'page'
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index a3f40207d20..79da09c5205 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -5,6 +5,7 @@
- page_title _("Merge requests")
- new_merge_request_email = @project.new_issuable_address(current_user, 'merge_request')
+- add_page_specific_style 'page_bundles/issuable_list'
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@project.name} merge requests")
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 203724fc1f1..1ef212ee5ce 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,113 +1 @@
-- @gfm_form = true
-- unless moved_mr_sidebar_enabled?
- - @content_class = "merge-request-container#{' limit-container-width' unless fluid_layout}"
-- add_to_breadcrumbs _("Merge requests"), project_merge_requests_path(@project)
-- breadcrumb_title @merge_request.to_reference
-- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", _("Merge requests")
-- page_description @merge_request.description_html
-- page_card_attributes @merge_request.card_attributes
-- suggest_changes_help_path = help_page_path('user/project/merge_requests/reviews/suggestions.md')
-- mr_action = j(params[:tab].presence || 'show')
-- add_page_specific_style 'page_bundles/design_management'
-- add_page_specific_style 'page_bundles/merge_requests'
-- add_page_specific_style 'page_bundles/pipelines'
-- add_page_specific_style 'page_bundles/reports'
-- add_page_specific_style 'page_bundles/ci_status'
-
-- add_page_startup_api_call @endpoint_metadata_url
-- if mr_action == 'diffs'
- - add_page_startup_api_call @endpoint_diff_batch_url
-
-.merge-request{ data: { mr_action: mr_action, url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project), lock_version: @merge_request.lock_version } }
- - if moved_mr_sidebar_enabled?
- #js-merge-sticky-header{ data: { data: sticky_header_data.to_json } }
- = render "projects/merge_requests/mr_title"
-
- .merge-request-details.issuable-details{ data: { id: @merge_request.project.id } }
- = render "projects/merge_requests/mr_box"
- .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
- = tab_link_for @merge_request, :show, force_link: @commit.present? do
- = _("Overview")
- = gl_badge_tag @merge_request.related_notes.user.count, { size: :sm }
- - if @merge_request.source_project
- = render "projects/merge_requests/tabs/tab", name: "commits", class: "commits-tab", qa_selector: "commits_tab" do
- = tab_link_for @merge_request, :commits do
- = _("Commits")
- = gl_badge_tag @commits_count, { size: :sm }
- - if @project.builds_enabled?
- = render "projects/merge_requests/tabs/tab", name: "pipelines", class: "pipelines-tab" do
- = 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
- = tab_link_for @merge_request, :diffs do
- = _("Changes")
- = gl_badge_tag @diffs_count, { size: :sm }
- .d-flex.flex-wrap.align-items-center.justify-content-lg-end
- #js-vue-discussion-counter{ data: { blocks_merge: @project.only_allow_merge_if_all_discussions_are_resolved?.to_s } }
- - if moved_mr_sidebar_enabled?
- - if !!@issuable_sidebar.dig(:current_user, :id)
- .js-sidebar-todo-widget-root{ data: { project_path: @issuable_sidebar[:project_full_path], iid: @issuable_sidebar[:iid], id: @issuable_sidebar[:id] } }
- .gl-ml-auto.gl-align-items-center.gl-display-none.gl-md-display-flex.gl-ml-3.js-expand-sidebar{ class: "gl-lg-display-none!" }
- = render Pajamas::ButtonComponent.new(icon: 'chevron-double-lg-left',
- button_options: { class: 'js-sidebar-toggle' }) do
- = _('Expand')
- .tab-content#diff-notes-app
- #js-diff-file-finder
- #js-code-navigation
- = render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
- %div{ class: "#{'merge-request-overview' if moved_mr_sidebar_enabled?}" }
- %section
- .issuable-discussion.js-vue-notes-event
- - if @merge_request.description.present?
- .detail-page-description.gl-pb-0
- = render "projects/merge_requests/description"
- = render "projects/merge_requests/awards_block"
- = render "projects/merge_requests/widget"
- - if mr_action === "show"
- - add_page_startup_api_call Feature.enabled?(:paginated_mr_discussions, @project) ? discussions_path(@merge_request, per_page: 20) : discussions_path(@merge_request)
- - add_page_startup_api_call widget_project_json_merge_request_path(@project, @merge_request, format: :json)
- - add_page_startup_api_call cached_widget_project_json_merge_request_path(@project, @merge_request, format: :json)
- #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request).to_json,
- endpoint_metadata: @endpoint_metadata_url,
- noteable_data: serialize_issuable(@merge_request, serializer: 'noteable'),
- noteable_type: 'MergeRequest',
- notes_filters: UserPreference.notes_filters.to_json,
- notes_filter_value: current_user&.notes_filter_for(@merge_request),
- target_type: 'merge_request',
- help_page_path: suggest_changes_help_path,
- current_user_data: @current_user_data,
- is_locked: @merge_request.discussion_locked.to_s } }
- - if moved_mr_sidebar_enabled?
- = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
-
- = render "projects/merge_requests/tabs/pane", name: "commits", id: "commits", class: "commits" do
- -# This tab is always loaded via AJAX
- = render "projects/merge_requests/tabs/pane", name: "pipelines", id: "pipelines", class: "pipelines" do
- - if @project.builds_enabled?
- = render 'projects/commit/pipelines_list', disable_initialization: true, endpoint: pipelines_project_merge_request_path(@project, @merge_request)
- - params = request.query_parameters.merge(diff_head: true)
- = render "projects/merge_requests/tabs/pane", name: "diffs", id: "js-diffs-app", class: "diffs", data: diffs_tab_pane_data(@project, @merge_request, params)
-
- .mr-loading-status
- .loading.hide
- = gl_loading_icon(size: 'lg')
-
-- unless moved_mr_sidebar_enabled?
- = render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
-
-- if @merge_request.can_be_reverted?(current_user)
- = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit
-- if @merge_request.can_be_cherry_picked?
- = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit
-
-#js-review-bar
-
-- if current_user && Feature.enabled?(:mr_experience_survey, current_user)
- #js-mr-experience-survey{ data: { account_age: current_user.account_age_in_days } }
-
-= render 'projects/invite_members_modal', project: @project
-= render 'shared/web_ide_path'
+= render 'page'
diff --git a/app/views/projects/ml/candidates/show.html.haml b/app/views/projects/ml/candidates/show.html.haml
new file mode 100644
index 00000000000..7fa98f69edf
--- /dev/null
+++ b/app/views/projects/ml/candidates/show.html.haml
@@ -0,0 +1,7 @@
+- experiment = @candidate.experiment
+- add_to_breadcrumbs _("Experiments"), project_ml_experiments_path(@project)
+- add_to_breadcrumbs experiment.name, project_ml_experiment_path(@project, experiment.iid)
+- breadcrumb_title "Candidate #{@candidate.iid}"
+- data = candidate_as_data(@candidate)
+
+#js-show-ml-candidate{ data: { candidate: data } }
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 2a3171e9fd8..70bb97b7625 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,10 +1,11 @@
- breadcrumb_title _("Graph")
- page_title _("Graph"), @ref
+- network_path = Feature.enabled?(:use_ref_type_parameter) ? project_network_path(@project, @id, ref_type: @ref_type) : project_network_path(@project, @id)
= render "head"
.gl-mt-5
.project-network.gl-border-1.gl-border-solid.gl-border-gray-300
.controls.gl-bg-gray-50.gl-p-2.gl-font-base.gl-text-gray-400.gl-border-b-1.gl-border-b-solid.gl-border-b-gray-300
- = form_tag project_network_path(@project, @id), method: :get, class: 'form-inline network-form' do |f|
+ = form_tag network_path, method: :get, class: 'form-inline network-form' do |f|
= text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: _("Git revision"), class: 'search-input form-control gl-form-input input-mx-250 search-sha gl-mr-2'
= render Pajamas::ButtonComponent.new(type: :submit, variant: :confirm, icon: 'search')
.inline.gl-ml-5
diff --git a/app/views/projects/notes/_more_actions_dropdown.html.haml b/app/views/projects/notes/_more_actions_dropdown.html.haml
index 5f70e25f802..2351bd209a7 100644
--- a/app/views/projects/notes/_more_actions_dropdown.html.haml
+++ b/app/views/projects/notes/_more_actions_dropdown.html.haml
@@ -10,7 +10,7 @@
- unless is_current_user
%li
= link_to new_abuse_report_path(user_id: note.author.id, ref_url: noteable_note_url(note)) do
- = _('Report abuse to admin')
+ = _('Report abuse to administrator')
- if note_editable
%li
= link_to note_url(note), method: :delete, data: { confirm: _('Are you sure you want to delete this comment?'), confirm_btn_variant: 'danger', qa_selector: 'delete_comment_button' }, aria: { label: _('Delete comment') }, remote: true, class: 'js-note-delete' do
diff --git a/app/views/projects/pages/_list.html.haml b/app/views/projects/pages/_list.html.haml
index 16312da1353..32e67fdadb8 100644
--- a/app/views/projects/pages/_list.html.haml
+++ b/app/views/projects/pages/_list.html.haml
@@ -12,7 +12,7 @@
- if verification_enabled
- tooltip, status = domain.unverified? ? [s_('GitLabPages|Unverified'), 'failed'] : [s_('GitLabPages|Verified'), 'success']
.domain-status.ci-status-icon.has-tooltip{ class: "gl-mr-5 ci-status-icon-#{status}", title: tooltip }
- = sprite_icon("status_#{status}" )
+ = sprite_icon("status_#{status}")
.domain-name
= external_link(domain.url, domain.url)
- if domain.certificate
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index 6de8117df6b..c88255e23f9 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -4,9 +4,8 @@
= _("New Pages Domain")
= render 'projects/pages_domains/helper_text'
%div
- = form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
+ = gitlab_ui_form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
- .form-actions
- = f.submit _('Create New Domain'), class: "gl-button btn btn-confirm"
- .float-right
- = link_to _('Cancel'), project_pages_path(@project), class: 'gl-button btn btn-default btn-cancel'
+ .form-actions.gl-display-flex
+ = f.submit _('Create New Domain'), class: 'gl-mr-3', pajamas_button: true
+ = link_to _('Cancel'), project_pages_path(@project), class: 'gl-button btn btn-default btn-cancel'
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 0edf75c9abc..5de5188ae6a 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -15,8 +15,8 @@
= _('Pages Domain')
= render 'projects/pages_domains/helper_text'
%div
- = form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
+ = gitlab_ui_form_for [@project, domain_presenter], html: { class: 'fieldset-form' } do |f|
= render 'form', { f: f }
- .form-actions.d-flex.justify-content-between
- = f.submit _('Save Changes'), class: "gl-button btn btn-confirm"
+ .form-actions.gl-display-flex
+ = f.submit _('Save Changes'), class: 'gl-mr-3', pajamas_button: true
= link_to _('Cancel'), project_pages_path(@project), class: 'gl-button btn btn-default btn-inverse'
diff --git a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
index 7b16564dfa2..0de31f59033 100644
--- a/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
+++ b/app/views/projects/pipeline_schedules/_pipeline_schedule.html.haml
@@ -1,33 +1,38 @@
- if pipeline_schedule
%tr.pipeline-schedule-table-row
- %td
- = pipeline_schedule.description
- %td.branch-name-cell.gl-text-truncate
- - 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
- - 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 ##{pipeline_schedule.last_pipeline.id}
- - else
- = s_("PipelineSchedules|None")
- %td.gl-text-gray-500{ 'data-testid': 'next-run-cell' }
- - if pipeline_schedule.active? && pipeline_schedule.next_run_at
- = time_ago_with_tooltip(pipeline_schedule.real_next_run)
- - else
- = s_("PipelineSchedules|Inactive")
- %td
- - 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
+ %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_to play_pipeline_schedule_path(pipeline_schedule), method: :post, title: _('Play'), class: 'btn gl-button btn-default btn-icon' do
diff --git a/app/views/projects/pipeline_schedules/_table.html.haml b/app/views/projects/pipeline_schedules/_table.html.haml
index d0c7ea77263..2f96ac6a534 100644
--- a/app/views/projects/pipeline_schedules/_table.html.haml
+++ b/app/views/projects/pipeline_schedules/_table.html.haml
@@ -1,12 +1,12 @@
.table-holder
- %table.table.ci-table
- %thead
- %tr
- %th= _("Description")
- %th= s_("PipelineSchedules|Target")
- %th= _("Last Pipeline")
- %th= s_("PipelineSchedules|Next Run")
- %th= _("Owner")
- %th
-
+ %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/index.html.haml b/app/views/projects/pipeline_schedules/index.html.haml
index 47ad8cc826d..cb7cd631859 100644
--- a/app/views/projects/pipeline_schedules/index.html.haml
+++ b/app/views/projects/pipeline_schedules/index.html.haml
@@ -21,8 +21,7 @@
%ul.content-list
= render partial: "table"
- else
- = render Pajamas::CardComponent.new(card_options: { class: 'bg-light gl-mt-3 gl-text-center' }) do |c|
- - c.body do
- = _("No schedules")
+ .nothing-here-block
+ = _("No schedules")
#pipeline-take-ownership-modal
diff --git a/app/views/projects/pipeline_schedules/new.html.haml b/app/views/projects/pipeline_schedules/new.html.haml
index d3757d0e339..2d4ed5a9872 100644
--- a/app/views/projects/pipeline_schedules/new.html.haml
+++ b/app/views/projects/pipeline_schedules/new.html.haml
@@ -9,6 +9,6 @@
= _("Schedule a new pipeline")
- if Feature.enabled?(:pipeline_schedules_vue, @project)
- #pipeline-schedules-form-new{ data: { full_path: @project.full_path } }
+ #pipeline-schedules-form-new{ data: { full_path: @project.full_path, cron: @schedule.cron, daily_limit: @schedule.daily_limit, timezone_data: timezone_data.to_json, cron_timezone: @schedule.cron_timezone, project_id: @project.id, default_branch: @project.default_branch, settings_link: project_settings_ci_cd_path(@project), } }
- else
= render "form"
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 30cc7f94311..1a079324a0f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,7 @@
- if Feature.enabled?(:pipeline_name, @pipeline.project) && @pipeline.name
- %h3
- = @pipeline.name
+ .gl-border-t.gl-p-5.gl-px-0
+ %h3.gl-m-0.gl-text-body
+ = @pipeline.name
- else
.commit-box
%h3.commit-title
@@ -45,7 +46,7 @@
- popover_content_text = _('Learn more about Auto DevOps')
= gl_badge_tag s_('Pipelines|Auto DevOps'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-autodevops', href: "#", tabindex: "0", role: "button", data: { container: 'body', toggle: 'popover', placement: 'top', html: 'true', triggers: 'focus', title: "<div class='gl-font-weight-normal gl-line-height-normal'>#{popover_title_text}</div>", content: "<a href='#{popover_content_url}' target='_blank' rel='noopener noreferrer nofollow'>#{popover_content_text}</a>" } }
- if @pipeline.detached_merge_request_pipeline?
- = gl_badge_tag s_('Pipelines|merge request'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', title: s_("Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.") }
+ = gl_badge_tag s_('Pipelines|merge request'), { variant: :info, size: :sm }, { class: 'js-pipeline-url-mergerequest has-tooltip', data: { qa_selector: 'merge_request_badge_tag' }, title: s_("Pipelines|This pipeline ran on the contents of this merge request's source branch, not the target branch.") }
- if @pipeline.stuck?
= gl_badge_tag s_('Pipelines|stuck'), { variant: :warning, size: :sm }, { class: 'js-pipeline-url-stuck has-tooltip' }
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
deleted file mode 100644
index e83547fd8f8..00000000000
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ /dev/null
@@ -1,48 +0,0 @@
-- return if pipeline_has_errors
-
-.tabs-holder
- %ul.pipelines-tabs.nav-links.no-top.no-bottom.mobile-separator.nav.nav-tabs
- %li.js-pipeline-tab-link
- = link_to project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-pipeline', action: 'pipelines', toggle: 'tab' }, class: 'pipeline-tab' do
- = _('Pipeline')
- %li.js-dag-tab-link
- = link_to dag_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-dag', action: 'dag', toggle: 'tab' }, class: 'dag-tab' do
- = _('Needs')
- %li.js-builds-tab-link
- = link_to builds_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-builds', action: 'builds', toggle: 'tab' }, class: 'builds-tab' do
- = _('Jobs')
- = gl_badge_tag @pipeline.total_size, { size: :sm }, { class: 'js-builds-counter' }
- - if @pipeline.failed_builds.present?
- %li.js-failures-tab-link
- = link_to failures_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-failures', action: 'failures', toggle: 'tab' }, class: 'failures-tab' do
- = _('Failed Jobs')
- = gl_badge_tag @pipeline.failed_builds.count, { size: :sm }, { class: 'js-failures-counter' }
- %li.js-tests-tab-link
- = link_to test_report_project_pipeline_path(@project, @pipeline), data: { target: '#js-tab-tests', action: 'test_report', toggle: 'tab' }, class: 'test-tab' do
- = s_('TestReports|Tests')
- = gl_badge_tag @pipeline.test_report_summary.total[:count], { size: :sm }, { class: 'js-test-report-badge-counter' }
- = render_if_exists "projects/pipelines/tabs_holder", pipeline: @pipeline, project: @project
-
-.tab-content
- #js-tab-pipeline.tab-pane.gl-w-full
- #js-pipeline-graph-vue
-
- #js-tab-builds.tab-pane
- - if stages.present?
- #js-pipeline-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid } }
-
- - if @pipeline.failed_builds.present?
- #js-tab-failures.tab-pane
- #js-pipeline-failed-jobs-vue{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, failed_jobs_summary_data: prepare_failed_jobs_summary_data(@pipeline.failed_builds) } }
-
- #js-tab-dag.tab-pane
- #js-pipeline-dag-vue{ data: { pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, empty_svg_path: image_path('illustrations/empty-state/empty-dag-md.svg'), about_dag_doc_path: help_page_path('ci/directed_acyclic_graph/index.md'), dag_doc_path: help_page_path('ci/yaml/index.md', anchor: 'needs')} }
-
- #js-tab-tests.tab-pane
- #js-pipeline-tests-detail{ data: { summary_endpoint: summary_project_pipeline_tests_path(@project, @pipeline, format: :json),
- 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.complete_and_has_reports?(Ci::JobArtifact.of_report_type(:test)).to_s,
- empty_state_image_path: image_path('illustrations/empty-state/empty-test-cases-lg.svg'),
- artifacts_expired_image_path: image_path('illustrations/pipeline.svg') } }
- = render_if_exists "projects/pipelines/tabs_content", pipeline: @pipeline, project: @project
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 4531bb2d0a9..9b0a81a2f60 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -9,7 +9,7 @@
- add_page_startup_graphql_call('pipelines/get_pipeline_details', { projectPath: @project.full_path, iid: @pipeline.iid })
.js-pipeline-container{ data: { controller_action: "#{controller.action_name}" } }
- #js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
+ #js-pipeline-header-vue.pipeline-header-container{ data: { full_path: @project.full_path, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline), pipeline_iid: @pipeline.iid, pipeline_id: @pipeline.id, pipelines_path: project_pipelines_path(@project) } }
= render_if_exists 'projects/pipelines/cc_validation_required_alert', pipeline: @pipeline
@@ -18,16 +18,10 @@
- if pipeline_has_errors
.bs-callout.bs-callout-danger
- %h4= _('Found errors in your %{gitlab_ci_yml}:') % { gitlab_ci_yml: '.gitlab-ci.yml' }
+ %h4= _('Unable to create pipeline')
%ul
- @pipeline.yaml_errors.split("\n").each do |error|
%li= error
- - lint_link_url = project_ci_pipeline_editor_path(@project, tab: "LINT_TAB")
- - lint_link_start = '<a href="%{url}" class="gl-text-blue-500!">'.html_safe % { url: lint_link_url }
- = s_('You can also test your %{gitlab_ci_yml} in %{lint_link_start}CI Lint%{lint_link_end}').html_safe % { gitlab_ci_yml: '.gitlab-ci.yml', lint_link_start: lint_link_start, lint_link_end: '</a>'.html_safe }
- - if Feature.enabled?(:pipeline_tabs_vue, @project)
- #js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline, @current_user) }
- else
- = render "projects/pipelines/with_tabs", pipeline: @pipeline, stages: @stages, pipeline_has_errors: pipeline_has_errors
-.js-pipeline-details-vue{ data: { metrics_path: namespace_project_ci_prometheus_metrics_histograms_path(namespace_id: @project.namespace, project_id: @project, format: :json), pipeline_project_path: @project.full_path, pipeline_iid: @pipeline.iid, graphql_resource_etag: graphql_etag_pipeline_path(@pipeline), pipeline_path: pipeline_path(@pipeline) } }
+ #js-pipeline-tabs{ data: js_pipeline_tabs_data(@project, @pipeline, @current_user) }
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index c7818602f52..4ac0e28d386 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -15,17 +15,17 @@
- invite_group_top_margin = ''
- if can_admin_project_member?(@project)
.js-import-project-members-trigger{ data: { classes: 'gl-md-w-auto gl-w-full' } }
- .js-import-project-members-modal{ data: { project_id: @project.id, project_name: @project.name } }
+ .js-import-project-members-modal{ data: { project_id: @project.id, project_name: @project.name, reload_page_on_submit: true.to_s } }
- invite_group_top_margin = 'gl-md-mt-0 gl-mt-3'
- if @project.allowed_to_share_with_group?
.js-invite-group-trigger{ data: { classes: "gl-md-w-auto gl-w-full gl-md-ml-3 #{invite_group_top_margin}", display_text: _('Invite a group') } }
- = render 'projects/invite_groups_modal', project: @project
+ = render 'projects/invite_groups_modal', project: @project, reload_page_on_submit: true
- 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',
display_text: _('Invite members') } }
- = render 'projects/invite_members_modal', project: @project
+ = render 'projects/invite_members_modal', project: @project, reload_page_on_submit: true
- else
- if project_can_be_shared?
%h4
diff --git a/app/views/projects/protected_branches/_branches_list.html.haml b/app/views/projects/protected_branches/_branches_list.html.haml
deleted file mode 100644
index 24d2b971472..00000000000
--- a/app/views/projects/protected_branches/_branches_list.html.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-- can_admin_project = can?(current_user, :admin_project, @project)
-
-= render layout: 'projects/protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
- = render partial: 'projects/protected_branches/protected_branch', collection: @protected_branches
diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml
deleted file mode 100644
index 2b0a502fe4d..00000000000
--- a/app/views/projects/protected_branches/_index.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-- content_for :create_protected_branch do
- = render 'projects/protected_branches/create_protected_branch'
-
-- content_for :branches_list do
- = render "projects/protected_branches/branches_list"
-
-= render 'projects/protected_branches/shared/index'
diff --git a/app/views/projects/protected_branches/_protected_branch.html.haml b/app/views/projects/protected_branches/_protected_branch.html.haml
deleted file mode 100644
index 366d7a7a2eb..00000000000
--- a/app/views/projects/protected_branches/_protected_branch.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-= render layout: 'projects/protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do
- = render_if_exists 'projects/protected_branches/update_protected_branch', protected_branch: protected_branch
diff --git a/app/views/projects/protected_branches/_update_protected_branch.html.haml b/app/views/projects/protected_branches/_update_protected_branch.html.haml
deleted file mode 100644
index b2ec98be056..00000000000
--- a/app/views/projects/protected_branches/_update_protected_branch.html.haml
+++ /dev/null
@@ -1 +0,0 @@
-= render 'shared/projects/protected_branches/update_protected_branch', protected_branch: protected_branch
diff --git a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
index 9ea7f397c0a..1db1da5e428 100644
--- a/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
+++ b/app/views/projects/protected_tags/shared/_create_protected_tag.html.haml
@@ -1,4 +1,4 @@
-= form_for [@project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
+= gitlab_ui_form_for [@project, @protected_tag], html: { class: 'new-protected-tag js-new-protected-tag' } do |f|
%input{ type: 'hidden', name: 'update_section', value: 'js-protected-tags-settings' }
= render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5' }) do |c|
- c.header do
@@ -20,4 +20,4 @@
= yield :create_access_levels
- c.footer do
- = f.submit _('Protect'), class: 'gl-button btn btn-confirm', disabled: true, data: { qa_selector: 'protect_tag_button' }
+ = f.submit _('Protect'), pajamas_button: true, disabled: true, data: { qa_selector: 'protect_tag_button' }
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 51f0b6319a1..910aab6da72 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,6 +1,6 @@
- page_title _("Container Registry")
- @content_class = "limit-container-width" unless fluid_layout
-- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil} )
+- add_page_startup_graphql_call('container_registry/get_container_repositories', { fullPath: @project.full_path, first: 10, name: nil, isGroupPage: false, sort: nil})
%section
#js-container-registry{ data: { endpoint: project_container_registry_index_path(@project),
@@ -15,7 +15,6 @@
"expiration_policy_help_page_path" => help_page_path('user/packages/container_registry/reduce_container_registry_storage', anchor: 'cleanup-policy'),
"garbage_collection_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'container-registry-garbage-collection'),
"run_cleanup_policies_help_page_path" => help_page_path('administration/packages/container_registry', anchor: 'run-the-cleanup-policy-now'),
- "container_registry_importing_help_page_path" => help_page_path('user/packages/container_registry/index', anchor: 'tags-temporarily-cannot-be-marked-for-deletion'),
"project_path": @project.full_path,
"gid_prefix": container_repository_gid_prefix,
"is_admin": current_user&.admin.to_s,
diff --git a/app/views/projects/runners/_group_runners.html.haml b/app/views/projects/runners/_group_runners.html.haml
index 5acd6f95df4..d71bcd12e64 100644
--- a/app/views/projects/runners/_group_runners.html.haml
+++ b/app/views/projects/runners/_group_runners.html.haml
@@ -35,7 +35,9 @@
= _('Ask your group owner to set up a group runner.')
- else
- %h4.underlined-title
- = _('Available group runners: %{runners}').html_safe % { runners: @group_runners.count }
- %ul.bordered-list
- = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
+ %div{ data: { testid: 'group-runners' } }
+ %h5.gl-mt-6.gl-mb-0
+ = _('Available group runners: %{runners}') % { runners: @group_runners_count }
+ %ul.bordered-list
+ = render partial: 'projects/runners/runner', collection: @group_runners, as: :runner
+ = paginate @group_runners, theme: "gitlab", param_name: "group_runners_page", params: { expand_runners: true, anchor: 'js-runners-settings' }
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 18803bdd8f3..e517b37aae9 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -7,7 +7,7 @@
- else
%span
= "##{runner.id} (#{runner.short_sha})"
- - if runner.locked?
+ - if runner.locked? && runner.project_type?
%span.has-tooltip{ title: s_('Runners|Runner is locked and available for currently assigned projects only. Only administrators can change the assigned projects.') }
= sprite_icon('lock')
.gl-ml-2
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 4689e70d907..9e7bbd6cefe 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -5,6 +5,9 @@
- if @shared_runners_count == 0
= _('This GitLab instance does not provide any shared runners yet. Instance administrators can register shared runners in the admin area.')
- else
- %h5.gl-mt-6.gl-mb-0 #{_('Available shared runners:')} #{@shared_runners_count}
- %ul.bordered-list.available-shared-runners
- = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
+ %div{ data: { testid: 'available-shared-runners' } }
+ %h5.gl-mt-6.gl-mb-0
+ = s_('Runners|Available shared runners: %{count}') % {count: @shared_runners_count}
+ %ul.bordered-list
+ = render partial: 'projects/runners/runner', collection: @shared_runners, as: :runner
+ = paginate @shared_runners, theme: "gitlab", param_name: "shared_runners_page", params: { expand_runners: true, anchor: 'js-runners-settings' }
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 3634bacb6ec..f3a7037bdab 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -17,7 +17,7 @@
group_path: '' }
- else
= _('Please contact an admin to register runners.')
- = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'prevent-users-from-registering-runners'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'restrict-runner-registration-by-all-users-in-an-instance'), target: '_blank', rel: 'noopener noreferrer'
%hr
diff --git a/app/views/projects/settings/_general.html.haml b/app/views/projects/settings/_general.html.haml
index 3a62c6f41cc..5f1dee39e25 100644
--- a/app/views/projects/settings/_general.html.haml
+++ b/app/views/projects/settings/_general.html.haml
@@ -1,5 +1,5 @@
- hidden_topics_field_id = 'project_topic_list_field'
-= form_for [@project], html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
+= gitlab_ui_form_for [@project], html: { multipart: true, class: "edit-project js-general-settings-form" }, authenticity_token: true do |f|
%input{ name: 'update_section', type: 'hidden', value: 'js-general-settings' }
%fieldset
@@ -39,4 +39,4 @@
%hr
= link_to _('Remove avatar'), project_avatar_path(@project), aria: { label: _('Remove avatar') }, data: { confirm: _('Avatar will be removed. Are you sure?'), 'confirm-btn-variant': 'danger' }, method: :delete, class: 'gl-button btn btn-danger-secondary'
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm gl-mt-6", data: { qa_selector: 'save_naming_topics_avatar_button' }
+ = f.submit _('Save changes'), pajamas_button: true, class: "gl-mt-6", data: { qa_selector: 'save_naming_topics_avatar_button' }
diff --git a/app/views/projects/settings/branch_rules/index.html.haml b/app/views/projects/settings/branch_rules/index.html.haml
index 571a992a552..80a41bb579b 100644
--- a/app/views/projects/settings/branch_rules/index.html.haml
+++ b/app/views/projects/settings/branch_rules/index.html.haml
@@ -3,4 +3,4 @@
%h3.gl-mb-5= s_('BranchRules|Branch rules details')
-#js-branch-rules{ data: { project_path: @project.full_path, protected_branches_path: project_settings_repository_path(@project, anchor: 'js-protected-branches-settings'), approval_rules_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-approval-settings'), status_checks_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-settings') } }
+#js-branch-rules{ data: { project_path: @project.full_path, protected_branches_path: project_settings_repository_path(@project, anchor: 'js-protected-branches-settings'), approval_rules_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-approval-settings'), status_checks_path: project_settings_merge_requests_path(@project, anchor: 'js-merge-request-settings'), branches_path: project_branches_path(@project) } }
diff --git a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
index 5748b4b0330..86238a41f0b 100644
--- a/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_autodevops_form.html.haml
@@ -10,8 +10,8 @@
- base_domain_link_start = link_start % { url: base_domain_path }
- help_link_continouos = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/stages.md', anchor: 'auto-deploy'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
-- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/customize.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_timed = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'timed-incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
+- help_link_incremental = link_to sprite_icon('question-o'), help_page_path('topics/autodevops/cicd_variables.md', anchor: 'incremental-rollout-to-production'), target: '_blank', rel: 'noopener noreferrer'
.row
.col-lg-12
diff --git a/app/views/projects/settings/operations/_alert_management.html.haml b/app/views/projects/settings/operations/_alert_management.html.haml
index d80f1e4597c..7433e81c11c 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) }
+%section.settings.no-animate#js-alert-management-settings{ class: ('expanded' if expanded), data: { qa_selector: 'alerts_settings_content' } }
.settings-header
%h4.settings-title.js-settings-toggle.js-settings-toggle-trigger-only
= _('Alerts')
diff --git a/app/views/projects/settings/repository/_protected_branches.html.haml b/app/views/projects/settings/repository/_protected_branches.html.haml
index 31630828571..d2356b5df09 100644
--- a/app/views/projects/settings/repository/_protected_branches.html.haml
+++ b/app/views/projects/settings/repository/_protected_branches.html.haml
@@ -1,2 +1,2 @@
-= render "projects/protected_branches/index"
+= render "protected_branches/index"
= render "projects/protected_tags/index"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 77c44b792ab..5fa70c3af32 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -15,7 +15,7 @@
= render "home_panel"
-- if can?(current_user, :download_code, @project) && @project.repository_languages.present?
+- if can?(current_user, :read_code, @project) && @project.repository_languages.present?
- add_page_startup_graphql_call('repository/path_last_commit', { projectPath: @project.full_path, ref: current_ref, path: current_route_path || "" })
= repository_languages_bar(@project.repository_languages)
@@ -25,8 +25,5 @@
- view_path = @project.default_view
-- if show_auto_devops_callout?(@project)
- = render 'shared/auto_devops_callout'
-
%div{ class: project_child_container_class(view_path) }
= render view_path, is_project_overview: true
diff --git a/app/views/projects/starrers/index.html.haml b/app/views/projects/starrers/index.html.haml
index fe8a6508dd7..23578652862 100644
--- a/app/views/projects/starrers/index.html.haml
+++ b/app/views/projects/starrers/index.html.haml
@@ -1,4 +1,5 @@
- page_title _("Starrers")
+- add_page_specific_style 'page_bundles/users'
.top-area.adjust
.nav-text
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index ed06c90efa8..2f8291d255f 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -2,7 +2,7 @@
- default_ref = params[:ref] || @project.default_branch
- if @error
- = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true ) do |c|
+ = render Pajamas::AlertComponent.new(variant: :danger, dismissible: true) do |c|
= c.body do
= @error
@@ -20,14 +20,9 @@
= label_tag :tag_name, nil
= text_field_tag :tag_name, params[:tag_name], required: true, autofocus: true, class: 'form-control', data: { qa_selector: "tag_name_field" }
.form-group.row
- .col-sm-12.create-from
+ .col-sm-auto.create-from
= label_tag :ref, 'Create from'
- .dropdown
- = hidden_field_tag :ref, default_ref
- = button_tag type: 'button', title: default_ref, class: 'dropdown-menu-toggle wide js-branch-select monospace', required: true, data: { toggle: 'dropdown', selected: default_ref, field_name: 'ref' } do
- .text-left.dropdown-toggle-text= default_ref
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- = render 'shared/ref_dropdown', dropdown_class: 'wide'
+ .js-new-tag-ref-selector{ data: { project_id: @project.id, default_branch_name: default_ref, hidden_input_name: 'ref' } }
.form-text.text-muted
= s_('TagsPage|Existing branch name, tag, or commit SHA')
.form-group.row
@@ -42,5 +37,4 @@
= s_('TagsPage|Create tag')
= render Pajamas::ButtonComponent.new(href: project_tags_path(@project)) do
= s_('TagsPage|Cancel')
--# haml-lint:disable InlineJavaScript
-%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
+
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 29bdca1c876..fd807350245 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,7 +2,7 @@
.tree-ref-container.gl-display-flex.mb-2.mb-md-0
.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'tree', show_create: true
+ #js-tree-ref-switcher{ data: { project_id: @project.id, project_root_path: project_path(@project) } }
#js-repo-breadcrumb{ data: breadcrumb_data_attributes }
diff --git a/app/views/projects/triggers/_form.html.haml b/app/views/projects/triggers/_form.html.haml
index b6b24a0c26a..b621f1ab3ed 100644
--- a/app/views/projects/triggers/_form.html.haml
+++ b/app/views/projects/triggers/_form.html.haml
@@ -6,6 +6,6 @@
%label.label-bold Token
%p.form-control-plaintext= @trigger.token
.form-group
- = f.label :key, "Description", class: "label-bold"
- = f.text_field :description, class: 'form-control gl-form-input', required: true, title: 'Trigger description is required.', placeholder: "Trigger description"
+ = f.label :key, s_("Trigger|Description"), class: "label-bold"
+ = f.text_field :description, class: 'form-control gl-form-input', required: true, title: 'Trigger description is required.', placeholder: s_("Trigger|Trigger description")
= f.submit btn_text, pajamas_button: true
diff --git a/app/views/protected_branches/_branches_list.html.haml b/app/views/protected_branches/_branches_list.html.haml
new file mode 100644
index 00000000000..82eac348f16
--- /dev/null
+++ b/app/views/protected_branches/_branches_list.html.haml
@@ -0,0 +1,4 @@
+- can_admin_project = can?(current_user, :admin_project, @project)
+
+= render layout: 'protected_branches/shared/branches_list', locals: { can_admin_project: can_admin_project } do
+ = render partial: 'protected_branches/protected_branch', collection: @protected_branches
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/protected_branches/_create_protected_branch.html.haml
index 76aadc3be28..22a49ba9c7e 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/_create_protected_branch.html.haml
@@ -11,4 +11,4 @@
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: 'allowed_to_push_dropdown' }})
-= render 'projects/protected_branches/shared/create_protected_branch'
+= render 'protected_branches/shared/create_protected_branch'
diff --git a/app/views/protected_branches/_index.html.haml b/app/views/protected_branches/_index.html.haml
new file mode 100644
index 00000000000..4beca4845b8
--- /dev/null
+++ b/app/views/protected_branches/_index.html.haml
@@ -0,0 +1,7 @@
+- content_for :create_protected_branch do
+ = render 'protected_branches/create_protected_branch'
+
+- content_for :branches_list do
+ = render "protected_branches/branches_list"
+
+= render 'protected_branches/shared/index'
diff --git a/app/views/protected_branches/_protected_branch.html.haml b/app/views/protected_branches/_protected_branch.html.haml
new file mode 100644
index 00000000000..423d7f23eb5
--- /dev/null
+++ b/app/views/protected_branches/_protected_branch.html.haml
@@ -0,0 +1,2 @@
+= render layout: 'protected_branches/shared/protected_branch', locals: { protected_branch: protected_branch } do
+ = render_if_exists 'protected_branches/update_protected_branch', protected_branch: protected_branch
diff --git a/app/views/protected_branches/_update_protected_branch.html.haml b/app/views/protected_branches/_update_protected_branch.html.haml
new file mode 100644
index 00000000000..a9290d9e0da
--- /dev/null
+++ b/app/views/protected_branches/_update_protected_branch.html.haml
@@ -0,0 +1 @@
+= render 'protected_branches/shared/update_protected_branch', protected_branch: protected_branch
diff --git a/app/views/projects/protected_branches/shared/_branches_list.html.haml b/app/views/protected_branches/shared/_branches_list.html.haml
index 64db51d5df2..d041f9c5b48 100644
--- a/app/views/projects/protected_branches/shared/_branches_list.html.haml
+++ b/app/views/protected_branches/shared/_branches_list.html.haml
@@ -28,7 +28,7 @@
%span.has-tooltip{ data: { container: 'body' }, title: s_('ProtectedBranch|Allow all users with push access to force push.'), 'aria-hidden': 'true' }
= sprite_icon('question', size: 16, css_class: 'gl-text-gray-500')
- = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table_head'
+ = render_if_exists 'protected_branches/ee/code_owner_approval_table_head'
- if can_admin_project
%th
diff --git a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml b/app/views/protected_branches/shared/_create_protected_branch.html.haml
index 770d79943b3..6b4a143df69 100644
--- a/app/views/projects/protected_branches/shared/_create_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_create_protected_branch.html.haml
@@ -8,7 +8,7 @@
.form-group.row
= f.label :name, s_('ProtectedBranch|Branch:'), class: 'col-sm-12'
.col-sm-12
- = render partial: "projects/protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-input-lg' }
+ = render partial: "protected_branches/shared/dropdown", locals: { f: f, toggle_classes: 'gl-w-full! gl-form-input-lg' }
.form-text.text-muted
- wildcards_url = help_page_url('user/project/protected_branches', anchor: 'configure-multiple-protected-branches-by-using-a-wildcard')
- wildcards_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: wildcards_url }
@@ -30,6 +30,6 @@
- force_push_docs_url = help_page_url('topics/git/git_rebase', anchor: 'force-push')
- force_push_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: force_push_docs_url }
= (s_("ProtectedBranch|Allow all users with push access to %{tag_start}force push%{tag_end}.") % { tag_start: force_push_link_start, tag_end: '</a>' }).html_safe
- = render_if_exists 'projects/protected_branches/ee/code_owner_approval_form', f: f
+ = render_if_exists 'protected_branches/ee/code_owner_approval_form', f: f
- c.footer do
= f.submit s_('ProtectedBranch|Protect'), disabled: true, data: { qa_selector: 'protect_button' }, pajamas_button: true
diff --git a/app/views/projects/protected_branches/shared/_dropdown.html.haml b/app/views/protected_branches/shared/_dropdown.html.haml
index c5dbf8991cd..c5dbf8991cd 100644
--- a/app/views/projects/protected_branches/shared/_dropdown.html.haml
+++ b/app/views/protected_branches/shared/_dropdown.html.haml
diff --git a/app/views/projects/protected_branches/shared/_index.html.haml b/app/views/protected_branches/shared/_index.html.haml
index c204508d355..c204508d355 100644
--- a/app/views/projects/protected_branches/shared/_index.html.haml
+++ b/app/views/protected_branches/shared/_index.html.haml
diff --git a/app/views/projects/protected_branches/shared/_matching_branch.html.haml b/app/views/protected_branches/shared/_matching_branch.html.haml
index 1a2ec38fae9..1a2ec38fae9 100644
--- a/app/views/projects/protected_branches/shared/_matching_branch.html.haml
+++ b/app/views/protected_branches/shared/_matching_branch.html.haml
diff --git a/app/views/projects/protected_branches/shared/_protected_branch.html.haml b/app/views/protected_branches/shared/_protected_branch.html.haml
index 098bd4a7eeb..5dea85aaa41 100644
--- a/app/views/projects/protected_branches/shared/_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_protected_branch.html.haml
@@ -16,7 +16,7 @@
= yield
- = render_if_exists 'projects/protected_branches/ee/code_owner_approval_table', protected_branch: protected_branch
+ = render_if_exists 'protected_branches/ee/code_owner_approval_table', protected_branch: protected_branch
- if can_admin_project
%td
diff --git a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml b/app/views/protected_branches/shared/_update_protected_branch.html.haml
index d10196a83cc..0244f9e2158 100644
--- a/app/views/shared/projects/protected_branches/_update_protected_branch.html.haml
+++ b/app/views/protected_branches/shared/_update_protected_branch.html.haml
@@ -9,7 +9,7 @@
%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') ,
+ = 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) }})
- if user_merge_access_levels.any?
@@ -22,7 +22,7 @@
%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') ,
+ = 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) }})
- if user_push_access_levels.any?
diff --git a/app/views/projects/protected_branches/show.html.haml b/app/views/protected_branches/show.html.haml
index c671757a603..e0bd392ae93 100644
--- a/app/views/projects/protected_branches/show.html.haml
+++ b/app/views/protected_branches/show.html.haml
@@ -19,7 +19,7 @@
%th Last commit
%tbody
- @matching_refs.each do |matching_branch|
- = render partial: "projects/protected_branches/shared/matching_branch", object: matching_branch
+ = render partial: "protected_branches/shared/matching_branch", object: matching_branch
- else
%p.settings-message.text-center
Couldn't find any matching branches.
diff --git a/app/views/pwa/manifest.json.erb b/app/views/pwa/manifest.json.erb
index 557a39ee157..c5403caeafa 100644
--- a/app/views/pwa/manifest.json.erb
+++ b/app/views/pwa/manifest.json.erb
@@ -1,7 +1,7 @@
{
- "name": "GitLab",
- "short_name": "GitLab",
- "description": "<%= _("The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly.") %>",
+ "name": "<%= Appearance.current&.title.presence || _('GitLab') %>",
+ "short_name": "<%= Appearance.current&.short_title.presence || _('GitLab') %>",
+ "description": "<%= Appearance.current&.description.presence || _("The complete DevOps platform. One application with endless possibilities. Organizations rely on GitLab’s source code management, CI/CD, security, and more to deliver software rapidly.") %>",
"start_url": "<%= explore_projects_path %>",
"scope": "<%= root_path %>",
"display": "browser",
diff --git a/app/views/registrations/welcome/show.html.haml b/app/views/registrations/welcome/show.html.haml
index 283659875ef..f4e9a597fe2 100644
--- a/app/views/registrations/welcome/show.html.haml
+++ b/app/views/registrations/welcome/show.html.haml
@@ -18,22 +18,24 @@
%p.gl-text-center= html_escape(_('%{gitlab_experience_text}. Don\'t worry, this information isn\'t shared outside of your self-managed GitLab instance.')) % { gitlab_experience_text: gitlab_experience_text }
= gitlab_ui_form_for(current_user,
url: users_sign_up_welcome_path(glm_tracking_params),
- html: { class: 'card gl-w-full! gl-p-5 js-users-signup-welcome',
+ html: { class: 'gl-w-full! gl-p-5 js-users-signup-welcome',
'aria-live' => 'assertive',
data: { testid: 'welcome-form' } }) do |f|
- .devise-errors
- = render 'devise/shared/error_messages', resource: current_user
- .row
- .form-group.col-sm-12
- = f.label :role, _('Role'), class: 'label-bold'
- = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' }
- = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
- = render_if_exists "registrations/welcome/setup_for_company", f: f
- = render_if_exists "registrations/welcome/joining_project"
- = render 'devise/shared/email_opted_in', f: f
- .row
- .form-group.col-sm-12.gl-mb-0
- - if partial_exists? "registrations/welcome/button"
- = render "registrations/welcome/button"
- - else
- = f.submit _('Get started!'), class: 'btn-confirm gl-button btn btn-block gl-mb-0 gl-p-3', data: { qa_selector: 'get_started_button' }
+ = render Pajamas::CardComponent.new do |c|
+ - c.body do
+ .devise-errors
+ = render 'devise/shared/error_messages', resource: current_user
+ .row
+ .form-group.col-sm-12
+ = f.label :role, _('Role'), class: 'label-bold'
+ = f.select :role, ::User.roles.keys.map { |role| [role.titleize, role] }, { include_blank: _('Select a role') }, class: 'form-control js-user-role-dropdown', autofocus: true, required: true, data: { qa_selector: 'role_dropdown' }
+ = render_if_exists "registrations/welcome/jobs_to_be_done", f: f
+ = render_if_exists "registrations/welcome/setup_for_company", f: f
+ = render_if_exists "registrations/welcome/joining_project"
+ = render 'devise/shared/email_opted_in', f: f
+ .row
+ .form-group.col-sm-12.gl-mb-0
+ - if partial_exists? "registrations/welcome/button"
+ = render "registrations/welcome/button"
+ - else
+ = f.submit _('Get started!'), class: 'btn-confirm gl-button btn btn-block gl-mb-0 gl-p-3', data: { qa_selector: 'get_started_button' }
diff --git a/app/views/search/_category.html.haml b/app/views/search/_category.html.haml
index c15afd7bd5b..3e483fe8cd2 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -23,7 +23,7 @@
= search_filter_link 'milestones', _("Milestones")
= users
- - elsif @show_snippets
+ - elsif @search_service.show_snippets?
= search_filter_link 'snippet_titles', _("Titles and Descriptions"), search: { snippets: true, group_id: nil, project_id: nil }
- else
= search_filter_link 'projects', _("Projects"), data: { qa_selector: 'projects_tab' }
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index 36458a909fc..188ead4008e 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -13,7 +13,7 @@
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
- %li.issuable-upvotes.gl-list-style-none
+ %li.gl-list-style-none
%span.has-tooltip{ title: _('Upvotes') }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= issuable.upvotes_count
diff --git a/app/views/search/show.html.haml b/app/views/search/show.html.haml
index 9d812e77ad4..e1efa271d57 100644
--- a/app/views/search/show.html.haml
+++ b/app/views/search/show.html.haml
@@ -9,7 +9,7 @@
- project_attributes = @project&.attributes&.slice('id', 'namespace_id', 'name')&.merge(name_with_namespace: @project&.name_with_namespace)
- if @search_results
- - if @without_count
+ - if @search_service.without_count?
- page_description(_("%{scope} results for term '%{term}'") % { scope: @scope, term: @search_term })
- else
- page_description(_("%{count} %{scope} for term '%{term}'") % { count: @search_results.formatted_count(@scope), scope: @scope, term: @search_term })
@@ -20,7 +20,7 @@
= render_if_exists 'search/form_elasticsearch', attrs: { class: 'mb-2 mb-sm-0 align-self-center' }
.gl-mt-3
- #js-search-topbar{ data: { "group-initial-data": group_attributes.to_json, "project-initial-data": project_attributes.to_json } }
+ #js-search-topbar{ data: { "group-initial-json": group_attributes.to_json, "project-initial-json": project_attributes.to_json, "elasticsearch-enabled": @elasticsearch_in_use.to_s, "default-branch-name": @project&.default_branch } }
- if @search_term
- if Feature.disabled?(:search_page_vertical_nav, current_user)
= render 'search/category'
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index c2b941c6106..93f919f01d9 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -1,13 +1,16 @@
-= render Pajamas::BannerComponent.new(button_text: s_('AutoDevOps|Enable in settings'),
- button_link: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'),
- svg_path: 'illustrations/autodevops.svg',
- banner_options: { class: 'js-autodevops-banner', data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } },
- close_options: { 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box'), class: 'js-close-callout' }) do |c|
- - c.title do
- = s_('AutoDevOps|Auto DevOps')
+- container = @no_breadcrumb_container ? 'container-fluid' : container_class
- %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+%div{ class: [container, @content_class, 'gl-pt-5!'] }
+ = render Pajamas::BannerComponent.new(button_text: s_('AutoDevOps|Enable in settings'),
+ button_link: project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'),
+ svg_path: 'illustrations/autodevops.svg',
+ banner_options: { class: 'js-autodevops-banner auto-devops-callout', data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } },
+ close_options: { 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box'), class: 'js-close-callout' }) do |c|
+ - c.title do
+ = s_('AutoDevOps|Auto DevOps')
- %p
- - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
- = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
+ %p= s_('AutoDevOps|It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+
+ %p
+ - link = link_to(s_('AutoDevOps|Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer')
+ = s_('AutoDevOps|Learn more in the %{link_to_documentation}').html_safe % { link_to_documentation: link }
diff --git a/app/views/shared/_file_highlight.html.haml b/app/views/shared/_file_highlight.html.haml
index 73ace033dc6..a749d1037a1 100644
--- a/app/views/shared/_file_highlight.html.haml
+++ b/app/views/shared/_file_highlight.html.haml
@@ -1,16 +1,28 @@
+-# We're not using `link_to` in the line loop because it is too slow once we get to thousands of lines.
+
+- offset = defined?(first_line_number) ? first_line_number : 1
+- highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
+- file_line_blame = Feature.enabled?(:file_line_blame)
+
+- if file_line_blame
+ - line_class = "js-line-links"
+ - blame_path = project_blame_path(@project, tree_join(@ref, blob.path))
+- else
+ - line_class = nil
+ - blame_path = nil
+
+- highlighted_blob = blob.present.highlight
+
#blob-content.file-content.code.js-syntax-highlight
- - offset = defined?(first_line_number) ? first_line_number : 1
- - if Feature.enabled?(:file_line_blame)
- - blame_path = project_blame_path(@project, tree_join(@ref, blob.path))
.line-numbers{ class: "gl-px-0!", data: { blame_path: blame_path } }
- if blob.data.present?
- - blob.data.each_line.each_with_index do |_, index|
+ - highlighted_blob.lines.count.times do |index|
- i = index + offset
- -# We're not using `link_to` because it is too slow once we get to thousands of lines.
- %a.file-line-num.diff-line-num{ class: ("js-line-links" if Feature.enabled?(:file_line_blame)), href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
+
+ %a.file-line-num.diff-line-num{ class: line_class, href: "#L#{i}", id: "L#{i}", 'data-line-number' => i }
= i
- - highlight = defined?(highlight_line) && highlight_line ? highlight_line - offset : nil
+
.blob-content{ data: { blob_id: blob.id, path: blob.path, highlight_line: highlight, qa_selector: 'file_content' } }
%pre.code.highlight
%code
- = blob.present.highlight
+ = highlighted_blob
diff --git a/app/views/shared/_ide_root.html.haml b/app/views/shared/_ide_root.html.haml
new file mode 100644
index 00000000000..848ff1e5728
--- /dev/null
+++ b/app/views/shared/_ide_root.html.haml
@@ -0,0 +1,11 @@
+- data = local_assigns.fetch(:data)
+- loading_text = local_assigns.fetch(:loading_text)
+
+-# Fix for iOS 13+, the height of the page is actually less than
+-# 100vh because of the presence of the bottom bar
+- @body_class = 'gl-max-h-full gl-fixed'
+
+#ide.gl--flex-center.gl-h-full{ data: data }
+ .gl-text-center
+ = gl_loading_icon(size: 'md')
+ %h2.clgray= loading_text
diff --git a/app/views/shared/_issuable_meta_data.html.haml b/app/views/shared/_issuable_meta_data.html.haml
index 01ab7bf9cd4..982d3b68792 100644
--- a/app/views/shared/_issuable_meta_data.html.haml
+++ b/app/views/shared/_issuable_meta_data.html.haml
@@ -6,23 +6,23 @@
- issuable_mr = @issuable_meta_data[issuable.id].merge_requests_count
- if issuable_mr > 0
- %li.issuable-mr.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests'), data: { testid: 'merge-requests' } }
+ %li.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Related merge requests'), data: { testid: 'merge-requests' } }
= sprite_icon('merge-request', css_class: "gl-vertical-align-middle")
= issuable_mr
- if upvotes > 0
- %li.issuable-upvotes.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Upvotes') }
+ %li.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Upvotes'), data: { testid: 'issuable-upvotes' } }
= sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
= upvotes
- if downvotes > 0
- %li.issuable-downvotes.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Downvotes') }
+ %li.gl-display-none.gl-sm-display-block.has-tooltip{ title: _('Downvotes'), data: { testid: 'issuable-downvotes' } }
= sprite_icon('thumb-down', css_class: "gl-vertical-align-middle")
= downvotes
= render_if_exists 'shared/issuable/blocking_issues_count', issuable: issuable
-%li.issuable-comments.gl-display-none.gl-sm-display-block
- = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count == 0)], title: _('Comments') do
+%li.gl-display-none.gl-sm-display-block
+ = link_to issuable_path, class: ['has-tooltip', ('no-comments' if note_count == 0)], title: _('Comments'), data: { testid: 'issuable-comments' } do
= sprite_icon('comments', css_class: 'gl-vertical-align-text-bottom')
= note_count
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 1645c2695b5..8a626f1620b 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -32,17 +32,17 @@
- if label.project_label? && label.project.group && can?(current_user, :admin_label, label.project.group)
%li
= render Pajamas::ButtonComponent.new(category: :tertiary,
- button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } } ) do
+ button_options: { class: 'js-promote-project-label-button', data: { url: promote_project_label_path(label.project, label), label_title: label.title, label_color: label.color, label_text_color: label.text_color, group_name: label.project.group.name } }) do
= _('Promote to group label')
%li
%span
= render Pajamas::ButtonComponent.new(category: :tertiary,
- button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } } ) do
+ button_options: { class: 'text-danger js-delete-label-modal-button', data: { label_name: label.name, subject_name: label.subject_name, destroy_path: label.destroy_path } }) do
= _('Delete')
- if current_user
%li.gl-display-inline-block.label-subscription.js-label-subscription.gl-ml-3
- if label.can_subscribe_to_label_in_different_levels?
- = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: "js-unsubscribe-button #{'hidden' if status.unsubscribed?}", data: { url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
= _('Unsubscribe')
.dropdown.dropdown-group-label{ class: ('hidden' unless status.unsubscribed?) }
= render Pajamas::ButtonComponent.new(button_options: { class: 'gl-w-full', data: { toggle: 'dropdown' } }) do
@@ -51,11 +51,11 @@
.dropdown-menu.dropdown-open-left
%ul
%li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } } ) do
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_project_label_path(@project, label) } }) do
= _('Subscribe at project level')
%li
- = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } } ) do
+ = render Pajamas::ButtonComponent.new(category: :tertiary, button_options: { class: "js-subscribe-button js-group-level #{'hidden' unless status.unsubscribed?}", data: { status: status, url: toggle_subscription_group_label_path(label.group, label) } }) do
= _('Subscribe at group level')
- else
- = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title } ) do
+ = render Pajamas::ButtonComponent.new(button_options: { class: 'js-subscribe-button gl-w-full', data: { status: status, url: toggle_subscription_path, toggle: 'tooltip', container: 'body' }, title: tooltip_title }) do
= label_subscription_toggle_button_text(label, @project)
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index ef41dc9bb79..0053f2fe444 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,6 +1,6 @@
- count_badge_classes = 'gl-display-none gl-sm-display-inline-flex'
-= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } } ) do
+= gl_tabs_nav({class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } }) do
= gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do
= _('Open')
= gl_tab_counter_badge counts[:opened], { class: count_badge_classes }
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 0bd5d1795d0..d080d8be8fe 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,5 +1,5 @@
- if any_projects?(@projects)
- .dropdown.b-dropdown.gl-new-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
+ .dropdown.b-dropdown.gl-dropdown.btn-group.project-item-select-holder{ class: 'gl-display-inline-flex!' }
%a.btn.gl-button.btn-confirm.split-content-button.js-new-project-item-link.block-truncated{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= gl_loading_icon(inline: true, color: 'light')
= project_select_tag :project_path, class: "project-item-select gl-absolute! gl-visibility-hidden", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path], with_shared: local_assigns[:with_shared], include_projects_in_subgroups: local_assigns[:include_projects_in_subgroups] }, with_feature_enabled: local_assigns[:with_feature_enabled]
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 20bf2141cc3..fa718a9c907 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -13,7 +13,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: ref, ref_type: @ref_type, refs_url: refs_project_path(@project, sort: 'updated_desc'), field_name: field_name, submit_form_on_click: true, visit: true, qa_selector: "branches_dropdown", testid: "branches-select" }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable.git-revision-dropdown.dropdown-menu-paging{ class: ("dropdown-menu-right" if local_assigns[:align_right]), data: { qa_selector: "branches_dropdown_content" } }
.dropdown-page-one
= dropdown_title _("Switch branch/tag")
diff --git a/app/views/shared/_web_ide_button.html.haml b/app/views/shared/_web_ide_button.html.haml
index 83646a3c92e..aeaccdfa54b 100644
--- a/app/views/shared/_web_ide_button.html.haml
+++ b/app/views/shared/_web_ide_button.html.haml
@@ -2,4 +2,4 @@
- button_data = web_ide_button_data({ blob: blob })
- fork_options = fork_modal_options(@project, @ref, @path, blob)
-.gl-display-inline-block{ data: { options: button_data.merge(fork_options).to_json }, id: "js-#{type}-web-ide-link" }
+.gl-display-inline-block{ data: { options: button_data.merge(fork_options).to_json, web_ide_promo_popover_img: image_path('web-ide-promo-popover.svg') }, id: "js-#{type}-web-ide-link" }
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 8e4b8d6d428..8f2b9fc06e3 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,6 +1,6 @@
- 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_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 })
diff --git a/app/views/shared/empty_states/_milestones.html.haml b/app/views/shared/empty_states/_milestones.html.haml
index fb69e75370e..0d7dbd1415b 100644
--- a/app/views/shared/empty_states/_milestones.html.haml
+++ b/app/views/shared/empty_states/_milestones.html.haml
@@ -6,7 +6,7 @@
.svg-content
= image_tag 'illustrations/milestone_burndown_chart.svg'
.col-12
- .text-content
+ .text-content.text-center
%h4= s_('Milestones|Use milestones to track issues and merge requests over a fixed period of time')
%p.state-description
= s_('Milestones|Organize issues and merge requests into a cohesive group, and set optional start and due dates. %{learn_more_link}').html_safe % { learn_more_link: learn_more_link }
diff --git a/app/views/shared/empty_states/_milestones_tab.html.haml b/app/views/shared/empty_states/_milestones_tab.html.haml
index f6760b0a3f4..52df30434b4 100644
--- a/app/views/shared/empty_states/_milestones_tab.html.haml
+++ b/app/views/shared/empty_states/_milestones_tab.html.haml
@@ -12,6 +12,6 @@
%h4.text-center= s_('Milestones|There are no closed milestones')
- else
%h4.text-center= s_('Milestones|There are no open milestones')
- %p.state-description
+ %p.state-description.text-center
= s_('Milestones|Create a milestone to better track your issues and merge requests. %{learn_more_link}').html_safe % { learn_more_link: learn_more_link }
= yield
diff --git a/app/views/shared/file_hooks/_index.html.haml b/app/views/shared/file_hooks/_index.html.haml
index d48e9f3d02e..16e89463a4b 100644
--- a/app/views/shared/file_hooks/_index.html.haml
+++ b/app/views/shared/file_hooks/_index.html.haml
@@ -11,15 +11,16 @@
.col-lg-8.gl-mb-3
- if file_hooks.any?
- .card
- .card-header
+ = render Pajamas::CardComponent.new do |c|
+ - c.header do
= _('File Hooks (%{count})') % { count: file_hooks.count }
- %ul.content-list
- - file_hooks.each do |file|
- %li
- .monospace
- = File.basename(file)
-
+ - c.body do
+ %ul.content-list
+ - file_hooks.each do |file|
+ %li
+ .monospace
+ = File.basename(file)
- else
- .card.bg-light.text-center
- .nothing-here-block= _('No file hooks found.')
+ = render Pajamas::CardComponent.new do |c|
+ - c.body do
+ .nothing-here-block= _('No file hooks found.')
diff --git a/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml
new file mode 100644
index 00000000000..9fe1400e877
--- /dev/null
+++ b/app/views/shared/gitlab_version/_security_patch_upgrade_alert.html.haml
@@ -0,0 +1,4 @@
+- return unless show_security_patch_upgrade_alert?
+
+#js-security-patch-upgrade-alert{ data: { "current_version": Gitlab.version_info } }
+#js-security-patch-upgrade-alert-modal{ data: { "current_version": Gitlab.version_info, "version": gitlab_version_check.to_json } }
diff --git a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
index 896249c6163..dda84e0fb9e 100644
--- a/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_custom_metrics.html.haml
@@ -6,12 +6,12 @@
= link_to s_('PrometheusService|More information'), help_page_path('operations/metrics/index.md', anchor: 'adding-custom-metrics'), target: '_blank', rel: "noopener noreferrer"
.col-lg-9
- .card.custom-monitored-metrics.js-panel-custom-monitored-metrics{ data: { qa_selector: 'custom_metrics_container', active_custom_metrics: project_prometheus_metrics_path(project), environments_data: environments_list_data, service_active: "#{integration.active}" } }
+ .card.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}" } }
.card-header
%strong
= s_('PrometheusService|Custom metrics')
= gl_badge_tag 0, nil, class: 'js-custom-monitored-count'
- = link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn gl-button btn-confirm gl-ml-auto js-new-metric-button hidden', data: { qa_selector: 'new_metric_button' }
+ = link_to s_('PrometheusService|New metric'), new_project_prometheus_metric_path(project), class: 'btn gl-button btn-confirm gl-ml-auto js-new-metric-button hidden'
.card-body
.flash-container.hidden
.flash-warning
diff --git a/app/views/shared/integrations/prometheus/_metrics.html.haml b/app/views/shared/integrations/prometheus/_metrics.html.haml
index 8ee0ddfa1b1..c74dbfd8b15 100644
--- a/app/views/shared/integrations/prometheus/_metrics.html.haml
+++ b/app/views/shared/integrations/prometheus/_metrics.html.haml
@@ -25,8 +25,8 @@
.card.hidden.js-panel-missing-env-vars
.card-header
- = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right' )
- = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden' )
+ = sprite_icon('chevron-lg-right', css_class: 'panel-toggle js-panel-toggle-right')
+ = sprite_icon('chevron-lg-down', css_class: 'panel-toggle js-panel-toggle-down hidden')
= s_('PrometheusService|Missing environment variable')
= gl_badge_tag 0, nil, class: 'js-env-var-count'
.card-body.hidden
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index a325ad5f447..07cdbbece8c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -62,9 +62,9 @@
= sanitize(html_escape(_('Please review the %{linkStart}contribution guidelines%{linkEnd} for this project.')) % { linkStart: contribution_guidelines_start, linkEnd: contribution_guidelines_end })
- if issuable.new_record?
- = form.submit "#{_('Create')} #{issuable.class.model_name.human.downcase}", class: 'gl-button btn btn-confirm gl-mr-2', 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', data: { qa_selector: 'issuable_create_button', track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- else
- = form.submit _('Save changes'), class: 'gl-button btn btn-confirm gl-mr-2', data: { track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
+ = form.submit _('Save changes'), pajamas_button: true, class: 'gl-mr-2', data: { track_action: 'click_button', track_label: 'submit_mr', track_value: 0 }
- if issuable.new_record?
= link_to _('Cancel'), polymorphic_path([@project, issuable.class]), class: 'btn gl-button btn-default js-reset-autosave'
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 0fd128df997..39a123f4775 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -20,7 +20,7 @@
.js-sidebar-todo-widget-root{ data: { project_path: issuable_sidebar[:project_full_path], iid: issuable_sidebar[:iid], id: issuable_sidebar[:id] } }
= form_for issuable_type, url: issuable_sidebar[:issuable_json_path], remote: true, html: { class: 'issuable-context-form inline-update js-issuable-update' } do |f|
- .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container' } }
+ .block.assignee{ class: "#{'gl-mt-3' if !signed_in && moved_sidebar_enabled}", data: { qa_selector: 'assignee_block_container', testid: 'assignee-block-container' } }
= render "shared/issuable/sidebar_assignees", issuable_sidebar: issuable_sidebar, assignees: assignees, signed_in: signed_in
- if issuable_sidebar[:supports_severity]
@@ -101,7 +101,7 @@
.sidebar-collapsed-icon{ data: { toggle: 'tooltip', placement: 'left', container: 'body', boundary: 'viewport' }, title: _('Move issue') }
= sprite_icon('long-arrow')
.dropdown.sidebar-move-issue-dropdown.hide-collapsed
- = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } } ) do
+ = render Pajamas::ButtonComponent.new(block: true, button_options: { class: 'js-sidebar-dropdown-toggle js-move-issue', data: { toggle: 'dropdown', display: 'static', track_label: "right_sidebar", track_property: "move_issue", track_action: "click_button", track_value: "" } }) do
= _('Move issue')
.dropdown-menu.dropdown-menu-selectable.dropdown-extended-height
= dropdown_title(_('Move issue'))
diff --git a/app/views/shared/issuable/form/_title.html.haml b/app/views/shared/issuable/form/_title.html.haml
index 51f49c7ca8e..0f6ef33d532 100644
--- a/app/views/shared/issuable/form/_title.html.haml
+++ b/app/views/shared/issuable/form/_title.html.haml
@@ -4,8 +4,8 @@
- no_issuable_templates = issuable_templates(ref_project, issuable.to_ability_name).empty?
- toggle_wip_link_start = '<a href="" class="js-toggle-wip">'
- toggle_wip_link_end = '</a>'
-- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe } ).html_safe
-- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.' ) % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe } ).html_safe
+- add_wip_text = (_('%{link_start}Start the title with %{draft_snippet}%{link_end} to prevent a merge request draft from merging before it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft:</code>'.html_safe }).html_safe
+- remove_wip_text = (_('%{link_start}Remove the %{draft_snippet} prefix%{link_end} from the title to allow this merge request to be merged when it\'s ready.') % { link_start: toggle_wip_link_start, link_end: toggle_wip_link_end, draft_snippet: '<code>Draft</code>'.html_safe }).html_safe
%div{ data: { testid: 'issue-title-input-field' } }
= form.text_field :title, required: true, aria: { required: true }, maxlength: 255, autofocus: true,
diff --git a/app/views/shared/issue_type/_details_content.html.haml b/app/views/shared/issue_type/_details_content.html.haml
index 8a9b71fd91e..42f6f7b71a3 100644
--- a/app/views/shared/issue_type/_details_content.html.haml
+++ b/app/views/shared/issue_type/_details_content.html.haml
@@ -29,7 +29,7 @@
- if can?(current_user, :admin_feature_flags_issue_links, @project)
= render_if_exists 'projects/issues/related_feature_flags'
- - if can?(current_user, :download_code, @project)
+ - if can?(current_user, :read_code, @project)
- add_page_startup_api_call related_branches_path
#related-branches{ data: { url: related_branches_path } }
-# This element is filled in using JavaScript.
diff --git a/app/views/shared/nav/_sidebar_submenu.html.haml b/app/views/shared/nav/_sidebar_submenu.html.haml
index 344dafe7c0f..33b48470020 100644
--- a/app/views/shared/nav/_sidebar_submenu.html.haml
+++ b/app/views/shared/nav/_sidebar_submenu.html.haml
@@ -1,5 +1,5 @@
%ul.sidebar-sub-level-items{ class: ('is-fly-out-only' unless sidebar_menu.has_renderable_items?) }
- = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' } ) do
+ = nav_link(**sidebar_menu.all_active_routes, html_options: { class: 'fly-out-top-item' }) do
%span.fly-out-top-item-container
%strong.fly-out-top-item-name
= sidebar_menu.title
diff --git a/app/views/shared/projects/_dropdown.html.haml b/app/views/shared/projects/_dropdown.html.haml
index 88ac03bf9e3..59f8bf0e875 100644
--- a/app/views/shared/projects/_dropdown.html.haml
+++ b/app/views/shared/projects/_dropdown.html.haml
@@ -1,5 +1,5 @@
- @sort ||= sort_value_latest_activity
-.dropdown.js-project-filter-dropdown-wrap
+.dropdown.js-project-filter-dropdown-wrap.gl-display-inline
= dropdown_toggle(projects_sort_options_hash[@sort], { toggle: 'dropdown', display: 'static' }, { id: 'sort-projects-dropdown' })
%ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
%li.dropdown-header
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 908eb2428e8..40cd81ab3da 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -52,7 +52,7 @@
%span.user-access-role.gl-display-block.gl-m-0{ data: { qa_selector: 'user_role_content' } }= Gitlab::Access.human_access(access)
- if !explore_projects_tab?
- = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project
+ = render_if_exists 'compliance_management/compliance_framework/compliance_framework_badge', project: project, additional_classes: 'gl-ml-3!'
- if show_last_commit_as_description
.description.gl-display-none.gl-sm-display-block.gl-overflow-hidden.gl-mr-3.gl-mt-2
diff --git a/app/views/shared/projects/_search_bar.html.haml b/app/views/shared/projects/_search_bar.html.haml
deleted file mode 100644
index 5271a5fac09..00000000000
--- a/app/views/shared/projects/_search_bar.html.haml
+++ /dev/null
@@ -1,26 +0,0 @@
-- @sort ||= sort_value_latest_activity
-- project_tab_filter = local_assigns.fetch(:project_tab_filter, "")
-- flex_grow_and_shrink_xs = 'd-flex flex-xs-grow-1 flex-xs-shrink-1 flex-grow-0 flex-shrink-0'
-
-.filtered-search-block.row-content-block.bt-0
- .filtered-search-wrapper.d-flex.gl-flex-nowrap.flex-column.flex-sm-wrap.flex-sm-row.flex-xl-nowrap
- - unless project_tab_filter == :starred
- .filtered-search-nav.mb-2.mb-lg-0{ class: flex_grow_and_shrink_xs }
- = render 'dashboard/projects/nav', project_tab_filter: project_tab_filter
- .filtered-search.d-flex.flex-grow-1.flex-shrink-1.w-100.mb-2.mb-lg-0.ml-0{ class: project_tab_filter == :starred ? "extended-filtered-search-box mb-2 mb-lg-0" : "ml-sm-3" }
- .btn-group.w-100{ role: "group" }
- .btn-group.w-100{ role: "group" }
- .filtered-search-box.m-0
- .filtered-search-box-input-container.pl-2
- = render 'shared/projects/search_form', admin_view: false, search_form_placeholder: _("Search projects...")
- = render Pajamas::ButtonComponent.new(icon: 'search', icon_classes: 'search-icon', button_options: { type: 'submit', form: 'project-filter-form' })
- .filtered-search-dropdown.flex-row.align-items-center.mb-2.m-sm-0#filtered-search-visibility-dropdown{ class: flex_grow_and_shrink_xs }
- .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
- %span
- = _("Visibility")
- = render 'explore/projects/filter', has_label: true
- .filtered-search-dropdown.flex-row.align-items-center.m-sm-0#filtered-search-sorting-dropdown{ class: flex_grow_and_shrink_xs }
- .filtered-search-dropdown-label.p-0.pl-sm-3.font-weight-bold
- %span
- = _("Sort by")
- = render 'shared/projects/sort_dropdown'
diff --git a/app/views/shared/projects/_search_form.html.haml b/app/views/shared/projects/_search_form.html.haml
index e598343d698..07a6d5bec78 100644
--- a/app/views/shared/projects/_search_form.html.haml
+++ b/app/views/shared/projects/_search_form.html.haml
@@ -1,10 +1,9 @@
-- form_field_classes = local_assigns[:admin_view] || !Feature.enabled?(:project_list_filter_bar) ? 'input-short js-projects-list-filter' : 'gl-w-full! gl-pl-7 '
- placeholder = local_assigns[:search_form_placeholder] ? search_form_placeholder : _('Filter by name')
= form_tag filter_projects_path, method: :get, class: 'project-filter-form', data: { qa_selector: 'project_filter_form_container' }, id: 'project-filter-form' do |f|
= search_field_tag :name, params[:name],
placeholder: placeholder,
- class: "project-filter-form-field form-control #{form_field_classes}",
+ class: "project-filter-form-field form-control input-short js-projects-list-filter",
spellcheck: false,
id: 'project-filter-form-field',
autofocus: local_assigns[:autofocus]
@@ -24,4 +23,22 @@
- if params[:visibility_level].present?
= hidden_field_tag :visibility_level, params[:visibility_level]
+ - if params[:language].present?
+ = hidden_field_tag :language, params[:language]
+
+ - if Feature.enabled?(:project_language_search, current_user)
+ .dropdown.inline
+ = dropdown_toggle(search_language_placeholder, { toggle: 'dropdown', testid: 'project-language-dropdown' })
+ %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
+ %li
+ = link_to _('Any'), filter_projects_path(language: nil)
+ - programming_languages.each do |language|
+ %li
+ = link_to filter_projects_path(language: language.id), class: language_state_class(language) do
+ = language.name
+
+ = submit_tag nil, class: 'gl-display-none!'
+
+ = render 'shared/projects/dropdown'
+
= render_if_exists 'shared/projects/search_fields'
diff --git a/app/views/shared/projects/_sort_dropdown.html.haml b/app/views/shared/projects/_sort_dropdown.html.haml
deleted file mode 100644
index f3aeaacbdb1..00000000000
--- a/app/views/shared/projects/_sort_dropdown.html.haml
+++ /dev/null
@@ -1,39 +0,0 @@
-- @sort ||= sort_value_latest_activity
-- toggle_text = projects_sort_option_titles[@sort]
-
-.btn-group.w-100{ role: "group" }
- .btn-group.w-100.dropdown.js-project-filter-dropdown-wrap{ role: "group" }
- %button#sort-projects-dropdown.gl-button.btn.btn-default.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown', display: 'static' } }
- = toggle_text
- = sprite_icon('chevron-down', css_class: 'dropdown-menu-toggle-icon gl-top-3')
- %ul.dropdown-menu.dropdown-menu-right.dropdown-menu-selectable
- %li.dropdown-header
- = _("Sort by")
- - projects_sort_options_hash.each do |value, title|
- %li
- = link_to title, filter_projects_path(sort: value), class: ("is-active" if toggle_text == title)
-
- %li.divider
- %li
- = link_to filter_projects_path(archived: nil), class: ("is-active" unless params[:archived].present?) do
- = _("Hide archived projects")
- %li
- = link_to filter_projects_path(archived: true), class: ("is-active" if Gitlab::Utils.to_boolean(params[:archived])) do
- = _("Show archived projects")
- %li
- = link_to filter_projects_path(archived: 'only'), class: ("is-active" if params[:archived] == 'only') do
- = _("Show archived projects only")
-
- - if current_user && @group && @group.shared_projects.present?
- %li.divider
- %li
- = link_to filter_projects_path(shared: nil), class: ("is-active" unless params[:shared].present?) do
- = _("All projects")
- %li
- = link_to filter_projects_path(shared: 0), class: ("is-active" if params[:shared] == '0') do
- = _("Hide shared projects")
- %li
- = link_to filter_projects_path(shared: 1), class: ("is-active" if params[:shared] == '1') do
- = _("Hide group projects")
-
- = project_sort_direction_button(@sort)
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index 024b06fe97a..f4b6c3c3a50 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -51,4 +51,4 @@
.col-sm-10
= f.text_field :private_projects_minutes_cost_factor, class: 'form-control'
.form-actions
- = f.submit _('Save changes'), class: 'gl-button btn btn-confirm'
+ = f.submit _('Save changes'), pajamas_button: true
diff --git a/app/views/shared/ssh_keys/_key_delete.html.haml b/app/views/shared/ssh_keys/_key_delete.html.haml
index f8bb0e21f67..4b89b2a0cbf 100644
--- a/app/views/shared/ssh_keys/_key_delete.html.haml
+++ b/app/views/shared/ssh_keys/_key_delete.html.haml
@@ -1,9 +1,7 @@
-- title = _('Delete Key')
-- aria = { label: title }
+- icon = local_assigns[:icon]
+- category = local_assigns[:category] || :primary
-- if defined?(text)
- = button_to text, '#', class: html_class, data: button_data, title: title, aria: aria
-- else
- = button_to '#', class: html_class, data: button_data, title: title, aria: aria do
- %span.sr-only= _('Delete')
- = sprite_icon('remove')
+.gl-p-2
+ = render Pajamas::ButtonComponent.new(variant: :danger, category: category, icon: ('remove' if icon), button_options: { class: 'js-confirm-modal-button', data: button_data }) do
+ - unless icon
+ = _('Delete')
diff --git a/app/views/shared/topics/_search_form.html.haml b/app/views/shared/topics/_search_form.html.haml
index 97343983b3c..2806b2865dd 100644
--- a/app/views/shared/topics/_search_form.html.haml
+++ b/app/views/shared/topics/_search_form.html.haml
@@ -1,6 +1,6 @@
= form_tag page_filter_path, method: :get, class: "topic-filter-form js-topic-filter-form", id: 'topic-filter-form' do |f|
= search_field_tag :search, params[:search],
- placeholder: s_('Filter by name'),
+ placeholder: _('Filter by name'),
class: 'topic-filter-form-field form-control input-short',
spellcheck: false,
id: 'topic-filter-form-field',
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ecb736dac4f..7eafd6ae092 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,13 +1,6 @@
= form_errors(hook)
-- if Feature.enabled?(:webhook_form_mask_url)
- .js-vue-webhook-form{ data: webhook_form_data(hook) }
-- else
- .form-group
- = form.label :url, s_('Webhooks|URL'), class: 'label-bold'
- = form.text_field :url, class: 'form-control gl-form-input', placeholder: 'http://example.com/trigger-ci.json'
- %p.form-text.text-muted
- = s_('Webhooks|URL must be percent-encoded if it contains one or more special characters.')
+.js-vue-webhook-form{ data: webhook_form_data(hook) }
.form-group
= form.label :token, s_('Webhooks|Secret token'), class: 'label-bold'
= form.password_field :token, value: hook.masked_token, autocomplete: 'new-password', class: 'form-control gl-form-input'
@@ -19,66 +12,57 @@
= form.label :url, s_('Webhooks|Trigger'), class: 'label-bold'
%ul.list-unstyled
%li.gl-pb-5
- - if Feature.enabled?(:enhanced_webhook_support_regex)
- - is_new_hook = hook.id.nil?
- .js-vue-push-events{ data: { push_events: hook.push_events.to_s, strategy: hook.branch_filter_strategy, is_new_hook: is_new_hook.to_s, push_events_branch_filter: hook.push_events_branch_filter } }
- - else
- = form.gitlab_ui_checkbox_component :push_events, s_('Webhooks|Push events')
- .gl-pl-6
- = form.text_field :push_events_branch_filter, class: 'form-control gl-form-input',
- placeholder: 'Branch name or wildcard pattern to trigger on (leave blank for all)'
- %p.form-text.text-muted.custom-control
- = s_('Webhooks|Push to the repository.')
+ .js-vue-push-events{ data: { push_events: hook.push_events.to_s, strategy: hook.branch_filter_strategy, is_new_hook: hook.new_record?.to_s, push_events_branch_filter: hook.push_events_branch_filter } }
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :tag_push_events,
- s_('Webhooks|Tag push events'),
+ integration_webhook_event_human_name(:tag_push_events),
help_text: s_('Webhooks|A new tag is pushed to the repository.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :note_events,
- s_('Webhooks|Comments'),
+ integration_webhook_event_human_name(:note_events),
help_text: s_('Webhooks|A comment is added to an issue or merge request.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :confidential_note_events,
- s_('Webhooks|Confidential comments'),
+ integration_webhook_event_human_name(:confidential_note_events),
help_text: s_('Webhooks|A comment is added to a confidential issue.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :issues_events,
- s_('Webhooks|Issues events'),
+ integration_webhook_event_human_name(:issues_events),
help_text: s_('Webhooks|An issue is created, updated, closed, or reopened.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :confidential_issues_events,
- s_('Webhooks|Confidential issues events'),
+ integration_webhook_event_human_name(:confidential_issues_events),
help_text: s_('Webhooks|A confidential issue is created, updated, closed, or reopened.')
- if @group
= render_if_exists 'groups/hooks/member_events', form: form
= render_if_exists 'groups/hooks/subgroup_events', form: form
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :merge_requests_events,
- s_('Webhooks|Merge request events'),
+ integration_webhook_event_human_name(:merge_requests_events),
help_text: s_('Webhooks|A merge request is created, updated, or merged.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :job_events,
- s_('Webhooks|Job events'),
+ integration_webhook_event_human_name(:job_events),
help_text: s_("Webhooks|A job's status changes.")
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :pipeline_events,
- s_('Webhooks|Pipeline events'),
+ integration_webhook_event_human_name(:pipeline_events),
help_text: s_("Webhooks|A pipeline's status changes.")
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :wiki_page_events,
- s_('Webhooks|Wiki page events'),
+ integration_webhook_event_human_name(:wiki_page_events),
help_text: s_('Webhooks|A wiki page is created or updated.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :deployment_events,
- s_('Webhooks|Deployment events'),
+ integration_webhook_event_human_name(:deployment_events),
help_text: s_('Webhooks|A deployment starts, finishes, fails, or is canceled.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :feature_flag_events,
- s_('Webhooks|Feature flag events'),
+ integration_webhook_event_human_name(:feature_flag_events),
help_text: s_('Webhooks|A feature flag is turned on or off.')
%li.gl-pb-5
= form.gitlab_ui_checkbox_component :releases_events,
- s_('Webhooks|Releases events'),
+ integration_webhook_event_human_name(:releases_events),
help_text: s_('Webhooks|A release is created or updated.')
.form-group
= form.label :enable_ssl_verification, s_('Webhooks|SSL verification'), class: 'label-bold checkbox'
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index 529ef47a2cf..c19b518acd6 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -16,7 +16,7 @@
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
- = gl_badge_tag(trigger.to_s.titleize, size: :sm)
+ = gl_badge_tag(integration_webhook_event_human_name(trigger), size: :sm)
= gl_badge_tag(sslBadgeText, size: :sm)
.col-md-4.col-lg-5.text-right-md.gl-mt-2
diff --git a/app/views/shared/web_hooks/_test_button.html.haml b/app/views/shared/web_hooks/_test_button.html.haml
index 3ffa45f01be..7a78a32fe87 100644
--- a/app/views/shared/web_hooks/_test_button.html.haml
+++ b/app/views/shared/web_hooks/_test_button.html.haml
@@ -2,12 +2,12 @@
- hook = local_assigns.fetch(:hook)
- triggers = hook.class.triggers
-.hook-test-button.dropdown.gl-new-dropdown.inline>
+.hook-test-button.dropdown.gl-dropdown.inline>
%button.btn.gl-button{ 'data-toggle' => 'dropdown', class: button_class }
= _('Test')
= sprite_icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-right{ role: 'menu' }
- .gl-new-dropdown-inner
+ .gl-dropdown-inner
- triggers.each_value do |event|
- %li.gl-new-dropdown-item
+ %li.gl-dropdown-item
= link_to_test_hook(hook, event)
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 7cef87ba19f..03ecf8cac22 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -25,7 +25,7 @@
- else
= render Pajamas::ButtonComponent.new(href: new_abuse_report_path(user_id: @user.id, ref_url: request.referer),
icon: 'error',
- button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: s_('UserProfile|Report abuse'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
+ button_options: { class: 'gl-flex-grow-1 gl-mx-1 has-tooltip', title: _('Report abuse to administrator'), data: { toggle: 'tooltip', placement: 'bottom', container: 'body' }})
- verified_gpg_keys = @user.gpg_keys.select(&:verified?)
- if verified_gpg_keys.any?
= render Pajamas::ButtonComponent.new(href: user_gpg_keys_path,
diff --git a/app/views/web_ide/remote_ide/index.html.haml b/app/views/web_ide/remote_ide/index.html.haml
new file mode 100644
index 00000000000..f007794d056
--- /dev/null
+++ b/app/views/web_ide/remote_ide/index.html.haml
@@ -0,0 +1,5 @@
+- data = local_assigns.fetch(:data)
+
+- page_title _('Web IDE')
+
+= render partial: 'shared/ide_root', locals: { data: data, loading_text: _('Connecting to the remote environment...') }
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index b9168a65764..652a0021b0f 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -66,6 +66,24 @@
:weight: 3
:idempotent: false
:tags: []
+- :name: batched_background_migrations:database_batched_background_migration_ci_execution
+ :worker_name: Database::BatchedBackgroundMigration::CiExecutionWorker
+ :feature_category: :database
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: batched_background_migrations:database_batched_background_migration_main_execution
+ :worker_name: Database::BatchedBackgroundMigration::MainExecutionWorker
+ :feature_category: :database
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: chaos:chaos_cpu_spin
:worker_name: Chaos::CpuSpinWorker
:feature_category: :not_owned
@@ -426,6 +444,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: cronjob:export_prune_project_export_jobs
+ :worker_name: Gitlab::Export::PruneProjectExportJobsWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:gitlab_service_ping
:worker_name: GitlabServicePingWorker
:feature_category: :service_ping
@@ -1038,6 +1065,33 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: github_gists_importer:github_gists_import_finish_import
+ :worker_name: Gitlab::GithubGistsImport::FinishImportWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: github_gists_importer:github_gists_import_import_gist
+ :worker_name: Gitlab::GithubGistsImport::ImportGistWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
+- :name: github_gists_importer:github_gists_import_start_import
+ :worker_name: Gitlab::GithubGistsImport::StartImportWorker
+ :feature_category: :importers
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: false
+ :tags: []
- :name: github_importer:github_import_attachments_import_issue
:worker_name: Gitlab::GithubImport::Attachments::ImportIssueWorker
:feature_category: :importers
@@ -1380,6 +1434,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: jira_connect:jira_connect_send_uninstalled_hook
+ :worker_name: JiraConnect::SendUninstalledHookWorker
+ :feature_category: :integrations
+ :has_external_dependencies: true
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: jira_connect:jira_connect_sync_branch
:worker_name: JiraConnect::SyncBranchWorker
:feature_category: :integrations
@@ -1560,15 +1623,6 @@
:weight: 1
:idempotent: false
:tags: []
-- :name: object_storage:object_storage_background_move
- :worker_name: ObjectStorage::BackgroundMoveWorker
- :feature_category: :not_owned
- :has_external_dependencies: false
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: false
- :tags: []
- :name: object_storage:object_storage_migrate_uploads
:worker_name: ObjectStorage::MigrateUploadsWorker
:feature_category: :not_owned
@@ -1623,6 +1677,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: package_repositories:packages_debian_process_package_file
+ :worker_name: Packages::Debian::ProcessPackageFileWorker
+ :feature_category: :package_registry
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: package_repositories:packages_go_sync_packages
:worker_name: Packages::Go::SyncPackagesWorker
:feature_category: :package_registry
@@ -1810,7 +1873,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 4
- :idempotent: false
+ :idempotent: true
:tags: []
- :name: pipeline_default:ci_create_cross_project_pipeline
:worker_name: Ci::CreateCrossProjectPipelineWorker
@@ -2730,15 +2793,6 @@
:weight: 1
:idempotent: true
:tags: []
-- :name: merge_requests_delete_branch
- :worker_name: MergeRequests::DeleteBranchWorker
- :feature_category: :source_code_management
- :has_external_dependencies: false
- :urgency: :high
- :resource_boundary: :unknown
- :weight: 1
- :idempotent: true
- :tags: []
- :name: merge_requests_delete_source_branch
:worker_name: MergeRequests::DeleteSourceBranchWorker
:feature_category: :source_code_management
@@ -3009,6 +3063,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: projects_delete_branch
+ :worker_name: Projects::DeleteBranchWorker
+ :feature_category: :source_code_management
+ :has_external_dependencies: false
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_git_garbage_collect
:worker_name: Projects::GitGarbageCollectWorker
:feature_category: :gitaly
@@ -3018,6 +3081,15 @@
:weight: 1
:idempotent: false
:tags: []
+- :name: projects_import_export_parallel_project_export
+ :worker_name: Projects::ImportExport::ParallelProjectExportWorker
+ :feature_category: :importers
+ :has_external_dependencies: false
+ :urgency: :low
+ :resource_boundary: :memory
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: projects_import_export_relation_export
:worker_name: Projects::ImportExport::RelationExportWorker
:feature_category: :importers
@@ -3281,7 +3353,7 @@
:tags: []
- :name: update_highest_role
:worker_name: UpdateHighestRoleWorker
- :feature_category: :subscription_usage_reports
+ :feature_category: :subscription_cost_management
:has_external_dependencies: false
:urgency: :high
:resource_boundary: :unknown
diff --git a/app/workers/bulk_imports/entity_worker.rb b/app/workers/bulk_imports/entity_worker.rb
index d23d57c33ab..fb99d63d06e 100644
--- a/app/workers/bulk_imports/entity_worker.rb
+++ b/app/workers/bulk_imports/entity_worker.rb
@@ -74,6 +74,8 @@ module BulkImports
source_version: source_version,
importer: 'gitlab_migration'
)
+
+ entity.fail_op!
end
private
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index 1a5f6250429..530419dac26 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -4,11 +4,15 @@ module BulkImports
class ExportRequestWorker
include ApplicationWorker
- data_consistency :always
-
idempotent!
- worker_has_external_dependencies!
+ data_consistency :always
feature_category :importers
+ sidekiq_options dead: false, retry: 5
+ worker_has_external_dependencies!
+
+ sidekiq_retries_exhausted do |msg, exception|
+ new.perform_failure(exception, msg['args'].first)
+ end
def perform(entity_id)
entity = BulkImports::Entity.find(entity_id)
@@ -18,26 +22,12 @@ module BulkImports
request_export(entity)
BulkImports::EntityWorker.perform_async(entity_id)
- rescue BulkImports::NetworkError => e
- if e.retriable?(entity)
- retry_request(e, entity)
- else
- log_exception(e,
- {
- bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- message: "Request to export #{entity.source_type} failed",
- source_version: entity.bulk_import.source_version_info.to_s,
- importer: 'gitlab_migration'
- }
- )
-
- BulkImports::Failure.create(failure_attributes(e, entity))
-
- entity.fail_op!
- end
+ end
+
+ def perform_failure(exception, entity_id)
+ entity = BulkImports::Entity.find(entity_id)
+
+ log_and_fail(exception, entity)
end
private
@@ -104,30 +94,32 @@ module BulkImports
end
end
- def retry_request(exception, entity)
+ def logger
+ @logger ||= Gitlab::Import::Logger.build
+ end
+
+ def log_exception(exception, payload)
+ Gitlab::ExceptionLogFormatter.format!(exception, payload)
+
+ logger.error(structured_payload(payload))
+ end
+
+ def log_and_fail(exception, entity)
log_exception(exception,
{
- message: 'Retrying export request',
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,
+ message: "Request to export #{entity.source_type} failed",
source_version: entity.bulk_import.source_version_info.to_s,
importer: 'gitlab_migration'
}
)
- self.class.perform_in(2.seconds, entity.id)
- end
-
- def logger
- @logger ||= Gitlab::Import::Logger.build
- end
-
- def log_exception(exception, payload)
- Gitlab::ExceptionLogFormatter.format!(exception, payload)
+ BulkImports::Failure.create(failure_attributes(exception, entity))
- logger.error(structured_payload(payload))
+ entity.fail_op!
end
end
end
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 5716f6e3f31..62e85d38e61 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -3,6 +3,7 @@
module BulkImports
class PipelineWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include ExclusiveLeaseGuard
FILE_EXTRACTION_PIPELINE_PERFORM_DELAY = 10.seconds
@@ -10,44 +11,24 @@ module BulkImports
feature_category :importers
sidekiq_options retry: false, dead: false
worker_has_external_dependencies!
+ deduplicate :until_executing
def perform(pipeline_tracker_id, stage, entity_id)
- @pipeline_tracker = ::BulkImports::Tracker
- .with_status(:enqueued)
- .find_by_id(pipeline_tracker_id)
-
- if pipeline_tracker.present?
- @entity = @pipeline_tracker.entity
-
- logger.info(
- structured_payload(
- bulk_import_entity_id: entity.id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- pipeline_name: pipeline_tracker.pipeline_name,
- message: 'Pipeline starting',
- source_version: source_version,
- importer: 'gitlab_migration'
- )
- )
-
- run
- else
- @entity = ::BulkImports::Entity.find(entity_id)
-
- logger.error(
- structured_payload(
- bulk_import_entity_id: entity_id,
- bulk_import_id: entity.bulk_import_id,
- bulk_import_entity_type: entity.source_type,
- source_full_path: entity.source_full_path,
- pipeline_tracker_id: pipeline_tracker_id,
- message: 'Unstarted pipeline not found',
- source_version: source_version,
- importer: 'gitlab_migration'
- )
- )
+ @entity = ::BulkImports::Entity.find(entity_id)
+ @pipeline_tracker = ::BulkImports::Tracker.find(pipeline_tracker_id)
+
+ try_obtain_lease do
+ if pipeline_tracker.enqueued?
+ logger.info(log_attributes(message: 'Pipeline starting'))
+
+ run
+ else
+ message = "Pipeline in #{pipeline_tracker.human_status_name} state instead of expected enqueued state"
+
+ logger.error(log_attributes(message: message))
+
+ fail_tracker(StandardError.new(message)) unless pipeline_tracker.finished? || pipeline_tracker.skipped?
+ end
end
ensure
@@ -63,6 +44,7 @@ module BulkImports
raise(Pipeline::ExpiredError, 'Pipeline timeout') if job_timeout?
raise(Pipeline::FailedError, "Export from source instance failed: #{export_status.error}") if export_failed?
+ raise(Pipeline::ExpiredError, 'Empty export status on source instance') if empty_export_timeout?
return re_enqueue if export_empty? || export_started?
@@ -82,29 +64,9 @@ module BulkImports
def fail_tracker(exception)
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
- log_exception(exception,
- {
- 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_tracker.pipeline_name,
- message: 'Pipeline failed',
- source_version: source_version,
- importer: 'gitlab_migration'
- }
- )
+ log_exception(exception, log_attributes(message: 'Pipeline failed'))
- Gitlab::ErrorTracking.track_exception(
- exception,
- 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_tracker.pipeline_name,
- source_version: source_version,
- importer: 'gitlab_migration'
- )
+ Gitlab::ErrorTracking.track_exception(exception, log_attributes)
BulkImports::Failure.create(
bulk_import_entity_id: entity.id,
@@ -144,7 +106,11 @@ module BulkImports
def job_timeout?
return false unless file_extraction_pipeline?
- (Time.zone.now - entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
+ time_since_entity_created > Pipeline::NDJSON_EXPORT_TIMEOUT
+ end
+
+ def empty_export_timeout?
+ export_empty? && time_since_entity_created > Pipeline::EMPTY_EXPORT_STATUS_TIMEOUT
end
def export_failed?
@@ -166,18 +132,7 @@ module BulkImports
end
def retry_tracker(exception)
- log_exception(exception,
- {
- 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_tracker.pipeline_name,
- message: "Retrying pipeline",
- source_version: source_version,
- importer: 'gitlab_migration'
- }
- )
+ log_exception(exception, log_attributes(message: "Retrying pipeline"))
pipeline_tracker.update!(status_event: 'retry', jid: jid)
@@ -185,25 +140,43 @@ module BulkImports
end
def skip_tracker
- logger.info(
- structured_payload(
+ logger.info(log_attributes(message: 'Skipping pipeline due to failed entity'))
+
+ pipeline_tracker.update!(status_event: 'skip', jid: jid)
+ end
+
+ def log_attributes(extra = {})
+ structured_payload(
+ {
bulk_import_entity_id: entity.id,
bulk_import_id: entity.bulk_import_id,
bulk_import_entity_type: entity.source_type,
source_full_path: entity.source_full_path,
+ pipeline_tracker_id: pipeline_tracker.id,
pipeline_name: pipeline_tracker.pipeline_name,
- message: 'Skipping pipeline due to failed entity',
+ pipeline_tracker_state: pipeline_tracker.human_status_name,
source_version: source_version,
importer: 'gitlab_migration'
- )
+ }.merge(extra)
)
-
- pipeline_tracker.update!(status_event: 'skip', jid: jid)
end
def log_exception(exception, payload)
Gitlab::ExceptionLogFormatter.format!(exception, payload)
+
logger.error(structured_payload(payload))
end
+
+ def time_since_entity_created
+ Time.zone.now - entity.created_at
+ end
+
+ def lease_timeout
+ 30
+ end
+
+ def lease_key
+ "gitlab:bulk_imports:pipeline_worker:#{pipeline_tracker.id}"
+ end
end
end
diff --git a/app/workers/ci/create_downstream_pipeline_worker.rb b/app/workers/ci/create_downstream_pipeline_worker.rb
index 747cb088272..9f5ff45b8a6 100644
--- a/app/workers/ci/create_downstream_pipeline_worker.rb
+++ b/app/workers/ci/create_downstream_pipeline_worker.rb
@@ -11,9 +11,15 @@ module Ci
def perform(bridge_id)
::Ci::Bridge.find_by_id(bridge_id).try do |bridge|
- ::Ci::CreateDownstreamPipelineService
+ result = ::Ci::CreateDownstreamPipelineService
.new(bridge.project, bridge.user)
.execute(bridge)
+
+ if result.success?
+ log_extra_metadata_on_done(:new_pipeline_id, result.payload.id)
+ else
+ log_extra_metadata_on_done(:create_error_message, result.message)
+ end
end
end
end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 9793278ac0c..c5c7da23892 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -47,6 +47,9 @@ module Gitlab
# Representation is created but the developer forgot to add a
# `:github_identifiers` field.
track_and_raise_exception(project, e, fail_import: true)
+ 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
@@ -86,13 +89,17 @@ module Gitlab
)
end
- def track_and_raise_exception(project, exception, fail_import: false)
+ 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
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
index 9300c2a5790..f23e3fb20c2 100644
--- a/app/workers/concerns/waitable_worker.rb
+++ b/app/workers/concerns/waitable_worker.rb
@@ -6,33 +6,8 @@ module WaitableWorker
class_methods do
# Schedules multiple jobs and waits for them to be completed.
def bulk_perform_and_wait(args_list)
- # Short-circuit: it's more efficient to do small numbers of jobs inline
- if args_list.size == 1 && !always_async_project_authorizations_refresh?
- return bulk_perform_inline(args_list)
- end
-
bulk_perform_async(args_list)
end
-
- # Performs multiple jobs directly. Failed jobs will be put into sidekiq so
- # they can benefit from retries
- def bulk_perform_inline(args_list)
- failed = []
-
- args_list.each do |args|
- worker = new
- Gitlab::AppJsonLogger.info(worker.structured_payload(message: 'running inline'))
- worker.perform(*args)
- rescue StandardError
- failed << args
- end
-
- bulk_perform_async(failed) if failed.present?
- end
-
- def always_async_project_authorizations_refresh?
- Feature.enabled?(:always_async_project_authorizations_refresh)
- end
end
def perform(*args)
diff --git a/app/workers/container_registry/cleanup_worker.rb b/app/workers/container_registry/cleanup_worker.rb
index 8350ae3431b..a838b97b35d 100644
--- a/app/workers/container_registry/cleanup_worker.rb
+++ b/app/workers/container_registry/cleanup_worker.rb
@@ -15,8 +15,6 @@ module ContainerRegistry
BATCH_SIZE = 200
def perform
- return unless Feature.enabled?(:container_registry_delete_repository_with_cron_worker)
-
log_counts
reset_stale_deletes
diff --git a/app/workers/container_registry/delete_container_repository_worker.rb b/app/workers/container_registry/delete_container_repository_worker.rb
index 1f94b1b9e71..d6ecd836ed2 100644
--- a/app/workers/container_registry/delete_container_repository_worker.rb
+++ b/app/workers/container_registry/delete_container_repository_worker.rb
@@ -17,6 +17,7 @@ module ContainerRegistry
MAX_CAPACITY = 2
CLEANUP_TAGS_SERVICE_PARAMS = {
'name_regex_delete' => '.*',
+ 'keep_latest' => false,
'container_expiration_policy' => true # to avoid permissions checks
}.freeze
diff --git a/app/workers/container_registry/migration/enqueuer_worker.rb b/app/workers/container_registry/migration/enqueuer_worker.rb
index 1dd29eff86e..a4f5ac8eb7e 100644
--- a/app/workers/container_registry/migration/enqueuer_worker.rb
+++ b/app/workers/container_registry/migration/enqueuer_worker.rb
@@ -130,13 +130,13 @@ module ContainerRegistry
# this issue.
# See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/87733 and
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/90735 for details.
- ContainerRepository.ready_for_import.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
+ ContainerRepository.ready_for_import.ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
def next_aborted_repository
strong_memoize(:next_aborted_repository) do
- ContainerRepository.with_migration_state('import_aborted').limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
+ ContainerRepository.with_migration_state('import_aborted').ordered.limit(25)[0] # rubocop:disable CodeReuse/ActiveRecord
end
end
diff --git a/app/workers/database/batched_background_migration/ci_database_worker.rb b/app/workers/database/batched_background_migration/ci_database_worker.rb
index b04db87631a..58b0f5496f4 100644
--- a/app/workers/database/batched_background_migration/ci_database_worker.rb
+++ b/app/workers/database/batched_background_migration/ci_database_worker.rb
@@ -7,6 +7,10 @@ module Database
def self.tracking_database
@tracking_database ||= Gitlab::Database::CI_DATABASE_NAME.to_sym
end
+
+ def execution_worker_class
+ @execution_worker_class ||= Database::BatchedBackgroundMigration::CiExecutionWorker
+ end
end
end
end
diff --git a/app/workers/database/batched_background_migration/ci_execution_worker.rb b/app/workers/database/batched_background_migration/ci_execution_worker.rb
new file mode 100644
index 00000000000..89c70e29dda
--- /dev/null
+++ b/app/workers/database/batched_background_migration/ci_execution_worker.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Database
+ module BatchedBackgroundMigration
+ class CiExecutionWorker # rubocop:disable Scalability/IdempotentWorker
+ include ExecutionWorker
+ 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 098153c742f..b59e4bd1f86 100644
--- a/app/workers/database/batched_background_migration/execution_worker.rb
+++ b/app/workers/database/batched_background_migration/execution_worker.rb
@@ -2,14 +2,47 @@
module Database
module BatchedBackgroundMigration
- class ExecutionWorker # rubocop:disable Scalability/IdempotentWorker
+ module ExecutionWorker
+ extend ActiveSupport::Concern
include ExclusiveLeaseGuard
include Gitlab::Utils::StrongMemoize
+ include ApplicationWorker
+ include LimitedCapacity::Worker
INTERVAL_VARIANCE = 5.seconds.freeze
LEASE_TIMEOUT_MULTIPLIER = 3
+ MAX_RUNNING_MIGRATIONS = 2
- def perform(database_name, migration_id)
+ included do
+ data_consistency :always
+ feature_category :database
+ queue_namespace :batched_background_migrations
+ end
+
+ class_methods do
+ def max_running_jobs
+ MAX_RUNNING_MIGRATIONS
+ end
+
+ # We have to overirde this one, as we want
+ # arguments passed as is, and not duplicated
+ def perform_with_capacity(args)
+ worker = new
+ worker.remove_failed_jobs
+
+ bulk_perform_async(args) # rubocop:disable Scalability/BulkPerformWithContext
+ end
+ end
+
+ def remaining_work_count(*args)
+ 0 # the cron worker is the only source of new jobs
+ end
+
+ def max_running_jobs
+ self.class.max_running_jobs
+ end
+
+ def perform_work(database_name, migration_id)
self.database_name = database_name
return unless enabled?
diff --git a/app/workers/database/batched_background_migration/main_execution_worker.rb b/app/workers/database/batched_background_migration/main_execution_worker.rb
new file mode 100644
index 00000000000..661496a86a9
--- /dev/null
+++ b/app/workers/database/batched_background_migration/main_execution_worker.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Database
+ module BatchedBackgroundMigration
+ class MainExecutionWorker # rubocop:disable Scalability/IdempotentWorker
+ include ExecutionWorker
+ end
+ end
+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 0c7c51d5c0a..e772216e557 100644
--- a/app/workers/database/batched_background_migration/single_database_worker.rb
+++ b/app/workers/database/batched_background_migration/single_database_worker.rb
@@ -39,7 +39,7 @@ module Database
unless base_model
Sidekiq.logger.info(
class: self.class.name,
- database: self.class.tracking_database,
+ database: tracking_database,
message: 'skipping migration execution for unconfigured database')
return
@@ -48,34 +48,61 @@ module Database
if shares_db_config?
Sidekiq.logger.info(
class: self.class.name,
- database: self.class.tracking_database,
+ database: tracking_database,
message: 'skipping migration execution for database that shares database configuration with another database')
return
end
Gitlab::Database::SharedModel.using_connection(base_model.connection) do
- break unless self.class.enabled? && active_migration
+ break unless self.class.enabled?
- with_exclusive_lease(active_migration.interval) do
- run_active_migration
+ if parallel_execution_enabled?
+ migrations = Gitlab::Database::BackgroundMigration::BatchedMigration
+ .active_migrations_distinct_on_table(connection: base_model.connection, limit: max_running_migrations).to_a
+
+ queue_migrations_for_execution(migrations) if migrations.any?
+ else
+ break unless active_migration
+
+ with_exclusive_lease(active_migration.interval) do
+ run_active_migration
+ end
end
end
end
private
+ def parallel_execution_enabled?
+ Feature.enabled?(:batched_migrations_parallel_execution)
+ end
+
+ def max_running_migrations
+ execution_worker_class.max_running_jobs
+ end
+
def active_migration
@active_migration ||= Gitlab::Database::BackgroundMigration::BatchedMigration.active_migration(connection: base_model.connection)
end
def run_active_migration
- Database::BatchedBackgroundMigration::ExecutionWorker.new.perform(self.class.tracking_database, active_migration.id)
+ execution_worker_class.new.perform_work(tracking_database, active_migration.id)
+ end
+
+ def tracking_database
+ self.class.tracking_database
+ end
+
+ def queue_migrations_for_execution(migrations)
+ jobs_arguments = migrations.map { |migration| [tracking_database.to_s, migration.id] }
+
+ execution_worker_class.perform_with_capacity(jobs_arguments)
end
def base_model
strong_memoize(:base_model) do
- Gitlab::Database.database_base_models[self.class.tracking_database]
+ Gitlab::Database.database_base_models[tracking_database]
end
end
diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb
index 29804be832d..1450613dd89 100644
--- a/app/workers/database/batched_background_migration_worker.rb
+++ b/app/workers/database/batched_background_migration_worker.rb
@@ -7,5 +7,9 @@ module Database
def self.tracking_database
@tracking_database ||= Gitlab::Database::MAIN_DATABASE_NAME.to_sym
end
+
+ def execution_worker_class
+ @execution_worker_class ||= Database::BatchedBackgroundMigration::MainExecutionWorker
+ end
end
end
diff --git a/app/workers/delete_container_repository_worker.rb b/app/workers/delete_container_repository_worker.rb
index 73e6843fdd0..d0552dce9fc 100644
--- a/app/workers/delete_container_repository_worker.rb
+++ b/app/workers/delete_container_repository_worker.rb
@@ -11,64 +11,5 @@ class DeleteContainerRepositoryWorker # rubocop:disable Scalability/IdempotentWo
queue_namespace :container_repository
feature_category :container_registry
- LEASE_TIMEOUT = 1.hour.freeze
- FIXED_DELAY = 10.seconds.freeze
-
- attr_reader :container_repository
-
- def perform(current_user_id, container_repository_id)
- current_user = User.find_by_id(current_user_id)
- @container_repository = ContainerRepository.find_by_id(container_repository_id)
- project = container_repository&.project
-
- return unless current_user && container_repository && project
-
- if migration.delete_container_repository_worker_support? && migrating?
- delay = migration_duration
-
- self.class.perform_in(delay.from_now)
-
- log_extra_metadata_on_done(:delete_postponed, delay)
-
- return
- end
-
- # If a user accidentally attempts to delete the same container registry in quick succession,
- # this can lead to orphaned tags.
- try_obtain_lease do
- Projects::ContainerRepository::DestroyService.new(project, current_user).execute(container_repository)
- end
- end
-
- private
-
- def migrating?
- !(container_repository.default? ||
- container_repository.import_done? ||
- container_repository.import_skipped?)
- end
-
- def migration_duration
- duration = migration.import_timeout.seconds + FIXED_DELAY
-
- if container_repository.pre_importing?
- duration += migration.dynamic_pre_import_timeout_for(container_repository)
- end
-
- duration
- end
-
- def migration
- ContainerRegistry::Migration
- end
-
- # For ExclusiveLeaseGuard concern
- def lease_key
- @lease_key ||= "container_repository:delete:#{container_repository.id}"
- end
-
- # For ExclusiveLeaseGuard concern
- def lease_timeout
- LEASE_TIMEOUT
- end
+ def perform(current_user_id, container_repository_id); end
end
diff --git a/app/workers/flush_counter_increments_worker.rb b/app/workers/flush_counter_increments_worker.rb
index 8c7e17587d0..e92e1a9b7b5 100644
--- a/app/workers/flush_counter_increments_worker.rb
+++ b/app/workers/flush_counter_increments_worker.rb
@@ -29,6 +29,6 @@ class FlushCounterIncrementsWorker
model = model_class.find_by_id(model_id)
return unless model
- model.flush_increments_to_database!(attribute)
+ Gitlab::Counters::BufferedCounter.new(model, attribute).commit_increment!
end
end
diff --git a/app/workers/gitlab/export/prune_project_export_jobs_worker.rb b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb
new file mode 100644
index 00000000000..9a3c0c80f85
--- /dev/null
+++ b/app/workers/gitlab/export/prune_project_export_jobs_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Export
+ class PruneProjectExportJobsWorker
+ include ApplicationWorker
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This worker updates several import states inline and does not schedule
+ # other jobs. So no context needed
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ feature_category :importers
+ data_consistency :always
+ idempotent!
+
+ def perform
+ ProjectExportJob.prune_expired_jobs
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_gists_import/finish_import_worker.rb b/app/workers/gitlab/github_gists_import/finish_import_worker.rb
new file mode 100644
index 00000000000..1989b6314ea
--- /dev/null
+++ b/app/workers/gitlab/github_gists_import/finish_import_worker.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubGistsImport
+ class FinishImportWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ queue_namespace :github_gists_importer
+ feature_category :importers
+ idempotent!
+
+ sidekiq_options dead: false, retry: 5
+
+ sidekiq_retries_exhausted do |msg, _|
+ Gitlab::GithubGistsImport::Status.new(msg['args'][0]).fail!
+ end
+
+ INTERVAL = 30.seconds.to_i
+ BLOCKING_WAIT_TIME = 5
+
+ def perform(user_id, waiter_key, remaining)
+ waiter = wait_for_jobs(waiter_key, remaining)
+
+ if waiter.nil?
+ Gitlab::GithubGistsImport::Status.new(user_id).finish!
+
+ Gitlab::GithubImport::Logger.info(user_id: user_id, message: 'GitHub Gists import finished')
+ else
+ self.class.perform_in(INTERVAL, user_id, waiter.key, waiter.jobs_remaining)
+ end
+ end
+
+ private
+
+ def wait_for_jobs(key, remaining)
+ waiter = JobWaiter.new(remaining, key)
+ waiter.wait(BLOCKING_WAIT_TIME)
+
+ return if waiter.jobs_remaining == 0
+
+ waiter
+ 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
new file mode 100644
index 00000000000..7e2b3709597
--- /dev/null
+++ b/app/workers/gitlab/github_gists_import/import_gist_worker.rb
@@ -0,0 +1,75 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubGistsImport
+ class ImportGistWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include Gitlab::NotifyUponDeath
+
+ data_consistency :always
+ queue_namespace :github_gists_importer
+ feature_category :importers
+
+ sidekiq_options dead: false, retry: 5
+
+ def perform(user_id, gist_hash, notify_key)
+ gist = ::Gitlab::GithubGistsImport::Representation::Gist.from_json_hash(gist_hash)
+
+ with_logging(user_id, gist.github_identifiers) do
+ result = importer_class.new(gist, user_id).execute
+ error(user_id, result.errors, gist.github_identifiers) unless result.success?
+
+ JobWaiter.notify(notify_key, jid)
+ end
+ rescue StandardError => e
+ log_and_track_error(user_id, e, gist.github_identifiers)
+
+ raise
+ end
+
+ private
+
+ def importer_class
+ ::Gitlab::GithubGistsImport::Importer::GistImporter
+ end
+
+ def with_logging(user_id, gist_id)
+ info(user_id, 'start importer', gist_id)
+
+ yield
+
+ info(user_id, 'importer finished', gist_id)
+ end
+
+ def log_and_track_error(user_id, exception, gist_id)
+ error(user_id, exception.message, gist_id)
+
+ Gitlab::ErrorTracking.track_exception(exception,
+ import_type: :github_gists,
+ user_id: user_id
+ )
+ end
+
+ def error(user_id, error_message, gist_id)
+ attributes = {
+ user_id: user_id,
+ github_identifiers: gist_id,
+ message: 'importer failed',
+ 'error.message': error_message
+ }
+
+ Gitlab::GithubImport::Logger.error(structured_payload(attributes))
+ end
+
+ def info(user_id, message, gist_id)
+ attributes = {
+ user_id: user_id,
+ message: message,
+ github_identifiers: gist_id
+ }
+
+ Gitlab::GithubImport::Logger.info(structured_payload(attributes))
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab/github_gists_import/start_import_worker.rb b/app/workers/gitlab/github_gists_import/start_import_worker.rb
new file mode 100644
index 00000000000..33c91611719
--- /dev/null
+++ b/app/workers/gitlab/github_gists_import/start_import_worker.rb
@@ -0,0 +1,64 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module GithubGistsImport
+ class StartImportWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+
+ data_consistency :always
+ queue_namespace :github_gists_importer
+ feature_category :importers
+
+ sidekiq_options dead: false, retry: 5
+
+ worker_has_external_dependencies!
+
+ sidekiq_retries_exhausted do |msg, _|
+ Gitlab::GithubGistsImport::Status.new(msg['args'][0]).fail!
+
+ user = User.find(msg['args'][0])
+ Gitlab::GithubImport::PageCounter.new(user, :gists, 'github-gists-importer').expire!
+ end
+
+ def perform(user_id, encrypted_token)
+ logger.info(structured_payload(user_id: user_id, message: 'starting importer'))
+
+ user = User.find(user_id)
+ decrypted_token = Gitlab::CryptoHelper.aes256_gcm_decrypt(encrypted_token)
+ result = Gitlab::GithubGistsImport::Importer::GistsImporter.new(user, decrypted_token).execute
+
+ if result.success?
+ schedule_finish_worker(user_id, result.waiter)
+ elsif result.next_attempt_in
+ schedule_next_attempt(result.next_attempt_in, user_id, encrypted_token)
+ else
+ log_error_and_raise!(user_id, result.error)
+ end
+ end
+
+ private
+
+ def schedule_finish_worker(user_id, waiter)
+ logger.info(structured_payload(user_id: user_id, message: 'importer finished'))
+
+ Gitlab::GithubGistsImport::FinishImportWorker.perform_async(user_id, waiter.key, waiter.jobs_remaining)
+ end
+
+ def schedule_next_attempt(next_attempt_in, user_id, encrypted_token)
+ logger.info(structured_payload(user_id: user_id, message: 'rate limit reached'))
+
+ self.class.perform_in(next_attempt_in, user_id, encrypted_token)
+ end
+
+ def log_error_and_raise!(user_id, error)
+ logger.error(structured_payload(user_id: user_id, message: 'import failed', 'error.message': error.message))
+
+ raise(error)
+ end
+
+ def logger
+ Gitlab::GithubImport::Logger
+ end
+ end
+ end
+end
diff --git a/app/workers/gitlab_shell_worker.rb b/app/workers/gitlab_shell_worker.rb
index 2f396dcdb86..b3c0fa79658 100644
--- a/app/workers/gitlab_shell_worker.rb
+++ b/app/workers/gitlab_shell_worker.rb
@@ -14,7 +14,7 @@ class GitlabShellWorker # rubocop:disable Scalability/IdempotentWorker
loggable_arguments 0
def perform(action, *arg)
- if ::Feature.enabled?(:verify_gitlab_shell_worker_method_names) && Gitlab::Shell::PERMITTED_ACTIONS.exclude?(action)
+ if Gitlab::Shell::PERMITTED_ACTIONS.exclude?(action)
raise(ArgumentError, "#{action} not allowed for #{self.class.name}")
end
diff --git a/app/workers/issuable_export_csv_worker.rb b/app/workers/issuable_export_csv_worker.rb
index ffa0ed68fc7..1c5fab8c4c0 100644
--- a/app/workers/issuable_export_csv_worker.rb
+++ b/app/workers/issuable_export_csv_worker.rb
@@ -24,7 +24,7 @@ class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
def export_service(type, user, project, params)
issuable_classes = issuable_classes_for(type.to_sym)
- issuables = issuable_classes[:finder].new(user, parse_params(params, project.id)).execute
+ issuables = issuable_classes[:finder].new(user, parse_params(params, project.id, type)).execute
issuable_classes[:service].new(issuables, project)
end
@@ -39,7 +39,7 @@ class IssuableExportCsvWorker # rubocop:disable Scalability/IdempotentWorker
end
end
- def parse_params(params, project_id)
+ def parse_params(params, project_id, _type)
params
.with_indifferent_access
.except(:sort)
diff --git a/app/workers/jira_connect/send_uninstalled_hook_worker.rb b/app/workers/jira_connect/send_uninstalled_hook_worker.rb
new file mode 100644
index 00000000000..530ef4a8b8a
--- /dev/null
+++ b/app/workers/jira_connect/send_uninstalled_hook_worker.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module JiraConnect
+ class SendUninstalledHookWorker
+ include ApplicationWorker
+
+ data_consistency :delayed
+ queue_namespace :jira_connect
+ feature_category :integrations
+ urgency :low
+
+ idempotent!
+
+ worker_has_external_dependencies!
+
+ def perform(installation_id, instance_url)
+ installation = JiraConnectInstallation.find_by_id(installation_id)
+
+ JiraConnectInstallations::ProxyLifecycleEventService.execute(installation, :uninstalled, instance_url)
+ end
+ end
+end
diff --git a/app/workers/mail_scheduler/notification_service_worker.rb b/app/workers/mail_scheduler/notification_service_worker.rb
index 12e8de4491e..8206a17021b 100644
--- a/app/workers/mail_scheduler/notification_service_worker.rb
+++ b/app/workers/mail_scheduler/notification_service_worker.rb
@@ -18,9 +18,7 @@ module MailScheduler
def perform(meth, *args)
check_arguments!(args)
- if ::Feature.enabled?(:verify_mail_scheduler_notification_service_worker_method_names) &&
- NotificationService.permitted_actions.exclude?(meth.to_sym)
-
+ if NotificationService.permitted_actions.exclude?(meth.to_sym)
raise(ArgumentError, "#{meth} not allowed for #{self.class.name}")
end
diff --git a/app/workers/merge_requests/delete_branch_worker.rb b/app/workers/merge_requests/delete_branch_worker.rb
deleted file mode 100644
index 6816f9a4b77..00000000000
--- a/app/workers/merge_requests/delete_branch_worker.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-module MergeRequests
- class DeleteBranchWorker
- include ApplicationWorker
-
- data_consistency :always
-
- feature_category :source_code_management
- urgency :high
- idempotent!
-
- def perform(merge_request_id, user_id, branch_name, retarget_branch)
- merge_request = MergeRequest.find_by_id(merge_request_id)
- user = User.find_by_id(user_id)
-
- return unless merge_request.present? && user.present?
-
- ::Branches::DeleteService.new(merge_request.source_project, user).execute(branch_name)
-
- return unless retarget_branch
-
- ::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
- .execute(merge_request)
- end
- end
-end
diff --git a/app/workers/merge_requests/delete_source_branch_worker.rb b/app/workers/merge_requests/delete_source_branch_worker.rb
index 96dde413d5b..da1eca067a9 100644
--- a/app/workers/merge_requests/delete_source_branch_worker.rb
+++ b/app/workers/merge_requests/delete_source_branch_worker.rb
@@ -19,13 +19,14 @@ class MergeRequests::DeleteSourceBranchWorker
return if merge_request.source_branch_sha != source_branch_sha
if Feature.enabled?(:add_delete_branch_worker, merge_request.source_project)
- ::MergeRequests::DeleteBranchWorker.perform_async(merge_request_id, user_id, merge_request.source_branch, true)
+ ::Projects::DeleteBranchWorker.new.perform(merge_request.source_project.id, user_id,
+ merge_request.source_branch)
else
::Branches::DeleteService.new(merge_request.source_project, user).execute(merge_request.source_branch)
-
- ::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
- .execute(merge_request)
end
+
+ ::MergeRequests::RetargetChainService.new(project: merge_request.source_project, current_user: user)
+ .execute(merge_request)
rescue ActiveRecord::RecordNotFound
end
end
diff --git a/app/workers/namespaces/root_statistics_worker.rb b/app/workers/namespaces/root_statistics_worker.rb
index e3aa8a1f779..02b3468c052 100644
--- a/app/workers/namespaces/root_statistics_worker.rb
+++ b/app/workers/namespaces/root_statistics_worker.rb
@@ -4,7 +4,7 @@ module Namespaces
class RootStatisticsWorker
include ApplicationWorker
- data_consistency :sticky, feature_flag: :root_statistics_worker_read_replica
+ data_consistency :sticky
sidekiq_options retry: 3
diff --git a/app/workers/object_storage/background_move_worker.rb b/app/workers/object_storage/background_move_worker.rb
deleted file mode 100644
index bb51f0d7e1f..00000000000
--- a/app/workers/object_storage/background_move_worker.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# frozen_string_literal: true
-
-module ObjectStorage
- class BackgroundMoveWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
- include ObjectStorageQueue
-
- sidekiq_options retry: 5
- feature_category :not_owned # rubocop:todo Gitlab/AvoidFeatureCategoryNotOwned
- loggable_arguments 0, 1, 2, 3
-
- def perform(uploader_class_name, subject_class_name, file_field, subject_id)
- uploader_class = uploader_class_name.constantize
- subject_class = subject_class_name.constantize
-
- return unless uploader_class < ObjectStorage::Concern
- return unless uploader_class.object_store_enabled?
- return unless uploader_class.background_upload_enabled?
-
- subject = subject_class.find(subject_id)
- uploader = build_uploader(subject, file_field&.to_sym)
- uploader.migrate!(ObjectStorage::Store::REMOTE)
- end
-
- def build_uploader(subject, mount_point)
- case subject
- when Upload then subject.retrieve_uploader(mount_point)
- else
- subject.send(mount_point) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
-end
diff --git a/app/workers/packages/debian/process_package_file_worker.rb b/app/workers/packages/debian/process_package_file_worker.rb
new file mode 100644
index 00000000000..587c0b78c9c
--- /dev/null
+++ b/app/workers/packages/debian/process_package_file_worker.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Packages
+ module Debian
+ class ProcessPackageFileWorker
+ include ApplicationWorker
+ include ::Packages::FIPS
+ include Gitlab::Utils::StrongMemoize
+
+ data_consistency :always
+
+ deduplicate :until_executed
+ idempotent!
+
+ queue_namespace :package_repositories
+ feature_category :package_registry
+
+ def perform(package_file_id, user_id, distribution_name, component_name)
+ raise DisabledError, 'Debian registry is not FIPS compliant' if Gitlab::FIPS.enabled?
+
+ @package_file_id = package_file_id
+ @user_id = user_id
+ @distribution_name = distribution_name
+ @component_name = component_name
+
+ return unless package_file && user && distribution_name && component_name
+ # return if file has already been processed
+ return unless package_file.debian_file_metadatum&.unknown?
+
+ ::Packages::Debian::ProcessPackageFileService.new(package_file, user, distribution_name, component_name).execute
+ rescue StandardError => e
+ raise if e.instance_of?(DisabledError)
+
+ Gitlab::ErrorTracking.log_exception(e, package_file_id: @package_file_id, user_id: @user_id,
+ distribution_name: @distribution_name, component_name: @component_name)
+ package_file.destroy!
+ end
+
+ private
+
+ def package_file
+ ::Packages::PackageFile.find_by_id(@package_file_id)
+ end
+ strong_memoize_attr :package_file
+
+ def user
+ ::User.find_by_id(@user_id)
+ end
+ strong_memoize_attr :user
+ end
+ end
+end
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 329ccfc6362..f95176da252 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -142,12 +142,16 @@ class PostReceive
def emit_snowplow_event(project, user)
return unless Feature.enabled?(:route_hll_to_snowplow_phase2, project.namespace)
+ metric_path = 'counts.source_code_pushes'
Gitlab::Tracking.event(
'PostReceive',
- 'source_code_pushes',
+ :push,
project: project,
namespace: project.namespace,
- user: user
+ user: user,
+ property: 'source_code_pushes',
+ label: metric_path,
+ context: [Gitlab::Tracking::ServicePingContext.new(data_source: :redis, key_path: metric_path).to_context]
)
end
end
diff --git a/app/workers/projects/delete_branch_worker.rb b/app/workers/projects/delete_branch_worker.rb
new file mode 100644
index 00000000000..1949fb67e83
--- /dev/null
+++ b/app/workers/projects/delete_branch_worker.rb
@@ -0,0 +1,30 @@
+# frozen_string_literal: true
+
+module Projects
+ class DeleteBranchWorker
+ include ApplicationWorker
+
+ data_consistency :always
+
+ feature_category :source_code_management
+ urgency :high
+ idempotent!
+
+ def perform(project_id, user_id, branch_name)
+ project = Project.find_by_id(project_id)
+ user = User.find_by_id(user_id)
+
+ return unless project.present? && user.present?
+ return unless project.repository.branch_exists?(branch_name)
+
+ delete_service_result = ::Branches::DeleteService.new(project, user)
+ .execute(branch_name)
+
+ return unless Feature.enabled?(:track_and_raise_delete_source_errors, project)
+ # Only want to raise on 400 to avoid permission and non existant branch error
+ return unless delete_service_result[:http_status] == 400
+
+ delete_service_result.track_and_raise_exception
+ end
+ end
+end
diff --git a/app/workers/projects/import_export/parallel_project_export_worker.rb b/app/workers/projects/import_export/parallel_project_export_worker.rb
new file mode 100644
index 00000000000..ba4194fd4bc
--- /dev/null
+++ b/app/workers/projects/import_export/parallel_project_export_worker.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Projects
+ module ImportExport
+ class ParallelProjectExportWorker
+ include ApplicationWorker
+ include ExceptionBacktrace
+
+ idempotent!
+ data_consistency :always
+ deduplicate :until_executed
+ feature_category :importers
+ worker_resource_boundary :memory
+ urgency :low
+ loggable_arguments 1, 2
+ sidekiq_options retries: 3, dead: false, status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION
+
+ sidekiq_retries_exhausted do |job, exception|
+ export_job = ProjectExportJob.find(job['args'].first)
+
+ export_job.fail_op!
+ project = export_job.project
+
+ log_payload = {
+ message: 'Parallel project export error',
+ export_error: job['error_message'],
+ project_export_job_id: export_job.id,
+ project_name: project.name,
+ project_id: project.id
+ }
+ Gitlab::ExceptionLogFormatter.format!(exception, log_payload)
+ Gitlab::Export::Logger.error(log_payload)
+ end
+
+ def perform(project_export_job_id, user_id, after_export_strategy = {})
+ export_job = ProjectExportJob.find(project_export_job_id)
+
+ return if export_job.finished?
+
+ export_job.update_attribute(:jid, jid)
+ current_user = User.find(user_id)
+ after_export = build!(after_export_strategy)
+
+ export_service = ::Projects::ImportExport::ParallelExportService.new(export_job, current_user, after_export)
+ export_service.execute
+
+ export_job.finish!
+ rescue Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError
+ export_job.fail_op!
+ end
+
+ private
+
+ def build!(after_export_strategy)
+ strategy_klass = after_export_strategy&.delete('klass')
+
+ Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy)
+ end
+ end
+ 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 af62efeb089..31fdb3d9615 100644
--- a/app/workers/projects/inactive_projects_deletion_cron_worker.rb
+++ b/app/workers/projects/inactive_projects_deletion_cron_worker.rb
@@ -22,9 +22,9 @@ module Projects
return unless ::Gitlab::CurrentSettings.delete_inactive_projects?
@start_time ||= ::Gitlab::Metrics::System.monotonic_time
- admin_user = User.admins.active.first
+ admin_bot = ::User.admin_bot
- return unless admin_user
+ return unless admin_bot
notified_inactive_projects = Gitlab::InactiveProjectsDeletionWarningTracker.notified_projects
@@ -39,14 +39,14 @@ module Projects
raise TimeoutError
end
- with_context(project: project, user: admin_user) do
+ with_context(project: project, user: admin_bot) do
deletion_warning_email_sent_on = notified_inactive_projects["project:#{project.id}"]
if send_deletion_warning_email?(deletion_warning_email_sent_on, project)
- send_notification(project, admin_user)
+ send_notification(project, admin_bot)
elsif deletion_warning_email_sent_on && delete_due_to_inactivity?(deletion_warning_email_sent_on)
Gitlab::InactiveProjectsDeletionWarningTracker.new(project.id).reset
- delete_project(project, admin_user)
+ delete_project(project, admin_bot)
end
end
end
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index 8974ddce47b..f31f006eec1 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -10,6 +10,8 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_creation
feature_category :continuous_integration
+ deduplicate :until_executed
+ idempotent!
def perform(schedule_id, user_id)
schedule = Ci::PipelineSchedule.find_by_id(schedule_id)
diff --git a/app/workers/update_highest_role_worker.rb b/app/workers/update_highest_role_worker.rb
index a05c9c7a1e7..dccf88e1b1a 100644
--- a/app/workers/update_highest_role_worker.rb
+++ b/app/workers/update_highest_role_worker.rb
@@ -7,7 +7,7 @@ class UpdateHighestRoleWorker
sidekiq_options retry: 3
- feature_category :subscription_usage_reports
+ feature_category :subscription_cost_management
urgency :high
weight 2