summaryrefslogtreecommitdiff
path: root/app/assets/javascripts
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js3
-rw-r--r--app/assets/javascripts/admin/users/components/actions/activate.vue31
-rw-r--r--app/assets/javascripts/admin/users/components/actions/approve.vue41
-rw-r--r--app/assets/javascripts/admin/users/components/actions/ban.vue69
-rw-r--r--app/assets/javascripts/admin/users/components/actions/block.vue21
-rw-r--r--app/assets/javascripts/admin/users/components/actions/deactivate.vue23
-rw-r--r--app/assets/javascripts/admin/users/components/actions/index.js4
-rw-r--r--app/assets/javascripts/admin/users/components/actions/reject.vue51
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unban.vue53
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unblock.vue20
-rw-r--r--app/assets/javascripts/admin/users/components/actions/unlock.vue18
-rw-r--r--app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue (renamed from app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue)4
-rw-r--r--app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue (renamed from app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue)0
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue117
-rw-r--r--app/assets/javascripts/admin/users/constants.js10
-rw-r--r--app/assets/javascripts/admin/users/index.js65
-rw-r--r--app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue (renamed from app/assets/javascripts/analytics/devops_report/components/usage_ping_disabled.vue)23
-rw-r--r--app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js (renamed from app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js)18
-rw-r--r--app/assets/javascripts/analytics/shared/components/daterange.vue121
-rw-r--r--app/assets/javascripts/analytics/shared/components/metric_card.vue80
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue241
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js12
-rw-r--r--app/assets/javascripts/analytics/shared/graphql/projects.query.graphql22
-rw-r--r--app/assets/javascripts/analytics/shared/utils.js4
-rw-r--r--app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue32
-rw-r--r--app/assets/javascripts/api.js10
-rw-r--r--app/assets/javascripts/api/analytics_api.js19
-rw-r--r--app/assets/javascripts/api/constants.js1
-rw-r--r--app/assets/javascripts/api/groups_api.js2
-rw-r--r--app/assets/javascripts/api/projects_api.js2
-rw-r--r--app/assets/javascripts/api/user_api.js10
-rw-r--r--app/assets/javascripts/awards_handler.js8
-rw-r--r--app/assets/javascripts/badges/components/badge.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue2
-rw-r--r--app/assets/javascripts/badges/components/badge_list_row.vue2
-rw-r--r--app/assets/javascripts/batch_comments/components/draft_note.vue8
-rw-r--r--app/assets/javascripts/batch_comments/components/review_bar.vue2
-rw-r--r--app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js28
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_mermaid.js10
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js8
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js6
-rw-r--r--app/assets/javascripts/blob/components/blob_content.vue6
-rw-r--r--app/assets/javascripts/blob/components/blob_edit_content.vue4
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue2
-rw-r--r--app/assets/javascripts/blob/csv/csv_viewer.vue55
-rw-r--r--app/assets/javascripts/blob/csv/index.js17
-rw-r--r--app/assets/javascripts/blob/csv_viewer.js3
-rw-r--r--app/assets/javascripts/blob/file_template_mediator.js11
-rw-r--r--app/assets/javascripts/blob/openapi/index.js6
-rw-r--r--app/assets/javascripts/blob/utils.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js40
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js8
-rw-r--r--app/assets/javascripts/boards/boards_util.js3
-rw-r--r--app/assets/javascripts/boards/components/board_blocked_icon.vue6
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue131
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue182
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue23
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue12
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue31
-rw-r--r--app/assets/javascripts/boards/components/board_list_deprecated.vue4
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue35
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue24
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue83
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue2
-rw-r--r--app/assets/javascripts/boards/components/boards_selector_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/issue_board_filtered_search.vue102
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js6
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue2
-rw-r--r--app/assets/javascripts/boards/components/project_select_deprecated.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_editable_item.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js5
-rw-r--r--app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql1
-rw-r--r--app/assets/javascripts/boards/index.js26
-rw-r--r--app/assets/javascripts/boards/issue_board_filters.js47
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js2
-rw-r--r--app/assets/javascripts/boards/models/list.js14
-rw-r--r--app/assets/javascripts/boards/mount_filtered_search_issue_boards.js31
-rw-r--r--app/assets/javascripts/boards/stores/actions.js29
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js11
-rw-r--r--app/assets/javascripts/boards/stores/getters.js2
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js18
-rw-r--r--app/assets/javascripts/branches/components/delete_branch_button.vue8
-rw-r--r--app/assets/javascripts/branches/divergence_graph.js2
-rw-r--r--app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js56
-rw-r--r--app/assets/javascripts/ci_lint/components/ci_lint.vue6
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js221
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue478
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue662
-rw-r--r--app/assets/javascripts/clusters/components/crossplane_provider_stack.vue93
-rw-r--r--app/assets/javascripts/clusters/components/knative_domain_editor.vue232
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_button.vue36
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue101
-rw-r--r--app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue66
-rw-r--r--app/assets/javascripts/clusters/constants.js57
-rw-r--r--app/assets/javascripts/clusters/services/application_state_machine.js250
-rw-r--r--app/assets/javascripts/clusters/services/clusters_service.js26
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js207
-rw-r--r--app/assets/javascripts/clusters_list/store/actions.js6
-rw-r--r--app/assets/javascripts/code_quality_walkthrough/utils.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js14
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue20
-rw-r--r--app/assets/javascripts/commit_merge_requests.js9
-rw-r--r--app/assets/javascripts/commits.js2
-rw-r--r--app/assets/javascripts/confidential_merge_request/components/project_form_group.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue31
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_image_button.vue110
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_link_button.vue14
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue91
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue28
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/image.vue31
-rw-r--r--app/assets/javascripts/content_editor/extensions/hard_break.js10
-rw-r--r--app/assets/javascripts/content_editor/extensions/horizontal_rule.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js130
-rw-r--r--app/assets/javascripts/content_editor/extensions/link.js33
-rw-r--r--app/assets/javascripts/content_editor/extensions/table.js7
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_row.js51
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js61
-rw-r--r--app/assets/javascripts/content_editor/services/upload_file.js44
-rw-r--r--app/assets/javascripts/content_editor/services/utils.js14
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue36
-rw-r--r--app/assets/javascripts/contributors/stores/actions.js8
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue2
-rw-r--r--app/assets/javascripts/create_cluster/gke_cluster/index.js6
-rw-r--r--app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/filter_bar.vue142
-rw-r--r--app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue32
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue10
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue93
-rw-r--r--app/assets/javascripts/cycle_analytics/constants.js1
-rw-r--r--app/assets/javascripts/cycle_analytics/index.js15
-rw-r--r--app/assets/javascripts/cycle_analytics/store/actions.js62
-rw-r--r--app/assets/javascripts/cycle_analytics/store/getters.js29
-rw-r--r--app/assets/javascripts/cycle_analytics/store/index.js2
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutation_types.js4
-rw-r--r--app/assets/javascripts/cycle_analytics/store/mutations.js38
-rw-r--r--app/assets/javascripts/cycle_analytics/store/state.js5
-rw-r--r--app/assets/javascripts/cycle_analytics/utils.js20
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue18
-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.vue6
-rw-r--r--app/assets/javascripts/design_management/components/design_todo_button.vue24
-rw-r--r--app/assets/javascripts/design_management/pages/design/index.vue4
-rw-r--r--app/assets/javascripts/design_management/utils/tracking.js8
-rw-r--r--app/assets/javascripts/diff.js8
-rw-r--r--app/assets/javascripts/diffs/components/app.vue92
-rw-r--r--app/assets/javascripts/diffs/components/collapsed_files_warning.vue7
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue21
-rw-r--r--app/assets/javascripts/diffs/components/diff_content.vue38
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue89
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue35
-rw-r--r--app/assets/javascripts/diffs/components/diff_line_note_form.vue5
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue556
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js41
-rw-r--r--app/assets/javascripts/diffs/components/diff_view.vue74
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_table_row.vue204
-rw-r--r--app/assets/javascripts/diffs/components/inline_diff_view.vue117
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_table_row.vue310
-rw-r--r--app/assets/javascripts/diffs/components/parallel_diff_view.vue142
-rw-r--r--app/assets/javascripts/diffs/components/pre_renderer.vue84
-rw-r--r--app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js51
-rw-r--r--app/assets/javascripts/diffs/constants.js1
-rw-r--r--app/assets/javascripts/diffs/index.js3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js60
-rw-r--r--app/assets/javascripts/diffs/store/getters.js6
-rw-r--r--app/assets/javascripts/diffs/store/getters_versions_dropdowns.js3
-rw-r--r--app/assets/javascripts/diffs/store/utils.js10
-rw-r--r--app/assets/javascripts/editor/constants.js7
-rw-r--r--app/assets/javascripts/editor/extensions/editor_file_template_ext.js8
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js (renamed from app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js)4
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_extension_base.js (renamed from app/assets/javascripts/editor/extensions/editor_lite_extension_base.js)10
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js8
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js (renamed from app/assets/javascripts/editor/extensions/editor_markdown_ext.js)4
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_webide_ext.js (renamed from app/assets/javascripts/editor/extensions/editor_lite_webide_ext.js)4
-rw-r--r--app/assets/javascripts/editor/source_editor.js (renamed from app/assets/javascripts/editor/editor_lite.js)42
-rw-r--r--app/assets/javascripts/emoji/components/emoji_group.vue1
-rw-r--r--app/assets/javascripts/environments/components/deploy_board.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue2
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue16
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue8
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue6
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js19
-rw-r--r--app/assets/javascripts/error_tracking/components/stacktrace_entry.vue2
-rw-r--r--app/assets/javascripts/error_tracking_settings/store/actions.js1
-rw-r--r--app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue1
-rw-r--r--app/assets/javascripts/feature_flags/components/edit_feature_flag.vue37
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue7
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue68
-rw-r--r--app/assets/javascripts/feature_flags/components/form.vue428
-rw-r--r--app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue2
-rw-r--r--app/assets/javascripts/feature_flags/components/new_feature_flag.vue40
-rw-r--r--app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue2
-rw-r--r--app/assets/javascripts/feature_flags/edit.js7
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/actions.js10
-rw-r--r--app/assets/javascripts/feature_flags/store/edit/mutations.js3
-rw-r--r--app/assets/javascripts/feature_flags/store/helpers.js149
-rw-r--r--app/assets/javascripts/feature_flags/store/index/mutations.js7
-rw-r--r--app/assets/javascripts/feature_flags/store/new/actions.js10
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js8
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js13
-rw-r--r--app/assets/javascripts/filtered_search/available_dropdown_mappings.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_ajax_filter.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_emoji.js2
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_non_user.js2
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js5
-rw-r--r--app/assets/javascripts/filtered_search/visual_token_value.js14
-rw-r--r--app/assets/javascripts/flash.js27
-rw-r--r--app/assets/javascripts/fly_out_nav.js11
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue32
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js3
-rw-r--r--app/assets/javascripts/gpg_badges.js5
-rw-r--r--app/assets/javascripts/grafana_integration/store/actions.js1
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js13
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js5
-rw-r--r--app/assets/javascripts/group.js12
-rw-r--r--app/assets/javascripts/group_label_subscription.js14
-rw-r--r--app/assets/javascripts/group_settings/components/shared_runners_form.vue2
-rw-r--r--app/assets/javascripts/groups/components/app.vue10
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue34
-rw-r--r--app/assets/javascripts/groups/components/groups.vue2
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue2
-rw-r--r--app/assets/javascripts/groups/groups_filterable_list.js9
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide.vue1
-rw-r--r--app/assets/javascripts/ide/components/ide_project_header.vue12
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue2
-rw-r--r--app/assets/javascripts/ide/components/merge_requests/item.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue3
-rw-r--r--app/assets/javascripts/ide/components/preview/navigator.vue6
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue27
-rw-r--r--app/assets/javascripts/ide/components/shared/tokened_input.vue2
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue2
-rw-r--r--app/assets/javascripts/ide/ide_router.js15
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js3
-rw-r--r--app/assets/javascripts/ide/services/index.js2
-rw-r--r--app/assets/javascripts/ide/stores/actions.js32
-rw-r--r--app/assets/javascripts/ide/stores/actions/merge_request.js3
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js21
-rw-r--r--app/assets/javascripts/ide/stores/modules/clientside/actions.js2
-rw-r--r--app/assets/javascripts/ide/stores/modules/commit/actions.js8
-rw-r--r--app/assets/javascripts/ide/stores/utils.js4
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue40
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue26
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue14
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue62
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js2
-rw-r--r--app/assets/javascripts/incidents_settings/incidents_settings_service.js1
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue42
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue79
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue24
-rw-r--r--app/assets/javascripts/invite_members/constants.js6
-rw-r--r--app/assets/javascripts/invite_members/utils/response_message_parser.js65
-rw-r--r--app/assets/javascripts/issuable/components/issuable_by_email.vue4
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue58
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/constants.js17
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js17
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js (renamed from app/assets/javascripts/issuable_bulk_update_actions.js)10
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable_bulk_update_sidebar.js)13
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js (renamed from app/assets/javascripts/issuable_init_bulk_update_sidebar.js)0
-rw-r--r--app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js (renamed from app/assets/javascripts/subscription_select.js)2
-rw-r--r--app/assets/javascripts/issuable_context.js30
-rw-r--r--app/assets/javascripts/issuable_create/components/issuable_form.vue21
-rw-r--r--app/assets/javascripts/issuable_index.js2
-rw-r--r--app/assets/javascripts/issuable_list/components/issuable_list_root.vue2
-rw-r--r--app/assets/javascripts/issuable_show/components/issuable_show_root.vue6
-rw-r--r--app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue23
-rw-r--r--app/assets/javascripts/issuable_sidebar/constants.js1
-rw-r--r--app/assets/javascripts/issue.js12
-rw-r--r--app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql1
-rw-r--r--app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql4
-rw-r--r--app/assets/javascripts/issue_show/services/index.js2
-rw-r--r--app/assets/javascripts/issue_status_select.js27
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue22
-rw-r--r--app/assets/javascripts/issues_list/components/issue_card_time_info.vue2
-rw-r--r--app/assets/javascripts/issues_list/components/issues_list_app.vue208
-rw-r--r--app/assets/javascripts/issues_list/constants.js235
-rw-r--r--app/assets/javascripts/issues_list/index.js12
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues.query.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql26
-rw-r--r--app/assets/javascripts/issues_list/queries/issue.fragment.graphql2
-rw-r--r--app/assets/javascripts/issues_list/queries/search_iterations.query.graphql10
-rw-r--r--app/assets/javascripts/issues_list/queries/search_labels.query.graphql12
-rw-r--r--app/assets/javascripts/issues_list/queries/search_milestones.query.graphql10
-rw-r--r--app/assets/javascripts/issues_list/queries/search_users.query.graphql14
-rw-r--r--app/assets/javascripts/issues_list/utils.js54
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue95
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue134
-rw-r--r--app/assets/javascripts/jira_connect/branches/constants.js2
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql17
-rw-r--r--app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql34
-rw-r--r--app/assets/javascripts/jira_connect/components/groups_list.vue1
-rw-r--r--app/assets/javascripts/jira_import/components/jira_import_form.vue4
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue11
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue6
-rw-r--r--app/assets/javascripts/jobs/components/log/collapsible_section.vue33
-rw-r--r--app/assets/javascripts/jobs/components/log/line_number.vue6
-rw-r--r--app/assets/javascripts/jobs/components/manual_variables_form.vue39
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue2
-rw-r--r--app/assets/javascripts/jobs/constants.js2
-rw-r--r--app/assets/javascripts/jobs/index.js2
-rw-r--r--app/assets/javascripts/jobs/store/actions.js24
-rw-r--r--app/assets/javascripts/jobs/store/mutations.js28
-rw-r--r--app/assets/javascripts/jobs/store/state.js3
-rw-r--r--app/assets/javascripts/jobs/store/utils.js75
-rw-r--r--app/assets/javascripts/jobs/utils.js6
-rw-r--r--app/assets/javascripts/label_manager.js12
-rw-r--r--app/assets/javascripts/labels_select.js16
-rw-r--r--app/assets/javascripts/lib/dompurify.js11
-rw-r--r--app/assets/javascripts/lib/graphql.js52
-rw-r--r--app/assets/javascripts/lib/utils/axios_utils.js3
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js139
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js47
-rw-r--r--app/assets/javascripts/lib/utils/finite_state_machine.js101
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js58
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js76
-rw-r--r--app/assets/javascripts/line_highlighter.js4
-rw-r--r--app/assets/javascripts/locale/index.js47
-rw-r--r--app/assets/javascripts/logs/components/environment_logs.vue8
-rw-r--r--app/assets/javascripts/logs/components/tokens/token_with_loading_state.vue2
-rw-r--r--app/assets/javascripts/main.js9
-rw-r--r--app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue1
-rw-r--r--app/assets/javascripts/members/components/app.vue7
-rw-r--r--app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue20
-rw-r--r--app/assets/javascripts/members/components/members_tabs.vue45
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue19
-rw-r--r--app/assets/javascripts/members/constants.js9
-rw-r--r--app/assets/javascripts/members/utils.js4
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue12
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue2
-rw-r--r--app/assets/javascripts/merge_request.js8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js28
-rw-r--r--app/assets/javascripts/milestone.js8
-rw-r--r--app/assets/javascripts/milestone_select.js17
-rw-r--r--app/assets/javascripts/milestones/components/milestone_combobox.vue4
-rw-r--r--app/assets/javascripts/milestones/milestone_utils.js32
-rw-r--r--app/assets/javascripts/mirrors/mirror_repos.js8
-rw-r--r--app/assets/javascripts/mirrors/ssh_mirror.js6
-rw-r--r--app/assets/javascripts/monitoring/components/alert_widget.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/charts/time_series.vue20
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue34
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_header.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard_panel.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue2
-rw-r--r--app/assets/javascripts/monitoring/components/graph_group.vue2
-rw-r--r--app/assets/javascripts/namespaces/leave_by_url.js12
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue4
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue2
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_sections.vue2
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue60
-rw-r--r--app/assets/javascripts/notes.js5
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_notes.vue25
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue8
-rw-r--r--app/assets/javascripts/notes/components/note_awards_list.vue8
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue1
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue14
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue22
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue13
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js7
-rw-r--r--app/assets/javascripts/notes/stores/actions.js84
-rw-r--r--app/assets/javascripts/notes/stores/getters.js2
-rw-r--r--app/assets/javascripts/notes/utils.js7
-rw-r--r--app/assets/javascripts/notifications/components/custom_notifications_modal.vue6
-rw-r--r--app/assets/javascripts/notifications/components/notifications_dropdown.vue2
-rw-r--r--app/assets/javascripts/operation_settings/store/actions.js1
-rw-r--r--app/assets/javascripts/packages/details/components/app.vue4
-rw-r--r--app/assets/javascripts/packages/list/constants.js8
-rw-r--r--app/assets/javascripts/packages/shared/constants.js2
-rw-r--r--app/assets/javascripts/packages/shared/utils.js4
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue301
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.js26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/constants.js8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/utils.js7
-rw-r--r--app/assets/javascripts/pager.js3
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js41
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/payload_previewer.js6
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js11
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/clusters/destroy/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/index/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/clusters/new/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/clusters/show/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/dev_ops_report/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/identities/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/impersonation_tokens/index.js5
-rw-r--r--app/assets/javascripts/pages/admin/integrations/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/jobs/index/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/keys/index.js4
-rw-r--r--app/assets/javascripts/pages/admin/labels/index/index.js6
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/components/delete_project_modal.vue2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index/index.js8
-rw-r--r--app/assets/javascripts/pages/admin/spam_logs/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/users/index.js67
-rw-r--r--app/assets/javascripts/pages/admin/users/keys/index.js5
-rw-r--r--app/assets/javascripts/pages/dashboard/groups/index/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/milestones/show/index.js8
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js12
-rw-r--r--app/assets/javascripts/pages/explore/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/destroy/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/clusters/new/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/clusters/show/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/milestones/show/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/new/group_path_validator.js21
-rw-r--r--app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue14
-rw-r--r--app/assets/javascripts/pages/profiles/notifications/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/browse/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/file/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/clusters/destroy/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/clusters/show/index.js10
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue14
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue2
-rw-r--r--app/assets/javascripts/pages/projects/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue43
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js8
-rw-r--r--app/assets/javascripts/pages/projects/new/components/app.vue26
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/show/index.js14
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/project.js8
-rw-r--r--app/assets/javascripts/pages/projects/security/configuration/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue12
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js8
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/username_validator.js8
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue18
-rw-r--r--app/assets/javascripts/pages/shared/wikis/index.js6
-rw-r--r--app/assets/javascripts/pages/users/activity_calendar.js8
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js4
-rw-r--r--app/assets/javascripts/performance/constants.js4
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue9
-rw-r--r--app/assets/javascripts/persistent_user_callout.js14
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue6
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue38
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue10
-rw-r--r--app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue8
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue2
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue3
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js10
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql3
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql12
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql4
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql12
-rw-r--r--app/assets/javascripts/pipeline_editor/graphql/resolvers.js24
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue110
-rw-r--r--app/assets/javascripts/pipeline_new/constants.js4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component.vue12
-rw-r--r--app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/graph/stage_column_component.vue40
-rw-r--r--app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js6
-rw-r--r--app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js17
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue28
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue51
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue (renamed from app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue)17
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue64
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue8
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue23
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue46
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue2
-rw-r--r--app/assets/javascripts/pipelines/constants.js3
-rw-r--r--app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js14
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js10
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediator.js6
-rw-r--r--app/assets/javascripts/pipelines/pipeline_shared_client.js1
-rw-r--r--app/assets/javascripts/pipelines/pipelines_index.js4
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue3
-rw-r--r--app/assets/javascripts/profile/profile.js13
-rw-r--r--app/assets/javascripts/project_find_file.js8
-rw-r--r--app/assets/javascripts/project_label_subscription.js8
-rw-r--r--app/assets/javascripts/projects/commit/components/branches_dropdown.vue2
-rw-r--r--app/assets/javascripts/projects/commits/components/author_select.vue5
-rw-r--r--app/assets/javascripts/projects/components/project_delete_button.vue4
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue10
-rw-r--r--app/assets/javascripts/projects/project_new.js21
-rw-r--r--app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue59
-rw-r--r--app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js8
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue7
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue2
-rw-r--r--app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue65
-rw-r--r--app/assets/javascripts/projects/terraform_notification/index.js18
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue6
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js8
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.js6
-rw-r--r--app/assets/javascripts/ref/constants.js3
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue71
-rw-r--r--app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue37
-rw-r--r--app/assets/javascripts/registry/explorer/constants/details.js4
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue9
-rw-r--r--app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue2
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue2
-rw-r--r--app/assets/javascripts/releases/components/app_index_apollo_client.vue6
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql14
-rw-r--r--app/assets/javascripts/releases/graphql/fragments/release_for_editing.fragment.graphql1
-rw-r--r--app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql9
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js1
-rw-r--r--app/assets/javascripts/reports/components/issue_body.js9
-rw-r--r--app/assets/javascripts/reports/components/report_item.vue21
-rw-r--r--app/assets/javascripts/reports/constants.js1
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue (renamed from app/assets/javascripts/repository/components/blob_replace.vue)47
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue67
-rw-r--r--app/assets/javascripts/repository/components/blob_edit.vue (renamed from app/assets/javascripts/repository/components/blob_header_edit.vue)0
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue51
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue3
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js27
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue25
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue151
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue2
-rw-r--r--app/assets/javascripts/repository/components/table/row.vue2
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue25
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue12
-rw-r--r--app/assets/javascripts/repository/constants.js8
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql5
-rw-r--r--app/assets/javascripts/repository/queries/commit.fragment.graphql1
-rw-r--r--app/assets/javascripts/right_sidebar.js40
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue21
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_type_cell.vue4
-rw-r--r--app/assets/javascripts/runner/components/helpers/masked_value.vue60
-rw-r--r--app/assets/javascripts/runner/components/runner_filtered_search_bar.vue141
-rw-r--r--app/assets/javascripts/runner/components/runner_list.vue36
-rw-r--r--app/assets/javascripts/runner/components/runner_manual_setup_help.vue6
-rw-r--r--app/assets/javascripts/runner/components/runner_registration_token_reset.vue10
-rw-r--r--app/assets/javascripts/runner/components/runner_tag.vue27
-rw-r--r--app/assets/javascripts/runner/components/runner_tags.vue13
-rw-r--r--app/assets/javascripts/runner/components/runner_type_help.vue4
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue67
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue91
-rw-r--r--app/assets/javascripts/runner/constants.js9
-rw-r--r--app/assets/javascripts/runner/graphql/get_runner.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/get_runners.query.graphql6
-rw-r--r--app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql (renamed from app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql)0
-rw-r--r--app/assets/javascripts/runner/graphql/runner_details.fragment.graphql13
-rw-r--r--app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql12
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/runner_update.mutation.graphql2
-rw-r--r--app/assets/javascripts/runner/runner_details/runner_details_app.vue34
-rw-r--r--app/assets/javascripts/runner/runner_details/runner_update_form_utils.js38
-rw-r--r--app/assets/javascripts/runner/runner_list/index.js3
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_list_app.vue35
-rw-r--r--app/assets/javascripts/runner/runner_list/runner_search_utils.js17
-rw-r--r--app/assets/javascripts/runner/sentry_utils.js20
-rw-r--r--app/assets/javascripts/search/store/actions.js45
-rw-r--r--app/assets/javascripts/search/store/constants.js7
-rw-r--r--app/assets/javascripts/search/store/getters.js9
-rw-r--r--app/assets/javascripts/search/store/index.js2
-rw-r--r--app/assets/javascripts/search/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/search/store/mutations.js3
-rw-r--r--app/assets/javascripts/search/store/state.js6
-rw-r--r--app/assets/javascripts/search/store/utils.js80
-rw-r--r--app/assets/javascripts/search/topbar/components/group_filter.vue12
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue12
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue41
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue8
-rw-r--r--app/assets/javascripts/search_autocomplete.js10
-rw-r--r--app/assets/javascripts/search_autocomplete_utils.js19
-rw-r--r--app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue41
-rw-r--r--app/assets/javascripts/security_configuration/components/configuration_table.vue2
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js58
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue3
-rw-r--r--app/assets/javascripts/security_configuration/components/redesigned_app.vue82
-rw-r--r--app/assets/javascripts/security_configuration/components/section_layout.vue6
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_secret_detection.mutation.graphql6
-rw-r--r--app/assets/javascripts/security_configuration/index.js81
-rw-r--r--app/assets/javascripts/security_configuration/utils.js4
-rw-r--r--app/assets/javascripts/self_monitor/components/self_monitor_form.vue2
-rw-r--r--app/assets/javascripts/sentry/index.js1
-rw-r--r--app/assets/javascripts/sentry/sentry_config.js4
-rw-r--r--app/assets/javascripts/service_ping_consent.js (renamed from app/assets/javascripts/usage_ping_consent.js)15
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignee_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue23
-rw-r--r--app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue6
-rw-r--r--app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue44
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/report.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue195
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/todo.vue2
-rw-r--r--app/assets/javascripts/sidebar/constants.js50
-rw-r--r--app/assets/javascripts/sidebar/lib/sidebar_move_issue.js9
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js78
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_reference.query.graphql10
-rw-r--r--app/assets/javascripts/sidebar/queries/epic_todo.query.graphql14
-rw-r--r--app/assets/javascripts/sidebar/queries/issue_todo.query.graphql14
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql14
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql14
-rw-r--r--app/assets/javascripts/sidebar/queries/milestone.fragment.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql1
-rw-r--r--app/assets/javascripts/sidebar/queries/project_milestones.query.graphql8
-rw-r--r--app/assets/javascripts/sidebar/queries/todo_create.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/todo_mark_done.mutation.graphql9
-rw-r--r--app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql3
-rw-r--r--app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql17
-rw-r--r--app/assets/javascripts/sidebar/services/sidebar_service.js3
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js8
-rw-r--r--app/assets/javascripts/smart_interval.js29
-rw-r--r--app/assets/javascripts/snippets/components/edit.vue6
-rw-r--r--app/assets/javascripts/snippets/components/embed_dropdown.vue2
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue6
-rw-r--r--app/assets/javascripts/snippets/components/snippet_header.vue2
-rw-r--r--app/assets/javascripts/sortable/sortable_config.js1
-rw-r--r--app/assets/javascripts/star.js8
-rw-r--r--app/assets/javascripts/static_site_editor/components/edit_drawer.vue2
-rw-r--r--app/assets/javascripts/static_site_editor/constants.js5
-rw-r--r--app/assets/javascripts/static_site_editor/image_repository.js7
-rw-r--r--app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue12
-rw-r--r--app/assets/javascripts/static_site_editor/services/submit_content_changes.js8
-rw-r--r--app/assets/javascripts/task_list.js6
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue4
-rw-r--r--app/assets/javascripts/terraform/components/terraform_list.vue2
-rw-r--r--app/assets/javascripts/toggle_buttons.js6
-rw-r--r--app/assets/javascripts/token_access/components/token_access.vue206
-rw-r--r--app/assets/javascripts/token_access/components/token_projects_table.vue81
-rw-r--r--app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql5
-rw-r--r--app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql5
-rw-r--r--app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql8
-rw-r--r--app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql7
-rw-r--r--app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql12
-rw-r--r--app/assets/javascripts/token_access/index.js31
-rw-r--r--app/assets/javascripts/tracking/index.js21
-rw-r--r--app/assets/javascripts/user_lists/components/user_lists.vue8
-rw-r--r--app/assets/javascripts/user_popovers.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/deployment/memory_usage.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue39
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue37
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue27
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue6
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js8
-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.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue29
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_project_avatar/default.vue (renamed from app/assets/javascripts/vue_shared/components/project_avatar/default.vue)1
-rw-r--r--app/assets/javascripts/vue_shared/components/deprecated_project_avatar/image.vue (renamed from app/assets/javascripts/vue_shared/components/project_avatar/image.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/dismissible_alert.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/file_finder/index.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js3
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js26
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue77
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/emoji_token.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/label_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_row.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestions.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar.stories.js30
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_list_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/select2_select.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue14
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue43
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql15
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js23
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue56
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js5
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue (renamed from app/assets/javascripts/vue_shared/components/editor_lite.vue)8
-rw-r--r--app/assets/javascripts/vue_shared/components/todo_button.vue28
-rw-r--r--app/assets/javascripts/vue_shared/components/user_popover/user_popover.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue23
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue10
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue14
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue12
-rw-r--r--app/assets/javascripts/vue_shared/plugins/global_toast.js2
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue35
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue13
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue2
-rw-r--r--app/assets/javascripts/vuex_shared/bindings.js6
-rw-r--r--app/assets/javascripts/whats_new/components/app.vue2
759 files changed, 10388 insertions, 7806 deletions
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index b671d038ce8..f45af5fe08e 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -42,7 +42,7 @@ export default class Activities {
}
updateTooltips() {
- localTimeAgo($('.js-timeago', '.content_list'));
+ localTimeAgo(document.querySelectorAll('.content_list .js-timeago'));
}
reloadActivities() {
diff --git a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
index a357d5d2f1f..cfa2f4b8762 100644
--- a/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
+++ b/app/assets/javascripts/admin/application_settings/setup_metrics_and_profiling.js
@@ -1,3 +1,4 @@
+import initSetHelperText from '~/pages/admin/application_settings/metrics_and_profiling/usage_statistics';
import PayloadPreviewer from '~/pages/admin/application_settings/payload_previewer';
export default () => {
@@ -5,3 +6,5 @@ export default () => {
new PayloadPreviewer(trigger).init();
});
};
+
+initSetHelperText();
diff --git a/app/assets/javascripts/admin/users/components/actions/activate.vue b/app/assets/javascripts/admin/users/components/actions/activate.vue
index 99c260bf11e..74e9c60a57b 100644
--- a/app/assets/javascripts/admin/users/components/actions/activate.vue
+++ b/app/assets/javascripts/admin/users/components/actions/activate.vue
@@ -1,6 +1,16 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|Reactivating a user will:')}</p>
+ <ul>
+ <li>${s__('AdminUsers|Restore user access to the account, including web, Git and API.')}</li>
+ </ul>
+ <p>${s__('AdminUsers|You can always deactivate their account again if needed.')}</p>
+`;
export default {
components: {
@@ -25,9 +35,14 @@ export default {
title: sprintf(s__('AdminUsers|Activate user %{username}?'), {
username: this.username,
}),
- message: s__('AdminUsers|You can always deactivate their account again if needed.'),
- okVariant: 'confirm',
- okTitle: s__('AdminUsers|Activate'),
+ messageHtml,
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.activate,
+ attributes: [{ variant: 'confirm' }],
+ },
}),
};
},
@@ -36,9 +51,7 @@ export default {
</script>
<template>
- <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
- <gl-dropdown-item>
- <slot></slot>
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/approve.vue b/app/assets/javascripts/admin/users/components/actions/approve.vue
index 6fc43c246ea..77a9be8eec2 100644
--- a/app/assets/javascripts/admin/users/components/actions/approve.vue
+++ b/app/assets/javascripts/admin/users/components/actions/approve.vue
@@ -1,21 +1,60 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|Approved users can:')}</p>
+ <ul>
+ <li>${s__('AdminUsers|Log in')}</li>
+ <li>${s__('AdminUsers|Access Git repositories')}</li>
+ <li>${s__('AdminUsers|Access the API')}</li>
+ <li>${s__('AdminUsers|Be added to groups and projects')}</li>
+ </ul>
+`;
export default {
components: {
GlDropdownItem,
},
props: {
+ username: {
+ type: String,
+ required: true,
+ },
path: {
type: String,
required: true,
},
},
+ computed: {
+ attributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Approve user %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.approve,
+ attributes: [{ variant: 'confirm', 'data-qa-selector': 'approve_user_confirm_button' }],
+ },
+ messageHtml,
+ }),
+ 'data-qa-selector': 'approve_user_button',
+ };
+ },
+ },
};
</script>
<template>
- <gl-dropdown-item :href="path" data-method="put">
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...attributes }">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/ban.vue b/app/assets/javascripts/admin/users/components/actions/ban.vue
new file mode 100644
index 00000000000..4e9cefbfdd7
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/ban.vue
@@ -0,0 +1,69 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|When banned, users:')}</p>
+ <ul>
+ <li>${s__("AdminUsers|Can't log in.")}</li>
+ <li>${s__("AdminUsers|Can't access Git repositories.")}</li>
+ </ul>
+ <p>${s__('AdminUsers|You can unban their account in the future. Their data remains intact.')}</p>
+ <p>${sprintf(
+ s__('AdminUsers|Learn more about %{link_start}banned users.%{link_end}'),
+ {
+ link_start: `<a href="${helpPagePath('user/admin_area/moderate_users', {
+ anchor: 'ban-a-user',
+ })}" target="_blank">`,
+ link_end: '</a>',
+ },
+ false,
+ )}</p>
+`;
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Ban user %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.ban,
+ attributes: [{ variant: 'confirm' }],
+ },
+ messageHtml,
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/block.vue b/app/assets/javascripts/admin/users/components/actions/block.vue
index 68dfefe14c2..03557008a89 100644
--- a/app/assets/javascripts/admin/users/components/actions/block.vue
+++ b/app/assets/javascripts/admin/users/components/actions/block.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
const messageHtml = `
@@ -11,6 +12,7 @@ const messageHtml = `
<li>${s__('AdminUsers|Personal projects will be left')}</li>
<li>${s__('AdminUsers|Owned groups will be left')}</li>
</ul>
+ <p>${s__('AdminUsers|You can always unblock their account, their data will remain intact.')}</p>
`;
export default {
@@ -34,8 +36,13 @@ export default {
'data-method': 'put',
'data-modal-attributes': JSON.stringify({
title: sprintf(s__('AdminUsers|Block user %{username}?'), { username: this.username }),
- okVariant: 'confirm',
- okTitle: s__('AdminUsers|Block'),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.block,
+ attributes: [{ variant: 'confirm' }],
+ },
messageHtml,
}),
};
@@ -45,9 +52,7 @@ export default {
</script>
<template>
- <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
- <gl-dropdown-item>
- <slot></slot>
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/deactivate.vue b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
index 7e0c17ba296..640c8fefc20 100644
--- a/app/assets/javascripts/admin/users/components/actions/deactivate.vue
+++ b/app/assets/javascripts/admin/users/components/actions/deactivate.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
const messageHtml = `
@@ -16,6 +17,9 @@ const messageHtml = `
)}</li>
<li>${s__('AdminUsers|Personal projects, group and user history will be left intact')}</li>
</ul>
+ <p>${s__(
+ 'AdminUsers|You can always re-activate their account, their data will remain intact.',
+ )}</p>
`;
export default {
@@ -41,8 +45,13 @@ export default {
title: sprintf(s__('AdminUsers|Deactivate user %{username}?'), {
username: this.username,
}),
- okVariant: 'confirm',
- okTitle: s__('AdminUsers|Deactivate'),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.deactivate,
+ attributes: [{ variant: 'confirm' }],
+ },
messageHtml,
}),
};
@@ -52,9 +61,7 @@ export default {
</script>
<template>
- <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
- <gl-dropdown-item>
- <slot></slot>
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/index.js b/app/assets/javascripts/admin/users/components/actions/index.js
index e34b01346b9..4e63a85df89 100644
--- a/app/assets/javascripts/admin/users/components/actions/index.js
+++ b/app/assets/javascripts/admin/users/components/actions/index.js
@@ -1,20 +1,24 @@
import Activate from './activate.vue';
import Approve from './approve.vue';
+import Ban from './ban.vue';
import Block from './block.vue';
import Deactivate from './deactivate.vue';
import Delete from './delete.vue';
import DeleteWithContributions from './delete_with_contributions.vue';
import Reject from './reject.vue';
+import Unban from './unban.vue';
import Unblock from './unblock.vue';
import Unlock from './unlock.vue';
export default {
Activate,
Approve,
+ Ban,
Block,
Deactivate,
Delete,
DeleteWithContributions,
+ Unban,
Unblock,
Unlock,
Reject,
diff --git a/app/assets/javascripts/admin/users/components/actions/reject.vue b/app/assets/javascripts/admin/users/components/actions/reject.vue
index a80c1ff5458..901306455fa 100644
--- a/app/assets/javascripts/admin/users/components/actions/reject.vue
+++ b/app/assets/javascripts/admin/users/components/actions/reject.vue
@@ -1,21 +1,70 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `
+ <p>${s__('AdminUsers|Rejected users:')}</p>
+ <ul>
+ <li>${s__('AdminUsers|Cannot sign in or access instance information')}</li>
+ <li>${s__('AdminUsers|Will be deleted')}</li>
+ </ul>
+ <p>${sprintf(
+ s__(
+ 'AdminUsers|For more information, please refer to the %{link_start}user account deletion documentation.%{link_end}',
+ ),
+ {
+ link_start: `<a href="${helpPagePath('user/profile/account/delete_account', {
+ anchor: 'associated-records',
+ })}" target="_blank">`,
+ link_end: '</a>',
+ },
+ false,
+ )}</p>
+`;
export default {
components: {
GlDropdownItem,
},
props: {
+ username: {
+ type: String,
+ required: true,
+ },
path: {
type: String,
required: true,
},
},
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'delete',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Reject user %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.reject,
+ attributes: [{ variant: 'danger' }],
+ },
+ messageHtml,
+ }),
+ };
+ },
+ },
};
</script>
<template>
- <gl-dropdown-item :href="path" data-method="delete">
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
<slot></slot>
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unban.vue b/app/assets/javascripts/admin/users/components/actions/unban.vue
new file mode 100644
index 00000000000..8083e26177e
--- /dev/null
+++ b/app/assets/javascripts/admin/users/components/actions/unban.vue
@@ -0,0 +1,53 @@
+<script>
+import { GlDropdownItem } from '@gitlab/ui';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
+
+// TODO: To be replaced with <template> content in https://gitlab.com/gitlab-org/gitlab/-/issues/320922
+const messageHtml = `<p>${s__(
+ 'AdminUsers|You can ban their account in the future if necessary.',
+)}</p>`;
+
+export default {
+ components: {
+ GlDropdownItem,
+ },
+ props: {
+ username: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ modalAttributes() {
+ return {
+ 'data-path': this.path,
+ 'data-method': 'put',
+ 'data-modal-attributes': JSON.stringify({
+ title: sprintf(s__('AdminUsers|Unban user %{username}?'), {
+ username: this.username,
+ }),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.unban,
+ attributes: [{ variant: 'confirm' }],
+ },
+ messageHtml,
+ }),
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
+</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unblock.vue b/app/assets/javascripts/admin/users/components/actions/unblock.vue
index d4c0f900c94..7de6653e0cd 100644
--- a/app/assets/javascripts/admin/users/components/actions/unblock.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unblock.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
export default {
components: {
@@ -24,8 +25,13 @@ export default {
'data-modal-attributes': JSON.stringify({
title: sprintf(s__('AdminUsers|Unblock user %{username}?'), { username: this.username }),
message: s__('AdminUsers|You can always block their account again if needed.'),
- okVariant: 'confirm',
- okTitle: s__('AdminUsers|Unblock'),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.unblock,
+ attributes: [{ variant: 'confirm' }],
+ },
}),
};
},
@@ -34,9 +40,7 @@ export default {
</script>
<template>
- <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
- <gl-dropdown-item>
- <slot></slot>
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/admin/users/components/actions/unlock.vue b/app/assets/javascripts/admin/users/components/actions/unlock.vue
index 294aaade7c1..10d4fb06d61 100644
--- a/app/assets/javascripts/admin/users/components/actions/unlock.vue
+++ b/app/assets/javascripts/admin/users/components/actions/unlock.vue
@@ -1,6 +1,7 @@
<script>
import { GlDropdownItem } from '@gitlab/ui';
import { sprintf, s__, __ } from '~/locale';
+import { I18N_USER_ACTIONS } from '../../constants';
export default {
components: {
@@ -24,8 +25,13 @@ export default {
'data-modal-attributes': JSON.stringify({
title: sprintf(s__('AdminUsers|Unlock user %{username}?'), { username: this.username }),
message: __('Are you sure?'),
- okVariant: 'confirm',
- okTitle: s__('AdminUsers|Unlock'),
+ actionCancel: {
+ text: __('Cancel'),
+ },
+ actionPrimary: {
+ text: I18N_USER_ACTIONS.unlock,
+ attributes: [{ variant: 'confirm' }],
+ },
}),
};
},
@@ -34,9 +40,7 @@ export default {
</script>
<template>
- <div class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
- <gl-dropdown-item>
- <slot></slot>
- </gl-dropdown-item>
- </div>
+ <gl-dropdown-item button-class="js-confirm-modal-button" v-bind="{ ...modalAttributes }">
+ <slot></slot>
+ </gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
index a3b78da6ef5..413163c8536 100644
--- a/app/assets/javascripts/pages/admin/users/components/delete_user_modal.vue
+++ b/app/assets/javascripts/admin/users/components/modals/delete_user_modal.vue
@@ -58,7 +58,7 @@ export default {
},
computed: {
modalTitle() {
- return sprintf(this.title, { username: this.username });
+ return sprintf(this.title, { username: this.username }, false);
},
secondaryButtonLabel() {
return s__('AdminUsers|Block user');
@@ -112,7 +112,7 @@ export default {
</gl-sprintf>
</p>
- <oncall-schedules-list v-if="schedules.length" :schedules="schedules" />
+ <oncall-schedules-list v-if="schedules.length" :schedules="schedules" :user-name="username" />
<p>
<gl-sprintf :message="s__('AdminUsers|To confirm, type %{username}')">
diff --git a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue b/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue
index 1dfea3f1e7b..1dfea3f1e7b 100644
--- a/app/assets/javascripts/pages/admin/users/components/user_modal_manager.vue
+++ b/app/assets/javascripts/admin/users/components/modals/user_modal_manager.vue
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index b782526e6be..c076e0bedf0 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -5,6 +5,7 @@ import {
GlDropdownItem,
GlDropdownSectionHeader,
GlDropdownDivider,
+ GlTooltipDirective,
} from '@gitlab/ui';
import { convertArrayToCamelCase } from '~/lib/utils/common_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
@@ -21,6 +22,9 @@ export default {
GlDropdownDivider,
...Actions,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
user: {
type: Object,
@@ -30,6 +34,11 @@ export default {
type: Object,
required: true,
},
+ showButtonLabels: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
userActions() {
@@ -56,6 +65,13 @@ export default {
userPaths() {
return generateUserPaths(this.paths, this.user.username);
},
+ editButtonAttrs() {
+ return {
+ 'data-testid': 'edit',
+ icon: 'pencil-square',
+ href: this.userPaths.edit,
+ };
+ },
},
methods: {
isLdapAction(action) {
@@ -70,51 +86,68 @@ export default {
</script>
<template>
- <div class="gl-display-flex gl-justify-content-end" :data-testid="`user-actions-${user.id}`">
- <gl-button v-if="hasEditAction" data-testid="edit" :href="userPaths.edit">{{
- $options.i18n.edit
- }}</gl-button>
+ <div
+ class="gl-display-flex gl-justify-content-end gl-my-n2 gl-mx-n2"
+ :data-testid="`user-actions-${user.id}`"
+ >
+ <div v-if="hasEditAction" class="gl-p-2">
+ <gl-button v-if="showButtonLabels" v-bind="editButtonAttrs">{{
+ $options.i18n.edit
+ }}</gl-button>
+ <gl-button
+ v-else
+ v-gl-tooltip="$options.i18n.edit"
+ v-bind="editButtonAttrs"
+ :aria-label="$options.i18n.edit"
+ />
+ </div>
- <gl-dropdown
- v-if="hasDropdownActions"
- data-testid="dropdown-toggle"
- right
- class="gl-ml-2"
- icon="settings"
- >
- <gl-dropdown-section-header>{{ $options.i18n.settings }}</gl-dropdown-section-header>
+ <div v-if="hasDropdownActions" class="gl-p-2">
+ <gl-dropdown
+ data-testid="dropdown-toggle"
+ right
+ :text="$options.i18n.userAdministration"
+ :text-sr-only="!showButtonLabels"
+ icon="settings"
+ data-qa-selector="user_actions_dropdown_toggle"
+ :data-qa-username="user.username"
+ >
+ <gl-dropdown-section-header>{{
+ $options.i18n.userAdministration
+ }}</gl-dropdown-section-header>
- <template v-for="action in dropdownSafeActions">
- <component
- :is="getActionComponent(action)"
- v-if="getActionComponent(action)"
- :key="action"
- :path="userPaths[action]"
- :username="user.name"
- :data-testid="action"
- >
- {{ $options.i18n[action] }}
- </component>
- <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
- {{ $options.i18n[action] }}
- </gl-dropdown-item>
- </template>
+ <template v-for="action in dropdownSafeActions">
+ <component
+ :is="getActionComponent(action)"
+ v-if="getActionComponent(action)"
+ :key="action"
+ :path="userPaths[action]"
+ :username="user.name"
+ :data-testid="action"
+ >
+ {{ $options.i18n[action] }}
+ </component>
+ <gl-dropdown-item v-else-if="isLdapAction(action)" :key="action" :data-testid="action">
+ {{ $options.i18n[action] }}
+ </gl-dropdown-item>
+ </template>
- <gl-dropdown-divider v-if="hasDeleteActions" />
+ <gl-dropdown-divider v-if="hasDeleteActions" />
- <template v-for="action in dropdownDeleteActions">
- <component
- :is="getActionComponent(action)"
- v-if="getActionComponent(action)"
- :key="action"
- :paths="userPaths"
- :username="user.name"
- :oncall-schedules="user.oncallSchedules"
- :data-testid="`delete-${action}`"
- >
- {{ $options.i18n[action] }}
- </component>
- </template>
- </gl-dropdown>
+ <template v-for="action in dropdownDeleteActions">
+ <component
+ :is="getActionComponent(action)"
+ v-if="getActionComponent(action)"
+ :key="action"
+ :paths="userPaths"
+ :username="user.name"
+ :oncall-schedules="user.oncallSchedules"
+ :data-testid="`delete-${action}`"
+ >
+ {{ $options.i18n[action] }}
+ </component>
+ </template>
+ </gl-dropdown>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/admin/users/constants.js b/app/assets/javascripts/admin/users/constants.js
index c55edefe607..4636c8705a5 100644
--- a/app/assets/javascripts/admin/users/constants.js
+++ b/app/assets/javascripts/admin/users/constants.js
@@ -6,7 +6,7 @@ export const LENGTH_OF_USER_NOTE_TOOLTIP = 100;
export const I18N_USER_ACTIONS = {
edit: __('Edit'),
- settings: __('Settings'),
+ userAdministration: s__('AdminUsers|User administration'),
unlock: __('Unlock'),
block: s__('AdminUsers|Block'),
unblock: s__('AdminUsers|Unblock'),
@@ -17,4 +17,12 @@ export const I18N_USER_ACTIONS = {
ldapBlocked: s__('AdminUsers|Cannot unblock LDAP blocked users'),
delete: s__('AdminUsers|Delete user'),
deleteWithContributions: s__('AdminUsers|Delete user and contributions'),
+ ban: s__('AdminUsers|Ban user'),
+ unban: s__('AdminUsers|Unban user'),
};
+
+export const CONFIRM_DELETE_BUTTON_SELECTOR = '.js-delete-user-modal-button';
+
+export const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
+
+export const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
diff --git a/app/assets/javascripts/admin/users/index.js b/app/assets/javascripts/admin/users/index.js
index 54c8edc080b..852b253d25a 100644
--- a/app/assets/javascripts/admin/users/index.js
+++ b/app/assets/javascripts/admin/users/index.js
@@ -2,7 +2,15 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import csrf from '~/lib/utils/csrf';
import AdminUsersApp from './components/app.vue';
+import ModalManager from './components/modals/user_modal_manager.vue';
+import UserActions from './components/user_actions.vue';
+import {
+ CONFIRM_DELETE_BUTTON_SELECTOR,
+ MODAL_TEXTS_CONTAINER_SELECTOR,
+ MODAL_MANAGER_SELECTOR,
+} from './constants';
Vue.use(VueApollo);
@@ -10,22 +18,71 @@ const apolloProvider = new VueApollo({
defaultClient: createDefaultClient({}, { assumeImmutableResults: true }),
});
-export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) => {
+const initApp = (el, component, userPropKey, props = {}) => {
if (!el) {
return false;
}
- const { users, paths } = el.dataset;
+ const { [userPropKey]: user, paths } = el.dataset;
return new Vue({
el,
apolloProvider,
render: (createElement) =>
- createElement(AdminUsersApp, {
+ createElement(component, {
props: {
- users: convertObjectPropsToCamelCase(JSON.parse(users), { deep: true }),
+ [userPropKey]: convertObjectPropsToCamelCase(JSON.parse(user), { deep: true }),
paths: convertObjectPropsToCamelCase(JSON.parse(paths)),
+ ...props,
},
}),
});
};
+
+export const initAdminUsersApp = (el = document.querySelector('#js-admin-users-app')) =>
+ initApp(el, AdminUsersApp, 'users');
+
+export const initAdminUserActions = (el = document.querySelector('#js-admin-user-actions')) =>
+ initApp(el, UserActions, 'user', { showButtonLabels: true });
+
+export const initDeleteUserModals = () => {
+ const modalsMountElement = document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR);
+
+ if (!modalsMountElement) {
+ return;
+ }
+
+ const modalConfiguration = Array.from(modalsMountElement.children).reduce((accumulator, node) => {
+ const { modal, ...config } = node.dataset;
+
+ return {
+ ...accumulator,
+ [modal]: {
+ title: node.dataset.title,
+ ...config,
+ content: node.innerHTML,
+ },
+ };
+ }, {});
+
+ // eslint-disable-next-line no-new
+ new Vue({
+ el: MODAL_MANAGER_SELECTOR,
+ functional: true,
+ methods: {
+ show(...args) {
+ this.$refs.manager.show(...args);
+ },
+ },
+ render(h) {
+ return h(ModalManager, {
+ ref: 'manager',
+ props: {
+ selector: CONFIRM_DELETE_BUTTON_SELECTOR,
+ modalConfiguration,
+ csrfToken: csrf.token,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/analytics/devops_report/components/usage_ping_disabled.vue b/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
index c0ad814172d..7c14cf3767f 100644
--- a/app/assets/javascripts/analytics/devops_report/components/usage_ping_disabled.vue
+++ b/app/assets/javascripts/analytics/devops_report/components/service_ping_disabled.vue
@@ -25,28 +25,33 @@ export default {
};
</script>
<template>
- <gl-empty-state class="js-empty-state" :title="__('Usage ping is off')" :svg-path="svgPath">
+ <gl-empty-state :title="s__('ServicePing|Service ping is off')" :svg-path="svgPath">
<template #description>
<gl-sprintf
v-if="!isAdmin"
:message="
- __(
- 'To view instance-level analytics, ask an admin to turn on %{docLinkStart}usage ping%{docLinkEnd}.',
+ s__(
+ 'ServicePing|To view instance-level analytics, ask an admin to turn on %{docLinkStart}service ping%{docLinkEnd}.',
)
"
>
<template #docLink="{ content }">
- <gl-link :href="docsLink" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="docsLink" target="_blank" data-testid="docs-link">{{ content }}</gl-link>
</template>
</gl-sprintf>
- <template v-else
- ><p>
- {{ __('Turn on usage ping to review instance-level analytics.') }}
+ <template v-else>
+ <p>
+ {{ s__('ServicePing|Turn on service ping to review instance-level analytics.') }}
</p>
- <gl-button category="primary" variant="success" :href="primaryButtonPath">
- {{ __('Turn on usage ping') }}</gl-button
+ <gl-button
+ category="primary"
+ variant="success"
+ :href="primaryButtonPath"
+ data-testid="power-on-button"
>
+ {{ s__('ServicePing|Turn on service ping') }}
+ </gl-button>
</template>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
index 0131407e723..63b36f35247 100644
--- a/app/assets/javascripts/analytics/devops_report/devops_score_disabled_usage_ping.js
+++ b/app/assets/javascripts/analytics/devops_report/devops_score_disabled_service_ping.js
@@ -1,27 +1,33 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import UserCallout from '~/user_callout';
-import UsagePingDisabled from './components/usage_ping_disabled.vue';
+import ServicePingDisabled from './components/service_ping_disabled.vue';
export default () => {
// eslint-disable-next-line no-new
new UserCallout();
- const emptyStateContainer = document.getElementById('js-devops-usage-ping-disabled');
+ const emptyStateContainer = document.getElementById('js-devops-service-ping-disabled');
if (!emptyStateContainer) return false;
- const { emptyStateSvgPath, enableUsagePingLink, docsLink, isAdmin } = emptyStateContainer.dataset;
+ const {
+ isAdmin,
+ emptyStateSvgPath,
+ enableServicePingPath,
+ docsLink,
+ } = emptyStateContainer.dataset;
return new Vue({
el: emptyStateContainer,
provide: {
- isAdmin: Boolean(isAdmin),
+ isAdmin: parseBoolean(isAdmin),
svgPath: emptyStateSvgPath,
- primaryButtonPath: enableUsagePingLink,
+ primaryButtonPath: enableServicePingPath,
docsLink,
},
render(h) {
- return h(UsagePingDisabled);
+ return h(ServicePingDisabled);
},
});
};
diff --git a/app/assets/javascripts/analytics/shared/components/daterange.vue b/app/assets/javascripts/analytics/shared/components/daterange.vue
new file mode 100644
index 00000000000..a5b9c40b9c9
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/daterange.vue
@@ -0,0 +1,121 @@
+<script>
+import { GlDaterangePicker, GlSprintf, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { getDayDifference } from '~/lib/utils/datetime_utility';
+import { __, sprintf } from '~/locale';
+import { OFFSET_DATE_BY_ONE } from '../constants';
+
+export default {
+ components: {
+ GlDaterangePicker,
+ GlSprintf,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ show: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ startDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ endDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ minDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ maxDate: {
+ type: Date,
+ required: false,
+ default() {
+ return new Date();
+ },
+ },
+ maxDateRange: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ includeSelectedDate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ maxDateRangeTooltip: sprintf(
+ __(
+ 'Showing data for workflow items created in this date range. Date range cannot exceed %{maxDateRange} days.',
+ ),
+ {
+ maxDateRange: this.maxDateRange,
+ },
+ ),
+ };
+ },
+ computed: {
+ dateRange: {
+ get() {
+ return { startDate: this.startDate, endDate: this.endDate };
+ },
+ set({ startDate, endDate }) {
+ this.$emit('change', { startDate, endDate });
+ },
+ },
+ numberOfDays() {
+ const dayDifference = getDayDifference(this.startDate, this.endDate);
+ return this.includeSelectedDate ? dayDifference + OFFSET_DATE_BY_ONE : dayDifference;
+ },
+ },
+};
+</script>
+<template>
+ <div
+ v-if="show"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row align-items-lg-center justify-content-lg-end"
+ >
+ <gl-daterange-picker
+ v-model="dateRange"
+ class="d-flex flex-column flex-lg-row"
+ :default-start-date="startDate"
+ :default-end-date="endDate"
+ :default-min-date="minDate"
+ :max-date-range="maxDateRange"
+ :default-max-date="maxDate"
+ :same-day-selection="includeSelectedDate"
+ theme="animate-picker"
+ start-picker-class="js-daterange-picker-from gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-lg-align-items-center gl-lg-mr-3 gl-mb-2 gl-lg-mb-0"
+ end-picker-class="js-daterange-picker-to d-flex flex-column flex-lg-row align-items-lg-center"
+ label-class="gl-mb-2 gl-lg-mb-0"
+ />
+ <div
+ v-if="maxDateRange"
+ class="daterange-indicator d-flex flex-row flex-lg-row align-items-flex-start align-items-lg-center"
+ >
+ <span class="number-of-days pl-2 pr-1">
+ <gl-sprintf :message="n__('1 day selected', '%d days selected', numberOfDays)">
+ <template #numberOfDays>{{ numberOfDays }}</template>
+ </gl-sprintf>
+ </span>
+ <gl-icon
+ v-gl-tooltip
+ data-testid="helper-icon"
+ :title="maxDateRangeTooltip"
+ name="question"
+ :size="14"
+ class="text-secondary"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/components/metric_card.vue b/app/assets/javascripts/analytics/shared/components/metric_card.vue
deleted file mode 100644
index e6e12821bec..00000000000
--- a/app/assets/javascripts/analytics/shared/components/metric_card.vue
+++ /dev/null
@@ -1,80 +0,0 @@
-<script>
-import {
- GlCard,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlLink,
- GlIcon,
- GlTooltipDirective,
-} from '@gitlab/ui';
-
-export default {
- name: 'MetricCard',
- components: {
- GlCard,
- GlSkeletonLoading,
- GlLink,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- metrics: {
- type: Array,
- required: true,
- },
- isLoading: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- valueText(metric) {
- const { value = null, unit = null } = metric;
- if (!value || value === '-') return '-';
- return unit && value ? `${value} ${unit}` : value;
- },
- },
-};
-</script>
-<template>
- <gl-card class="gl-mb-5">
- <template #header>
- <strong ref="title">{{ title }}</strong>
- </template>
- <template #default>
- <gl-skeleton-loading v-if="isLoading" class="gl-h-auto gl-py-3" />
- <div v-else ref="metricsWrapper" class="gl-display-flex">
- <div
- v-for="metric in metrics"
- :key="metric.key"
- ref="metricItem"
- class="js-metric-card-item gl-flex-grow-1 gl-text-center"
- >
- <gl-link v-if="metric.link" :href="metric.link">
- <h3 class="gl-my-2 gl-text-blue-700">{{ valueText(metric) }}</h3>
- </gl-link>
- <h3 v-else class="gl-my-2">{{ valueText(metric) }}</h3>
- <p class="text-secondary gl-font-sm gl-mb-2">
- {{ metric.label }}
- <span v-if="metric.tooltipText">
- &nbsp;
- <gl-icon
- v-gl-tooltip="{ title: metric.tooltipText }"
- :size="14"
- class="gl-vertical-align-middle"
- name="question"
- data-testid="tooltip"
- />
- </span>
- </p>
- </div>
- </div>
- </template>
- </gl-card>
-</template>
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
new file mode 100644
index 00000000000..a490111e13b
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -0,0 +1,241 @@
+<script>
+import {
+ GlIcon,
+ GlLoadingIcon,
+ GlAvatar,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+} from '@gitlab/ui';
+import { debounce } from 'lodash';
+import { filterBySearchTerm } from '~/analytics/shared/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { n__, s__, __ } from '~/locale';
+import getProjects from '../graphql/projects.query.graphql';
+
+export default {
+ name: 'ProjectsDropdownFilter',
+ components: {
+ GlIcon,
+ GlLoadingIcon,
+ GlAvatar,
+ GlDropdown,
+ GlDropdownSectionHeader,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ },
+ props: {
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ groupNamespace: {
+ type: String,
+ required: true,
+ },
+ multiSelect: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ label: {
+ type: String,
+ required: false,
+ default: s__('CycleAnalytics|project dropdown filter'),
+ },
+ queryParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ defaultProjects: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ data() {
+ return {
+ loading: true,
+ projects: [],
+ selectedProjects: this.defaultProjects || [],
+ searchTerm: '',
+ isDirty: false,
+ };
+ },
+ computed: {
+ selectedProjectsLabel() {
+ if (this.selectedProjects.length === 1) {
+ return this.selectedProjects[0].name;
+ } else if (this.selectedProjects.length > 1) {
+ return n__(
+ 'CycleAnalytics|Project selected',
+ 'CycleAnalytics|%d projects selected',
+ this.selectedProjects.length,
+ );
+ }
+
+ return this.selectedProjectsPlaceholder;
+ },
+ selectedProjectsPlaceholder() {
+ return this.multiSelect ? __('Select projects') : __('Select a project');
+ },
+ isOnlyOneProjectSelected() {
+ return this.selectedProjects.length === 1;
+ },
+ selectedProjectIds() {
+ return this.selectedProjects.map((p) => p.id);
+ },
+ availableProjects() {
+ return filterBySearchTerm(this.projects, this.searchTerm);
+ },
+ noResultsAvailable() {
+ const { loading, availableProjects } = this;
+ return !loading && !availableProjects.length;
+ },
+ },
+ watch: {
+ searchTerm() {
+ this.search();
+ },
+ },
+ mounted() {
+ this.search();
+ },
+ methods: {
+ search: debounce(function debouncedSearch() {
+ this.fetchData();
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ getSelectedProjects(selectedProject, isMarking) {
+ return isMarking
+ ? this.selectedProjects.concat([selectedProject])
+ : this.selectedProjects.filter((project) => project.id !== selectedProject.id);
+ },
+ singleSelectedProject(selectedObj, isMarking) {
+ return isMarking ? [selectedObj] : [];
+ },
+ setSelectedProjects(selectedObj, isMarking) {
+ this.selectedProjects = this.multiSelect
+ ? this.getSelectedProjects(selectedObj, isMarking)
+ : this.singleSelectedProject(selectedObj, isMarking);
+ },
+ onClick({ project, isSelected }) {
+ this.setSelectedProjects(project, !isSelected);
+ this.$emit('selected', this.selectedProjects);
+ },
+ onMultiSelectClick({ project, isSelected }) {
+ this.setSelectedProjects(project, !isSelected);
+ this.isDirty = true;
+ },
+ onSelected(ev) {
+ if (this.multiSelect) {
+ this.onMultiSelectClick(ev);
+ } else {
+ this.onClick(ev);
+ }
+ },
+ onHide() {
+ if (this.multiSelect && this.isDirty) {
+ this.$emit('selected', this.selectedProjects);
+ }
+ this.searchTerm = '';
+ this.isDirty = false;
+ },
+ fetchData() {
+ this.loading = true;
+
+ return this.$apollo
+ .query({
+ query: getProjects,
+ variables: {
+ groupFullPath: this.groupNamespace,
+ search: this.searchTerm,
+ ...this.queryParams,
+ },
+ })
+ .then((response) => {
+ const {
+ data: {
+ group: {
+ projects: { nodes },
+ },
+ },
+ } = response;
+
+ this.loading = false;
+ this.projects = nodes;
+ });
+ },
+ isProjectSelected(id) {
+ return this.selectedProjects ? this.selectedProjectIds.includes(id) : false;
+ },
+ getEntityId(project) {
+ return getIdFromGraphQLId(project.id);
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ ref="projectsDropdown"
+ class="dropdown dropdown-projects"
+ toggle-class="gl-shadow-none"
+ @hide="onHide"
+ >
+ <template #button-content>
+ <div class="gl-display-flex gl-flex-grow-1">
+ <gl-avatar
+ v-if="isOnlyOneProjectSelected"
+ :src="selectedProjects[0].avatarUrl"
+ :entity-id="getEntityId(selectedProjects[0])"
+ :entity-name="selectedProjects[0].name"
+ :size="16"
+ shape="rect"
+ :alt="selectedProjects[0].name"
+ class="gl-display-inline-flex gl-vertical-align-middle gl-mr-2"
+ />
+ {{ selectedProjectsLabel }}
+ </div>
+ <gl-icon class="gl-ml-2" name="chevron-down" />
+ </template>
+ <template #header>
+ <gl-dropdown-section-header>{{ __('Projects') }}</gl-dropdown-section-header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <gl-dropdown-item
+ v-for="project in availableProjects"
+ :key="project.id"
+ :is-check-item="true"
+ :is-checked="isProjectSelected(project.id)"
+ @click.native.capture.stop="
+ onSelected({ project, isSelected: isProjectSelected(project.id) })
+ "
+ >
+ <div class="gl-display-flex">
+ <gl-avatar
+ class="gl-mr-2 vertical-align-middle"
+ :alt="project.name"
+ :size="16"
+ :entity-id="getEntityId(project)"
+ :entity-name="project.name"
+ :src="project.avatarUrl"
+ shape="rect"
+ />
+ <div>
+ <div data-testid="project-name">{{ project.name }}</div>
+ <div class="gl-text-gray-500" data-testid="project-full-path">
+ {{ project.fullPath }}
+ </div>
+ </div>
+ </div>
+ </gl-dropdown-item>
+ <gl-dropdown-item v-show="noResultsAvailable" class="gl-pointer-events-none text-secondary">{{
+ __('No matching results')
+ }}</gl-dropdown-item>
+ <gl-dropdown-item v-if="loading">
+ <gl-loading-icon size="lg" />
+ </gl-dropdown-item>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
new file mode 100644
index 00000000000..44d9b4b4262
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -0,0 +1,12 @@
+import { masks } from 'dateformat';
+
+export const DATE_RANGE_LIMIT = 180;
+export const OFFSET_DATE_BY_ONE = 1;
+export const PROJECTS_PER_PAGE = 50;
+
+const { isoDate, mediumDate } = masks;
+export const dateFormats = {
+ isoDate,
+ defaultDate: mediumDate,
+ defaultDateTime: 'mmm d, yyyy h:MMtt',
+};
diff --git a/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
new file mode 100644
index 00000000000..63e95d6804c
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/graphql/projects.query.graphql
@@ -0,0 +1,22 @@
+query getGroupProjects(
+ $groupFullPath: ID!
+ $search: String!
+ $first: Int!
+ $includeSubgroups: Boolean = false
+) {
+ group(fullPath: $groupFullPath) {
+ projects(
+ search: $search
+ first: $first
+ includeSubgroups: $includeSubgroups
+ sort: SIMILARITY
+ ) {
+ nodes {
+ id
+ name
+ avatarUrl
+ fullPath
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/analytics/shared/utils.js b/app/assets/javascripts/analytics/shared/utils.js
new file mode 100644
index 00000000000..84189b675f2
--- /dev/null
+++ b/app/assets/javascripts/analytics/shared/utils.js
@@ -0,0 +1,4 @@
+export const filterBySearchTerm = (data = [], searchTerm = '', filterByKey = 'name') => {
+ if (!searchTerm?.length) return data;
+ return data.filter((item) => item[filterByKey].toLowerCase().includes(searchTerm.toLowerCase()));
+};
diff --git a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
index 0b4fa879b03..1eb4832a2a3 100644
--- a/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
+++ b/app/assets/javascripts/analytics/usage_trends/components/usage_counts.vue
@@ -1,5 +1,6 @@
<script>
-import MetricCard from '~/analytics/shared/components/metric_card.vue';
+import { GlDeprecatedSkeletonLoading as GlSkeletonLoading } from '@gitlab/ui';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
import createFlash from '~/flash';
import { number } from '~/lib/utils/unit_format';
import { s__ } from '~/locale';
@@ -10,7 +11,8 @@ const defaultPrecision = 0;
export default {
name: 'UsageCounts',
components: {
- MetricCard,
+ GlSkeletonLoading,
+ GlSingleStat,
},
data() {
return {
@@ -56,10 +58,24 @@ export default {
</script>
<template>
- <metric-card
- :title="__('Usage Trends')"
- :metrics="counts"
- :is-loading="$apollo.queries.counts.loading"
- class="gl-mt-4"
- />
+ <div>
+ <h2>
+ {{ __('Usage Trends') }}
+ </h2>
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-my-6 gl-align-items-flex-start"
+ >
+ <gl-skeleton-loading v-if="$apollo.queries.counts.loading" />
+ <template v-else>
+ <gl-single-stat
+ v-for="count in counts"
+ :key="count.key"
+ class="gl-pr-9 gl-my-4 gl-md-mt-0 gl-md-mb-0"
+ :value="`${count.value}`"
+ :title="count.label"
+ :should-animate="true"
+ />
+ </template>
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 41cc2036a6b..84a5d5ae4b3 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -3,7 +3,7 @@ import { __ } from '~/locale';
import axios from './lib/utils/axios_utils';
import { joinPaths } from './lib/utils/url_utility';
-const DEFAULT_PER_PAGE = 20;
+export const DEFAULT_PER_PAGE = 20;
/**
* Slow deprecation Notice: Please rather use for new calls
@@ -83,8 +83,8 @@ const Api = {
tagsPath: '/api/:version/projects/:id/repository/tags',
freezePeriodsPath: '/api/:version/projects/:id/freeze_periods',
freezePeriodPath: '/api/:version/projects/:id/freeze_periods/:freeze_period_id',
- usageDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
- usageDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
+ serviceDataIncrementCounterPath: '/api/:version/usage_data/increment_counter',
+ serviceDataIncrementUniqueUsersPath: '/api/:version/usage_data/increment_unique_users',
featureFlagUserLists: '/api/:version/projects/:id/feature_flags_user_lists',
featureFlagUserList: '/api/:version/projects/:id/feature_flags_user_lists/:list_iid',
containerRegistryDetailsPath: '/api/:version/registry/repositories/:id/',
@@ -875,7 +875,7 @@ const Api = {
return null;
}
- const url = Api.buildUrl(this.usageDataIncrementCounterPath);
+ const url = Api.buildUrl(this.serviceDataIncrementCounterPath);
const headers = {
'Content-Type': 'application/json',
};
@@ -888,7 +888,7 @@ const Api = {
return null;
}
- const url = Api.buildUrl(this.usageDataIncrementUniqueUsersPath);
+ const url = Api.buildUrl(this.serviceDataIncrementUniqueUsersPath);
const headers = {
'Content-Type': 'application/json',
};
diff --git a/app/assets/javascripts/api/analytics_api.js b/app/assets/javascripts/api/analytics_api.js
index 58494c5a2b8..fd9b0160b0d 100644
--- a/app/assets/javascripts/api/analytics_api.js
+++ b/app/assets/javascripts/api/analytics_api.js
@@ -1,6 +1,8 @@
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
+const GROUP_VSA_PATH_BASE =
+ '/groups/:id/-/analytics/value_stream_analytics/value_streams/:value_stream_id/stages/:stage_id';
const PROJECT_VSA_PATH_BASE = '/:project_path/-/analytics/value_stream_analytics/value_streams';
const PROJECT_VSA_STAGES_PATH = `${PROJECT_VSA_PATH_BASE}/:value_stream_id/stages`;
@@ -13,6 +15,12 @@ const buildProjectValueStreamPath = (projectPath, valueStreamId = null) => {
return buildApiUrl(PROJECT_VSA_PATH_BASE).replace(':project_path', projectPath);
};
+const buildGroupValueStreamPath = ({ groupId, valueStreamId = null, stageId = null }) =>
+ buildApiUrl(GROUP_VSA_PATH_BASE)
+ .replace(':id', groupId)
+ .replace(':value_stream_id', valueStreamId)
+ .replace(':stage_id', stageId);
+
export const getProjectValueStreams = (projectPath) => {
const url = buildProjectValueStreamPath(projectPath);
return axios.get(url);
@@ -30,3 +38,14 @@ export const getProjectValueStreamStageData = ({ requestPath, stageId, params })
export const getProjectValueStreamMetrics = (requestPath, params) =>
axios.get(requestPath, { params });
+
+/**
+ * Shared group VSA paths
+ * We share some endpoints across and group and project level VSA
+ * When used for project level VSA, requests should include the `project_id` in the params object
+ */
+
+export const getValueStreamStageMedian = ({ groupId, valueStreamId, stageId }, params = {}) => {
+ const stageBase = buildGroupValueStreamPath({ groupId, valueStreamId, stageId });
+ return axios.get(`${stageBase}/median`, { params });
+};
diff --git a/app/assets/javascripts/api/constants.js b/app/assets/javascripts/api/constants.js
deleted file mode 100644
index b6c720a85f3..00000000000
--- a/app/assets/javascripts/api/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const DEFAULT_PER_PAGE = 20;
diff --git a/app/assets/javascripts/api/groups_api.js b/app/assets/javascripts/api/groups_api.js
index d6c9e1d42cc..a563afc6abb 100644
--- a/app/assets/javascripts/api/groups_api.js
+++ b/app/assets/javascripts/api/groups_api.js
@@ -1,6 +1,6 @@
+import { DEFAULT_PER_PAGE } from '~/api';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
-import { DEFAULT_PER_PAGE } from './constants';
const GROUPS_PATH = '/api/:version/groups.json';
const DESCENDANT_GROUPS_PATH = '/api/:version/groups/:id/descendant_groups';
diff --git a/app/assets/javascripts/api/projects_api.js b/app/assets/javascripts/api/projects_api.js
index d9a2467cff3..1cd7fb0b954 100644
--- a/app/assets/javascripts/api/projects_api.js
+++ b/app/assets/javascripts/api/projects_api.js
@@ -1,6 +1,6 @@
+import { DEFAULT_PER_PAGE } from '~/api';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
-import { DEFAULT_PER_PAGE } from './constants';
const PROJECTS_PATH = '/api/:version/projects.json';
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index 27901120c53..09995fad628 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -1,8 +1,8 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import { DEFAULT_PER_PAGE } from '~/api';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import axios from '../lib/utils/axios_utils';
import { buildApiUrl } from './api_utils';
-import { DEFAULT_PER_PAGE } from './constants';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
@@ -52,7 +52,11 @@ export function getUserProjects(userId, query, options, callback) {
params: { ...defaults, ...options },
})
.then(({ data }) => callback(data))
- .catch(() => flash(__('Something went wrong while fetching projects')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while fetching projects'),
+ }),
+ );
}
export function updateUserStatus({ emoji, message, availability, clearStatusAfter }) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 43f44370af8..43ca5b5cf89 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -7,7 +7,7 @@ import { uniq } from 'lodash';
import * as Emoji from '~/emoji';
import { scrollToElement } from '~/lib/utils/common_utils';
import { dispose, fixTitle } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { isInVueNoteablePage } from './lib/utils/dom_utils';
import { __ } from './locale';
@@ -488,7 +488,11 @@ export class AwardsHandler {
callback();
}
})
- .catch(() => flash(__('Something went wrong on our end.')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong on our end.'),
+ }),
+ );
}
findEmojiIcon(votesBlock, emoji) {
diff --git a/app/assets/javascripts/badges/components/badge.vue b/app/assets/javascripts/badges/components/badge.vue
index 309af368df9..53469ac8999 100644
--- a/app/assets/javascripts/badges/components/badge.vue
+++ b/app/assets/javascripts/badges/components/badge.vue
@@ -84,7 +84,7 @@ export default {
/>
</a>
- <gl-loading-icon v-show="isLoading" :inline="true" />
+ <gl-loading-icon v-show="isLoading" size="sm" :inline="true" />
<div v-show="hasError" class="btn-group">
<div class="btn btn-default btn-sm disabled">
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 7c4ff830a9d..7e605099655 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -221,7 +221,7 @@ export default {
:link-url="renderedLinkUrl"
/>
<p v-show="isRendering">
- <gl-loading-icon :inline="true" />
+ <gl-loading-icon size="sm" :inline="true" />
</p>
<p v-show="!renderedBadge && !isRendering" class="disabled-content">
{{ s__('Badges|No image to preview') }}
diff --git a/app/assets/javascripts/badges/components/badge_list_row.vue b/app/assets/javascripts/badges/components/badge_list_row.vue
index fda51c98e2c..d8525c15087 100644
--- a/app/assets/javascripts/badges/components/badge_list_row.vue
+++ b/app/assets/javascripts/badges/components/badge_list_row.vue
@@ -73,7 +73,7 @@ export default {
data-testid="delete-badge"
@click="updateBadgeInModal(badge)"
/>
- <gl-loading-icon v-show="badge.isDeleting" :inline="true" />
+ <gl-loading-icon v-show="badge.isDeleting" size="sm" :inline="true" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/batch_comments/components/draft_note.vue b/app/assets/javascripts/batch_comments/components/draft_note.vue
index e6de724512f..96c3b8276ee 100644
--- a/app/assets/javascripts/batch_comments/components/draft_note.vue
+++ b/app/assets/javascripts/batch_comments/components/draft_note.vue
@@ -94,9 +94,11 @@ export default {
@handleUpdateNote="update"
@toggleResolveStatus="toggleResolveDiscussion(draft.id)"
>
- <strong slot="note-header-info" class="badge draft-pending-label gl-mr-2">
- {{ __('Pending') }}
- </strong>
+ <template #note-header-info>
+ <strong class="badge draft-pending-label gl-mr-2">
+ {{ __('Pending') }}
+ </strong>
+ </template>
</noteable-note>
</ul>
diff --git a/app/assets/javascripts/batch_comments/components/review_bar.vue b/app/assets/javascripts/batch_comments/components/review_bar.vue
index 9ffc5ee34cf..080a5543e53 100644
--- a/app/assets/javascripts/batch_comments/components/review_bar.vue
+++ b/app/assets/javascripts/batch_comments/components/review_bar.vue
@@ -26,7 +26,7 @@ export default {
</script>
<template>
<div v-show="draftsCount > 0">
- <nav class="review-bar-component">
+ <nav class="review-bar-component" data-testid="review_bar_component">
<div
class="review-bar-content d-flex gl-justify-content-end"
data-qa-selector="review_bar_content"
diff --git a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
index a8c0b064595..4ee22918463 100644
--- a/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
+++ b/app/assets/javascripts/batch_comments/stores/modules/batch_comments/actions.js
@@ -1,5 +1,5 @@
import { isEmpty } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { scrollToElement } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import { CHANGES_TAB, DISCUSSION_TAB, SHOW_TAB } from '../../../constants';
@@ -18,7 +18,9 @@ export const addDraftToDiscussion = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- flash(__('An error occurred adding a draft to the thread.'));
+ createFlash({
+ message: __('An error occurred adding a draft to the thread.'),
+ });
});
export const createNewDraft = ({ commit }, { endpoint, data }) =>
@@ -30,7 +32,9 @@ export const createNewDraft = ({ commit }, { endpoint, data }) =>
return res;
})
.catch(() => {
- flash(__('An error occurred adding a new draft.'));
+ createFlash({
+ message: __('An error occurred adding a new draft.'),
+ });
});
export const deleteDraft = ({ commit, getters }, draft) =>
@@ -39,7 +43,11 @@ export const deleteDraft = ({ commit, getters }, draft) =>
.then(() => {
commit(types.DELETE_DRAFT, draft.id);
})
- .catch(() => flash(__('An error occurred while deleting the comment')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while deleting the comment'),
+ }),
+ );
export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
service
@@ -53,7 +61,11 @@ export const fetchDrafts = ({ commit, getters, state, dispatch }) =>
}
});
})
- .catch(() => flash(__('An error occurred while fetching pending comments')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while fetching pending comments'),
+ }),
+ );
export const publishSingleDraft = ({ commit, dispatch, getters }, draftId) => {
commit(types.REQUEST_PUBLISH_DRAFT, draftId);
@@ -111,7 +123,11 @@ export const updateDraft = (
.then((res) => res.data)
.then((data) => commit(types.RECEIVE_DRAFT_UPDATE_SUCCESS, data))
.then(callback)
- .catch(() => flash(__('An error occurred while updating the comment')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while updating the comment'),
+ }),
+ );
};
export const scrollToDraft = ({ dispatch, rootGetters }, draft) => {
diff --git a/app/assets/javascripts/behaviors/markdown/render_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
index 5fecadf2794..293fe9f4133 100644
--- a/app/assets/javascripts/behaviors/markdown/render_mermaid.js
+++ b/app/assets/javascripts/behaviors/markdown/render_mermaid.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { once, countBy } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { __, sprintf } from '~/locale';
@@ -78,7 +78,9 @@ function importMermaidModule() {
mermaidModule = initMermaid(mermaid);
})
.catch((err) => {
- flash(sprintf(__("Can't load mermaid module: %{err}"), { err }));
+ createFlash({
+ message: sprintf(__("Can't load mermaid module: %{err}"), { err }),
+ });
// eslint-disable-next-line no-console
console.error(err);
});
@@ -205,7 +207,9 @@ function renderMermaids($els) {
});
})
.catch((err) => {
- flash(sprintf(__('Encountered an error while rendering: %{err}'), { err }));
+ createFlash({
+ message: sprintf(__('Encountered an error while rendering: %{err}'), { err }),
+ });
// eslint-disable-next-line no-console
console.error(err);
});
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index 5405819cfe0..a1911585f80 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -1,7 +1,7 @@
/* eslint-disable func-names */
import $ from 'jquery';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -79,7 +79,11 @@ MarkdownPreview.prototype.fetchMarkdownPreview = function (text, url, success) {
};
success(data);
})
- .catch(() => flash(__('An error occurred while fetching markdown preview')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while fetching markdown preview'),
+ }),
+ );
};
MarkdownPreview.prototype.hideReferencedUsers = function ($form) {
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index c9152db509a..af8e8a4cd3d 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -1,9 +1,11 @@
+import createFlash from '~/flash';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as Flash } from '../flash';
import BalsamiqViewer from './balsamiq/balsamiq_viewer';
function onError() {
- const flash = new Flash(__('Balsamiq file could not be loaded.'));
+ const flash = createFlash({
+ message: __('Balsamiq file could not be loaded.'),
+ });
return flash;
}
diff --git a/app/assets/javascripts/blob/components/blob_content.vue b/app/assets/javascripts/blob/components/blob_content.vue
index 60729c11002..1a74675100b 100644
--- a/app/assets/javascripts/blob/components/blob_content.vue
+++ b/app/assets/javascripts/blob/components/blob_content.vue
@@ -27,6 +27,11 @@ export default {
default: false,
required: false,
},
+ richViewer: {
+ type: String,
+ default: '',
+ required: false,
+ },
loading: {
type: Boolean,
default: true,
@@ -71,6 +76,7 @@ export default {
v-else
ref="contentViewer"
:content="content"
+ :rich-viewer="richViewer"
:is-raw-content="isRawContent"
:file-name="blob.name"
:type="activeViewer.fileType"
diff --git a/app/assets/javascripts/blob/components/blob_edit_content.vue b/app/assets/javascripts/blob/components/blob_edit_content.vue
index 73ccc3289b9..0e670bbd80a 100644
--- a/app/assets/javascripts/blob/components/blob_edit_content.vue
+++ b/app/assets/javascripts/blob/components/blob_edit_content.vue
@@ -1,6 +1,6 @@
<script>
import { debounce } from 'lodash';
-import { initEditorLite } from '~/blob/utils';
+import { initSourceEditor } from '~/blob/utils';
import { SNIPPET_MEASURE_BLOBS_CONTENT } from '~/performance/constants';
import eventHub from './eventhub';
@@ -36,7 +36,7 @@ export default {
},
},
mounted() {
- this.editor = initEditorLite({
+ this.editor = initSourceEditor({
el: this.$refs.editor,
blobPath: this.fileName,
blobContent: this.value,
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index 99fe3938046..cb441a7e491 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -29,7 +29,7 @@ export default {
<slot name="filepath-prepend"></slot>
<template v-if="blob.path">
- <file-icon :file-name="blob.path" :size="18" aria-hidden="true" css-classes="mr-2" />
+ <file-icon :file-name="blob.path" :size="16" aria-hidden="true" css-classes="mr-2" />
<strong
class="file-title-name mr-1 js-blob-header-filepath"
data-qa-selector="file_title_content"
diff --git a/app/assets/javascripts/blob/csv/csv_viewer.vue b/app/assets/javascripts/blob/csv/csv_viewer.vue
new file mode 100644
index 00000000000..050f2785d9a
--- /dev/null
+++ b/app/assets/javascripts/blob/csv/csv_viewer.vue
@@ -0,0 +1,55 @@
+<script>
+import { GlAlert, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import Papa from 'papaparse';
+
+export default {
+ components: {
+ GlTable,
+ GlAlert,
+ GlLoadingIcon,
+ },
+ props: {
+ csv: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ items: [],
+ errorMessage: null,
+ loading: true,
+ };
+ },
+ mounted() {
+ const parsed = Papa.parse(this.csv, { skipEmptyLines: true });
+ this.items = parsed.data;
+
+ if (parsed.errors.length) {
+ this.errorMessage = parsed.errors.map((e) => e.message).join('. ');
+ }
+
+ this.loading = false;
+ },
+};
+</script>
+
+<template>
+ <div class="container-fluid md gl-mt-3 gl-mb-3">
+ <div v-if="loading" class="gl-text-center loading">
+ <gl-loading-icon class="gl-mt-5" size="lg" />
+ </div>
+ <div v-else>
+ <gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+ <gl-table
+ :empty-text="__('No CSV data to display.')"
+ :items="items"
+ :fields="$options.fields"
+ show-empty
+ thead-class="gl-display-none"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/blob/csv/index.js b/app/assets/javascripts/blob/csv/index.js
new file mode 100644
index 00000000000..4cf6c169c68
--- /dev/null
+++ b/app/assets/javascripts/blob/csv/index.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import CsvViewer from './csv_viewer.vue';
+
+export default () => {
+ const el = document.getElementById('js-csv-viewer');
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(CsvViewer, {
+ props: {
+ csv: el.dataset.data,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/blob/csv_viewer.js b/app/assets/javascripts/blob/csv_viewer.js
new file mode 100644
index 00000000000..64d3ba0b390
--- /dev/null
+++ b/app/assets/javascripts/blob/csv_viewer.js
@@ -0,0 +1,3 @@
+import renderCSV from './csv';
+
+export default renderCSV;
diff --git a/app/assets/javascripts/blob/file_template_mediator.js b/app/assets/javascripts/blob/file_template_mediator.js
index 59ab84bf208..136457c115d 100644
--- a/app/assets/javascripts/blob/file_template_mediator.js
+++ b/app/assets/javascripts/blob/file_template_mediator.js
@@ -2,11 +2,10 @@ import $ from 'jquery';
import Api from '~/api';
import initPopover from '~/blob/suggest_gitlab_ci_yml';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
-import { deprecatedCreateFlash as Flash } from '../flash';
-
import BlobCiYamlSelector from './template_selectors/ci_yaml_selector';
import DockerfileSelector from './template_selectors/dockerfile_selector';
import GitignoreSelector from './template_selectors/gitignore_selector';
@@ -146,7 +145,7 @@ export default class FileTemplateMediator {
text: __('Undo'),
onClick: (e, toastObj) => {
self.restoreFromCache();
- toastObj.goAway(0);
+ toastObj.hide();
},
},
});
@@ -155,7 +154,11 @@ export default class FileTemplateMediator {
initPopover(suggestCommitChanges);
}
})
- .catch((err) => new Flash(`An error occurred while fetching the template: ${err}`));
+ .catch((err) =>
+ createFlash({
+ message: __(`An error occurred while fetching the template: ${err}`),
+ }),
+ );
}
displayMatchedTemplateSelector() {
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index e6dc463f764..cb251274b18 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -1,5 +1,5 @@
import { SwaggerUIBundle } from 'swagger-ui-dist';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
export default () => {
@@ -13,7 +13,9 @@ export default () => {
});
})
.catch((error) => {
- flash(__('Something went wrong while initializing the OpenAPI viewer'));
+ createFlash({
+ message: __('Something went wrong while initializing the OpenAPI viewer'),
+ });
throw error;
});
};
diff --git a/app/assets/javascripts/blob/utils.js b/app/assets/javascripts/blob/utils.js
index 8043c0bbc07..bbc061dd36e 100644
--- a/app/assets/javascripts/blob/utils.js
+++ b/app/assets/javascripts/blob/utils.js
@@ -1,6 +1,6 @@
-import Editor from '~/editor/editor_lite';
+import Editor from '~/editor/source_editor';
-export function initEditorLite({ el, ...args }) {
+export function initSourceEditor({ el, ...args }) {
const editor = new Editor({
scrollbar: {
alwaysConsumeMouseWheel: false,
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 22c6b31143f..4d133659daa 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -1,14 +1,16 @@
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import {
REPO_BLOB_LOAD_VIEWER_START,
REPO_BLOB_LOAD_VIEWER_FINISH,
REPO_BLOB_LOAD_VIEWER,
+ REPO_BLOB_SWITCH_TO_VIEWER_START,
+ REPO_BLOB_SWITCH_VIEWER,
} from '~/performance/constants';
import { performanceMarkAndMeasure } from '~/performance/utils';
import { fixTitle } from '~/tooltips';
-import { deprecatedCreateFlash as Flash } from '../../flash';
import axios from '../../lib/utils/axios_utils';
import { handleLocationHash } from '../../lib/utils/common_utils';
import eventHub from '../../notes/event_hub';
@@ -21,6 +23,8 @@ const loadRichBlobViewer = (type) => {
return import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer');
case 'openapi':
return import(/* webpackChunkName: 'openapi_viewer' */ '../openapi_viewer');
+ case 'csv':
+ return import(/* webpackChunkName: 'csv_viewer' */ '../csv_viewer');
case 'pdf':
return import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer');
case 'sketch':
@@ -38,13 +42,18 @@ export const handleBlobRichViewer = (viewer, type) => {
loadRichBlobViewer(type)
.then((module) => module?.default(viewer))
.catch((error) => {
- Flash(__('Error loading file viewer.'));
+ createFlash({
+ message: __('Error loading file viewer.'),
+ });
throw error;
});
};
export default class BlobViewer {
constructor() {
+ performanceMarkAndMeasure({
+ mark: REPO_BLOB_LOAD_VIEWER_START,
+ });
const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
const type = viewer?.dataset?.richType;
BlobViewer.initAuxiliaryViewer();
@@ -137,7 +146,7 @@ export default class BlobViewer {
switchToViewer(name) {
performanceMarkAndMeasure({
- mark: REPO_BLOB_LOAD_VIEWER_START,
+ mark: REPO_BLOB_SWITCH_TO_VIEWER_START,
});
const newViewer = this.$fileHolder[0].querySelector(`.blob-viewer[data-type='${name}']`);
if (this.activeViewer === newViewer) return;
@@ -167,11 +176,15 @@ export default class BlobViewer {
BlobViewer.loadViewer(newViewer)
.then((viewer) => {
$(viewer).renderGFM();
+ window.requestIdleCallback(() => {
+ this.$fileHolder.trigger('highlight:line');
+ handleLocationHash();
- this.$fileHolder.trigger('highlight:line');
- handleLocationHash();
+ viewer.setAttribute('data-loaded', 'true');
+ this.toggleCopyButtonState();
+ eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
+ });
- this.toggleCopyButtonState();
performanceMarkAndMeasure({
mark: REPO_BLOB_LOAD_VIEWER_FINISH,
measures: [
@@ -179,10 +192,18 @@ export default class BlobViewer {
name: REPO_BLOB_LOAD_VIEWER,
start: REPO_BLOB_LOAD_VIEWER_START,
},
+ {
+ name: REPO_BLOB_SWITCH_VIEWER,
+ start: REPO_BLOB_SWITCH_TO_VIEWER_START,
+ },
],
});
})
- .catch(() => new Flash(__('Error loading viewer')));
+ .catch(() =>
+ createFlash({
+ message: __('Error loading viewer'),
+ }),
+ );
}
static loadViewer(viewerParam) {
@@ -197,9 +218,10 @@ export default class BlobViewer {
return axios.get(url).then(({ data }) => {
viewer.innerHTML = data.html;
- viewer.setAttribute('data-loaded', 'true');
- eventHub.$emit('showBlobInteractionZones', viewer.dataset.path);
+ window.requestIdleCallback(() => {
+ viewer.removeAttribute('data-loading');
+ });
return viewer;
});
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 7c8d0d5ded0..7bfda46d71c 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import EditorLite from '~/editor/editor_lite';
-import { FileTemplateExtension } from '~/editor/extensions/editor_file_template_ext';
+import { FileTemplateExtension } from '~/editor/extensions/source_editor_file_template_ext';
+import SourceEditor from '~/editor/source_editor';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { addEditorMarkdownListeners } from '~/lib/utils/text_markdown';
@@ -16,7 +16,7 @@ export default class EditBlob {
this.configureMonacoEditor();
if (this.options.isMarkdown) {
- import('~/editor/extensions/editor_markdown_ext')
+ import('~/editor/extensions/source_editor_markdown_ext')
.then(({ EditorMarkdownExtension: MarkdownExtension } = {}) => {
this.editor.use(new MarkdownExtension());
addEditorMarkdownListeners(this.editor);
@@ -40,7 +40,7 @@ export default class EditBlob {
const fileContentEl = document.getElementById('file-content');
const form = document.querySelector('.js-edit-blob-form');
- const rootEditor = new EditorLite();
+ const rootEditor = new SourceEditor();
this.editor = rootEditor.createInstance({
el: editorEl,
diff --git a/app/assets/javascripts/boards/boards_util.js b/app/assets/javascripts/boards/boards_util.js
index e14a770411e..46f97e09385 100644
--- a/app/assets/javascripts/boards/boards_util.js
+++ b/app/assets/javascripts/boards/boards_util.js
@@ -54,6 +54,7 @@ export function formatListIssues(listIssues) {
const listIssue = {
...i,
id,
+ fullId: i.id,
labels: i.labels?.nodes || [],
assignees: i.assignees?.nodes || [],
};
@@ -106,8 +107,8 @@ export function formatIssueInput(issueInput, boardConfig) {
const { labels, assigneeId, milestoneId } = boardConfig;
return {
- milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
...issueInput,
+ milestoneId: milestoneId ? fullMilestoneId(milestoneId) : null,
labelIds: [...labelIds, ...(labels?.map((l) => fullLabelId(l)) || [])],
assigneeIds: [...assigneeIds, ...(assigneeId ? [fullUserId(assigneeId)] : [])],
};
diff --git a/app/assets/javascripts/boards/components/board_blocked_icon.vue b/app/assets/javascripts/boards/components/board_blocked_icon.vue
index 0f92e714752..b81edb4dfe6 100644
--- a/app/assets/javascripts/boards/components/board_blocked_icon.vue
+++ b/app/assets/javascripts/boards/components/board_blocked_icon.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlPopover, GlLoadingIcon } from '@gitlab/ui';
import { blockingIssuablesQueries, issuableTypes } from '~/boards/constants';
-import { IssueType } from '~/graphql_shared/constants';
+import { TYPE_ISSUE } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { truncate } from '~/lib/utils/text_utility';
import { __, n__, s__, sprintf } from '~/locale';
@@ -13,7 +13,7 @@ export default {
},
},
graphQLIdType: {
- [issuableTypes.issue]: IssueType,
+ [issuableTypes.issue]: TYPE_ISSUE,
},
referenceFormatter: {
[issuableTypes.issue]: (r) => r.split('/')[1],
@@ -163,7 +163,7 @@ export default {
><span data-testid="popover-title">{{ blockedLabel }}</span></template
>
<template v-if="loading">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
<p class="gl-mt-4 gl-mb-0 gl-font-small">{{ loadingMessage }}</p>
</template>
<template v-else>
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index 2f4e9044b9e..05b64ddc773 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -1,5 +1,12 @@
<script>
-import { GlLabel, GlTooltipDirective, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlLabel,
+ GlTooltip,
+ GlTooltipDirective,
+ GlIcon,
+ GlLoadingIcon,
+ GlSprintf,
+} from '@gitlab/ui';
import { sortBy } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
@@ -16,6 +23,7 @@ import IssueTimeEstimate from './issue_time_estimate.vue';
export default {
components: {
+ GlTooltip,
GlLabel,
GlLoadingIcon,
GlIcon,
@@ -25,6 +33,7 @@ export default {
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
BoardBlockedIcon,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -55,7 +64,7 @@ export default {
};
},
computed: {
- ...mapState(['isShowingLabels', 'issuableType']),
+ ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']),
...mapGetters(['isEpicBoard']),
cappedAssignees() {
// e.g. maxRender is 4,
@@ -99,6 +108,12 @@ export default {
}
return false;
},
+ shouldRenderEpicCountables() {
+ return this.isEpicBoard && this.item.hasIssues;
+ },
+ shouldRenderEpicProgress() {
+ return this.totalWeight > 0;
+ },
showLabelFooter() {
return this.isShowingLabels && this.item.labels.find(this.showLabel);
},
@@ -115,6 +130,20 @@ export default {
}
return __('Blocked issue');
},
+ totalEpicsCount() {
+ return this.item.descendantCounts.openedEpics + this.item.descendantCounts.closedEpics;
+ },
+ totalIssuesCount() {
+ return this.item.descendantCounts.openedIssues + this.item.descendantCounts.closedIssues;
+ },
+ totalWeight() {
+ return (
+ this.item.descendantWeightSum.openedIssues + this.item.descendantWeightSum.closedIssues
+ );
+ },
+ totalProgress() {
+ return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100);
+ },
},
methods: {
...mapActions(['performSearch', 'setError']),
@@ -227,17 +256,93 @@ export default {
{{ itemId }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
- <issue-due-date
- v-if="item.dueDate"
- :date="item.dueDate"
- :closed="item.closed || Boolean(item.closedAt)"
- />
- <issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
- <issue-card-weight
- v-if="validIssueWeight(item)"
- :weight="item.weight"
- @click="filterByWeight(item.weight)"
- />
+ <span v-if="shouldRenderEpicCountables" data-testid="epic-countables">
+ <gl-tooltip :target="() => $refs.countBadge" data-testid="epic-countables-tooltip">
+ <p v-if="allowSubEpics" class="gl-font-weight-bold gl-m-0">
+ {{ __('Epics') }} &#8226;
+ <span class="gl-font-weight-normal">
+ <gl-sprintf :message="__('%{openedEpics} open, %{closedEpics} closed')">
+ <template #openedEpics>{{ item.descendantCounts.openedEpics }}</template>
+ <template #closedEpics>{{ item.descendantCounts.closedEpics }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Issues') }} &#8226;
+ <span class="gl-font-weight-normal">
+ <gl-sprintf :message="__('%{openedIssues} open, %{closedIssues} closed')">
+ <template #openedIssues>{{ item.descendantCounts.openedIssues }}</template>
+ <template #closedIssues>{{ item.descendantCounts.closedIssues }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Total weight') }} &#8226;
+ <span class="gl-font-weight-normal" data-testid="epic-countables-total-weight">
+ {{ totalWeight }}
+ </span>
+ </p>
+ </gl-tooltip>
+
+ <gl-tooltip
+ v-if="shouldRenderEpicProgress"
+ :target="() => $refs.progressBadge"
+ data-testid="epic-progress-tooltip"
+ >
+ <p class="gl-font-weight-bold gl-m-0">
+ {{ __('Progress') }} &#8226;
+ <span class="gl-font-weight-normal" data-testid="epic-progress-tooltip-content">
+ <gl-sprintf
+ :message="__('%{completedWeight} of %{totalWeight} weight completed')"
+ >
+ <template #completedWeight>{{
+ item.descendantWeightSum.closedIssues
+ }}</template>
+ <template #totalWeight>{{ totalWeight }}</template>
+ </gl-sprintf>
+ </span>
+ </p>
+ </gl-tooltip>
+
+ <span ref="countBadge" class="issue-count-badge board-card-info gl-mr-0 gl-pr-0">
+ <span v-if="allowSubEpics" class="gl-mr-3">
+ <gl-icon name="epic" />
+ {{ totalEpicsCount }}
+ </span>
+ <span class="gl-mr-3" data-testid="epic-countables-counts-issues">
+ <gl-icon name="issues" />
+ {{ totalIssuesCount }}
+ </span>
+ <span class="gl-mr-3" data-testid="epic-countables-weight-issues">
+ <gl-icon name="weight" />
+ {{ totalWeight }}
+ </span>
+ </span>
+
+ <span
+ v-if="shouldRenderEpicProgress"
+ ref="progressBadge"
+ class="issue-count-badge board-card-info gl-pl-0"
+ >
+ <span class="gl-mr-3" data-testid="epic-progress">
+ <gl-icon name="progress" />
+ {{ totalProgress }}%
+ </span>
+ </span>
+ </span>
+ <span v-if="!isEpicBoard">
+ <issue-due-date
+ v-if="item.dueDate"
+ :date="item.dueDate"
+ :closed="item.closed || Boolean(item.closedAt)"
+ />
+ <issue-time-estimate v-if="item.timeEstimate" :estimate="item.timeEstimate" />
+ <issue-card-weight
+ v-if="validIssueWeight(item)"
+ :weight="item.weight"
+ @click="filterByWeight(item.weight)"
+ />
+ </span>
</span>
</div>
<div class="board-card-assignee gl-display-flex">
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index cc7262f3a39..69abf886ad7 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -41,7 +41,7 @@ export default {
watch: {
filterParams: {
handler() {
- if (this.list.id) {
+ if (this.list.id && !this.list.collapsed) {
this.fetchItemsForList({ listId: this.list.id });
}
},
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index b770ac06e89..53b071aaed1 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -12,10 +12,8 @@ import BoardColumnDeprecated from './board_column_deprecated.vue';
export default {
components: {
BoardAddNewColumn,
- BoardColumn:
- gon.features?.graphqlBoardLists || gon.features?.epicBoards
- ? BoardColumn
- : BoardColumnDeprecated,
+ BoardColumn,
+ BoardColumnDeprecated,
BoardContentSidebar: () => import('~/boards/components/board_content_sidebar.vue'),
EpicBoardContentSidebar: () =>
import('ee_component/boards/components/epic_board_content_sidebar.vue'),
@@ -38,11 +36,14 @@ export default {
computed: {
...mapState(['boardLists', 'error', 'addColumnForm']),
...mapGetters(['isSwimlanesOn', 'isEpicBoard']),
+ useNewBoardColumnComponent() {
+ return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard;
+ },
addColumnFormVisible() {
return this.addColumnForm?.visible;
},
boardListsToUse() {
- return this.glFeatures.graphqlBoardLists || this.isSwimlanesOn || this.isEpicBoard
+ return this.useNewBoardColumnComponent
? sortBy([...Object.values(this.boardLists)], 'position')
: this.lists;
},
@@ -65,6 +66,9 @@ export default {
return this.canDragColumns ? options : {};
},
+ boardColumnComponent() {
+ return this.useNewBoardColumnComponent ? BoardColumn : BoardColumnDeprecated;
+ },
},
methods: {
...mapActions(['moveList', 'unsetError']),
@@ -102,7 +106,8 @@ export default {
class="boards-list gl-w-full gl-py-5 gl-px-3 gl-white-space-nowrap"
@end="handleDragOnEnd"
>
- <board-column
+ <component
+ :is="boardColumnComponent"
v-for="(list, index) in boardListsToUse"
:key="index"
ref="board"
@@ -125,14 +130,9 @@ export default {
<board-content-sidebar
v-if="isSwimlanesOn || glFeatures.graphqlBoardLists"
- class="boards-sidebar"
data-testid="issue-boards-sidebar"
/>
- <epic-board-content-sidebar
- v-else-if="isEpicBoard"
- class="boards-sidebar"
- data-testid="epic-boards-sidebar"
- />
+ <epic-board-content-sidebar v-else-if="isEpicBoard" data-testid="epic-boards-sidebar" />
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index 16a8a9d253f..e014b82d362 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -1,20 +1,20 @@
<script>
import { GlDrawer } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import BoardSidebarLabelsSelect from '~/boards/components/sidebar/board_sidebar_labels_select.vue';
import BoardSidebarTimeTracker from '~/boards/components/sidebar/board_sidebar_time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { ISSUABLE } from '~/boards/constants';
-import { contentTop } from '~/lib/utils/common_utils';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
+import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
- headerHeight: `${contentTop()}px`,
components: {
GlDrawer,
BoardSidebarTitle,
@@ -25,8 +25,10 @@ export default {
BoardSidebarLabelsSelect,
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
- BoardSidebarWeightInput: () =>
- import('ee_component/boards/components/sidebar/board_sidebar_weight_input.vue'),
+ SidebarTodoWidget,
+ MountingPortal,
+ SidebarWeightWidget: () =>
+ import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
IterationSidebarDropdownWidget: () =>
import('ee_component/sidebar/components/iteration_sidebar_dropdown_widget.vue'),
},
@@ -45,6 +47,7 @@ export default {
default: false,
},
},
+ inheritAttrs: false,
computed: {
...mapGetters([
'isSidebarOpen',
@@ -64,7 +67,12 @@ export default {
},
},
methods: {
- ...mapActions(['toggleBoardItem', 'setAssignees', 'setActiveItemConfidential']),
+ ...mapActions([
+ 'toggleBoardItem',
+ 'setAssignees',
+ 'setActiveItemConfidential',
+ 'setActiveItemWeight',
+ ]),
handleClose() {
this.toggleBoardItem({ boardItem: this.activeBoardItem, sidebarType: this.sidebarType });
},
@@ -73,87 +81,105 @@ export default {
</script>
<template>
- <gl-drawer
- v-if="showSidebar"
- :open="isSidebarOpen"
- :header-height="$options.headerHeight"
- @close="handleClose"
- >
- <template #header>{{ __('Issue details') }}</template>
- <template #default>
- <board-sidebar-title />
- <sidebar-assignees-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :initial-assignees="activeBoardItem.assignees"
- :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
- @assignees-updated="setAssignees"
- />
- <sidebar-dropdown-widget
- v-if="epicFeatureAvailable"
- :iid="activeBoardItem.iid"
- issuable-attribute="epic"
- :workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
- :issuable-type="issuableType"
- data-testid="sidebar-epic"
- />
- <div>
+ <mounting-portal mount-to="#js-right-sidebar-portal" name="board-content-sidebar" append>
+ <gl-drawer
+ v-if="showSidebar"
+ v-bind="$attrs"
+ :open="isSidebarOpen"
+ class="boards-sidebar gl-absolute"
+ @close="handleClose"
+ >
+ <template #title>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
+ </template>
+ <template #header>
+ <sidebar-todo-widget
+ class="gl-mt-3"
+ :issuable-id="activeBoardItem.fullId"
+ :issuable-iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ />
+ </template>
+ <template #default>
+ <board-sidebar-title />
+ <sidebar-assignees-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :initial-assignees="activeBoardItem.assignees"
+ :allow-multiple-assignees="multipleAssigneesFeatureAvailable"
+ @assignees-updated="setAssignees"
+ />
<sidebar-dropdown-widget
+ v-if="epicFeatureAvailable"
:iid="activeBoardItem.iid"
- issuable-attribute="milestone"
+ issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
- data-testid="sidebar-milestones"
+ data-testid="sidebar-epic"
/>
- <template v-if="!glFeatures.iterationCadences">
+ <div>
<sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
:iid="activeBoardItem.iid"
- issuable-attribute="iteration"
+ issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
+ :attr-workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- class="gl-mt-5"
- data-testid="iteration-edit"
- data-qa-selector="iteration_container"
+ data-testid="sidebar-milestones"
/>
- </template>
- <template v-else>
- <iteration-sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
- :iid="activeBoardItem.iid"
- :workspace-path="projectPathForActiveIssue"
- :attr-workspace-path="groupPathForActiveIssue"
- :issuable-type="issuableType"
- class="gl-mt-5"
- data-testid="iteration-edit"
- data-qa-selector="iteration_container"
- />
- </template>
- </div>
- <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
- <sidebar-date-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- data-testid="sidebar-due-date"
- />
- <board-sidebar-labels-select class="labels" />
- <board-sidebar-weight-input v-if="weightFeatureAvailable" class="weight" />
- <sidebar-confidentiality-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- @confidentialityUpdated="setActiveItemConfidential($event)"
- />
- <sidebar-subscriptions-widget
- :iid="activeBoardItem.iid"
- :full-path="fullPath"
- :issuable-type="issuableType"
- data-testid="sidebar-notifications"
- />
- </template>
- </gl-drawer>
+ <template v-if="!glFeatures.iterationCadences">
+ <sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ issuable-attribute="iteration"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ />
+ </template>
+ <template v-else>
+ <iteration-sidebar-dropdown-widget
+ v-if="iterationFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :workspace-path="projectPathForActiveIssue"
+ :attr-workspace-path="groupPathForActiveIssue"
+ :issuable-type="issuableType"
+ class="gl-mt-5"
+ data-testid="iteration-edit"
+ />
+ </template>
+ </div>
+ <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
+ <sidebar-date-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-due-date"
+ />
+ <board-sidebar-labels-select class="labels" />
+ <sidebar-weight-widget
+ v-if="weightFeatureAvailable"
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @weightUpdated="setActiveItemWeight($event)"
+ />
+ <sidebar-confidentiality-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ @confidentialityUpdated="setActiveItemConfidential($event)"
+ />
+ <sidebar-subscriptions-widget
+ :iid="activeBoardItem.iid"
+ :full-path="fullPath"
+ :issuable-type="issuableType"
+ data-testid="sidebar-notifications"
+ />
+ </template>
+ </gl-drawer>
+ </mounting-portal>
</template>
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 13388f02f1f..cfd6b21fa66 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -27,7 +27,7 @@ export default {
},
computed: {
urlParams() {
- const { authorUsername, labelName, search } = this.filterParams;
+ const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
let notParams = {};
if (Object.prototype.hasOwnProperty.call(this.filterParams, 'not')) {
@@ -35,6 +35,7 @@ export default {
{
'not[label_name][]': this.filterParams.not.labelName,
'not[author_username]': this.filterParams.not.authorUsername,
+ 'not[assignee_username]': this.filterParams.not.assigneeUsername,
},
undefined,
);
@@ -44,6 +45,7 @@ export default {
...notParams,
author_username: authorUsername,
'label_name[]': labelName,
+ assignee_username: assigneeUsername,
search,
};
},
@@ -62,7 +64,7 @@ export default {
this.performSearch();
},
getFilteredSearchValue() {
- const { authorUsername, labelName, search } = this.filterParams;
+ const { authorUsername, labelName, assigneeUsername, search } = this.filterParams;
const filteredSearchValue = [];
if (authorUsername) {
@@ -72,6 +74,13 @@ export default {
});
}
+ if (assigneeUsername) {
+ filteredSearchValue.push({
+ type: 'assignee_username',
+ value: { data: assigneeUsername, operator: '=' },
+ });
+ }
+
if (labelName?.length) {
filteredSearchValue.push(
...labelName.map((label) => ({
@@ -88,6 +97,13 @@ export default {
});
}
+ if (this.filterParams['not[assigneeUsername]']) {
+ filteredSearchValue.push({
+ type: 'assignee_username',
+ value: { data: this.filterParams['not[assigneeUsername]'], operator: '!=' },
+ });
+ }
+
if (this.filterParams['not[labelName]']) {
filteredSearchValue.push(
...this.filterParams['not[labelName]'].map((label) => ({
@@ -121,6 +137,9 @@ export default {
case 'author_username':
filterParams.authorUsername = filter.value.data;
break;
+ case 'assignee_username':
+ filterParams.assigneeUsername = filter.value.data;
+ break;
case 'label_name':
labels.push(filter.value.data);
break;
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index aa75a0d68f5..386ed6bd0a1 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -2,9 +2,9 @@
import { GlModal, GlAlert } from '@gitlab/ui';
import { mapGetters, mapActions, mapState } from 'vuex';
import ListLabel from '~/boards/models/label';
+import { TYPE_ITERATION, TYPE_MILESTONE, TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import { visitUrl } from '~/lib/utils/url_utility';
+import { getParameterByName, visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import { fullLabelId, fullBoardId } from '../boards_util';
import { formType } from '../constants';
@@ -188,21 +188,19 @@ export default {
};
},
issueBoardScopeMutationVariables() {
- /* eslint-disable @gitlab/require-i18n-strings */
return {
weight: this.board.weight,
assigneeId: this.board.assignee?.id
- ? convertToGraphQLId('User', this.board.assignee.id)
+ ? convertToGraphQLId(TYPE_USER, this.board.assignee.id)
: null,
milestoneId:
this.board.milestone?.id || this.board.milestone?.id === 0
- ? convertToGraphQLId('Milestone', this.board.milestone.id)
+ ? convertToGraphQLId(TYPE_MILESTONE, this.board.milestone.id)
: null,
iterationId: this.board.iteration_id
- ? convertToGraphQLId('Iteration', this.board.iteration_id)
+ ? convertToGraphQLId(TYPE_ITERATION, this.board.iteration_id)
: null,
};
- /* eslint-enable @gitlab/require-i18n-strings */
},
boardScopeMutationVariables() {
return {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 81740b5cd17..8dca6be853f 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -6,6 +6,7 @@ import { sortableStart, sortableEnd } from '~/boards/mixins/sortable_default_opt
import { sprintf, __ } from '~/locale';
import defaultSortableConfig from '~/sortable/sortable_config';
import Tracking from '~/tracking';
+import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import BoardCard from './board_card.vue';
import BoardNewIssue from './board_new_issue.vue';
@@ -21,6 +22,7 @@ export default {
components: {
BoardCard,
BoardNewIssue,
+ BoardNewEpic: () => import('ee_component/boards/components/board_new_epic.vue'),
GlLoadingIcon,
GlIntersectionObserver,
},
@@ -49,6 +51,7 @@ export default {
scrollOffset: 250,
showCount: false,
showIssueForm: false,
+ showEpicForm: false,
};
},
computed: {
@@ -64,6 +67,9 @@ export default {
issuableType: this.isEpicBoard ? 'epics' : 'issues',
});
},
+ toggleFormEventPrefix() {
+ return this.isEpicBoard ? toggleFormEventPrefix.epic : toggleFormEventPrefix.issue;
+ },
boardItemsSizeExceedsMax() {
return this.list.maxIssueCount > 0 && this.listItemsCount > this.list.maxIssueCount;
},
@@ -76,6 +82,12 @@ export default {
loadingMore() {
return this.listsFlags[this.list.id]?.isLoadingMore;
},
+ epicCreateFormVisible() {
+ return this.isEpicBoard && this.list.listType !== 'closed' && this.showEpicForm;
+ },
+ issueCreateFormVisible() {
+ return !this.isEpicBoard && this.list.listType !== 'closed' && this.showIssueForm;
+ },
listRef() {
// When list is draggable, the reference to the list needs to be accessed differently
return this.canAdminList ? this.$refs.list.$el : this.$refs.list;
@@ -116,9 +128,10 @@ export default {
'list.id': {
handler(id, oldVal) {
if (id) {
- eventHub.$on(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ eventHub.$on(`${this.toggleFormEventPrefix}${this.list.id}`, this.toggleForm);
eventHub.$on(`scroll-board-list-${this.list.id}`, this.scrollToTop);
- eventHub.$off(`toggle-issue-form-${oldVal}`, this.toggleForm);
+
+ eventHub.$off(`${this.toggleFormEventPrefix}${oldVal}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${oldVal}`, this.scrollToTop);
}
},
@@ -126,7 +139,7 @@ export default {
},
},
beforeDestroy() {
- eventHub.$off(`toggle-issue-form-${this.list.id}`, this.toggleForm);
+ eventHub.$off(`${this.toggleFormEventPrefix}${this.list.id}`, this.toggleForm);
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
methods: {
@@ -147,7 +160,11 @@ export default {
this.fetchItemsForList({ listId: this.list.id, fetchNext: true });
},
toggleForm() {
- this.showIssueForm = !this.showIssueForm;
+ if (this.isEpicBoard) {
+ this.showEpicForm = !this.showEpicForm;
+ } else {
+ this.showIssueForm = !this.showIssueForm;
+ }
},
onReachingListBottom() {
if (!this.loadingMore && this.hasNextPage) {
@@ -225,9 +242,10 @@ export default {
:aria-label="$options.i18n.loading"
data-testid="board_list_loading"
>
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
</div>
- <board-new-issue v-if="list.listType !== 'closed' && showIssueForm" :list="list" />
+ <board-new-issue v-if="issueCreateFormVisible" :list="list" />
+ <board-new-epic v-if="epicCreateFormVisible" :list="list" />
<component
:is="treeRootWrapper"
v-show="!loading"
@@ -255,6 +273,7 @@ export default {
<li v-if="showCount" class="board-list-count gl-text-center" data-issue-id="-1">
<gl-loading-icon
v-if="loadingMore"
+ size="sm"
:label="$options.i18n.loadingMoreboardItems"
data-testid="count-loading-icon"
/>
diff --git a/app/assets/javascripts/boards/components/board_list_deprecated.vue b/app/assets/javascripts/boards/components/board_list_deprecated.vue
index 9b3e7e1547d..fabaf7a85f5 100644
--- a/app/assets/javascripts/boards/components/board_list_deprecated.vue
+++ b/app/assets/javascripts/boards/components/board_list_deprecated.vue
@@ -429,7 +429,7 @@ export default {
data-qa-selector="board_list_cards_area"
>
<div v-if="loading" class="board-list-loading text-center" :aria-label="__('Loading issues')">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
</div>
<board-new-issue v-if="list.type !== 'closed' && showIssueForm" :list="list" />
<ul
@@ -450,7 +450,7 @@ export default {
:disabled="disabled"
/>
<li v-if="showCount" class="board-list-count text-center" data-issue-id="-1">
- <gl-loading-icon v-show="list.loadingMore" label="Loading more issues" />
+ <gl-loading-icon v-show="list.loadingMore" size="sm" label="Loading more issues" />
<span v-if="list.issues.length === list.issuesSize">{{ __('Showing all issues') }}</span>
<span v-else>{{ paginatedIssueText }}</span>
</li>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bf8396f52a6..8d5f0f7eb89 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -16,13 +16,14 @@ import { n__, s__, __ } from '~/locale';
import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import AccessorUtilities from '../../lib/utils/accessor';
-import { inactiveId, LIST, ListType } from '../constants';
+import { inactiveId, LIST, ListType, toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import ItemCount from './item_count.vue';
export default {
i18n: {
newIssue: __('New issue'),
+ newEpic: s__('Boards|New epic'),
listSettings: __('List settings'),
expand: s__('Boards|Expand'),
collapse: s__('Boards|Collapse'),
@@ -72,7 +73,7 @@ export default {
},
computed: {
...mapState(['activeId']),
- ...mapGetters(['isEpicBoard']),
+ ...mapGetters(['isEpicBoard', 'isSwimlanesOn']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@@ -102,7 +103,7 @@ export default {
},
showListHeaderActions() {
if (this.isLoggedIn) {
- return this.isNewIssueShown || this.isSettingsShown;
+ return this.isNewIssueShown || this.isNewEpicShown || this.isSettingsShown;
}
return false;
},
@@ -124,6 +125,9 @@ export default {
isNewIssueShown() {
return (this.listType === ListType.backlog || this.showListHeaderButton) && !this.isEpicBoard;
},
+ isNewEpicShown() {
+ return this.isEpicBoard && this.listType !== ListType.closed;
+ },
isSettingsShown() {
return (
this.listType !== ListType.backlog && this.showListHeaderButton && !this.list.collapsed
@@ -165,7 +169,17 @@ export default {
},
showNewIssueForm() {
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ if (this.isSwimlanesOn) {
+ eventHub.$emit('open-unassigned-lane');
+ this.$nextTick(() => {
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ });
+ } else {
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
+ }
+ },
+ showNewEpicForm() {
+ eventHub.$emit(`${toggleFormEventPrefix.epic}${this.list.id}`);
},
toggleExpanded() {
const collapsed = !this.list.collapsed;
@@ -342,7 +356,7 @@ export default {
<!-- EE end -->
<div
- class="issue-count-badge gl-display-inline-flex gl-pr-0 no-drag gl-text-gray-500"
+ class="issue-count-badge gl-display-inline-flex gl-pr-2 no-drag gl-text-gray-500"
data-testid="issue-count-badge"
:class="{
'gl-display-none!': list.collapsed && isSwimlanesHeader,
@@ -380,6 +394,17 @@ export default {
/>
<gl-button
+ v-if="isNewEpicShown"
+ v-show="!list.collapsed"
+ v-gl-tooltip.hover
+ :aria-label="$options.i18n.newEpic"
+ :title="$options.i18n.newEpic"
+ class="no-drag"
+ icon="plus"
+ @click="showNewEpicForm"
+ />
+
+ <gl-button
v-if="isSettingsShown"
ref="settingsBtn"
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index a63b49f9508..caeecb25227 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -4,13 +4,13 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import { getMilestone } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
import { __ } from '~/locale';
+import { toggleFormEventPrefix } from '../constants';
import eventHub from '../eventhub';
import ProjectSelect from './project_select.vue';
export default {
name: 'BoardNewIssue',
i18n: {
- submit: __('Create issue'),
cancel: __('Cancel'),
},
components: {
@@ -32,7 +32,15 @@ export default {
},
computed: {
...mapState(['selectedProject']),
- ...mapGetters(['isGroupBoard']),
+ ...mapGetters(['isGroupBoard', 'isEpicBoard']),
+ /**
+ * We've extended this component in EE where
+ * submitButtonTitle returns a different string
+ * hence this is kept as a computed prop.
+ */
+ submitButtonTitle() {
+ return __('Create issue');
+ },
disabled() {
if (this.isGroupBoard) {
return this.title === '' || !this.selectedProject.name;
@@ -50,9 +58,7 @@ export default {
},
methods: {
...mapActions(['addListNewIssue']),
- submit(e) {
- e.preventDefault();
-
+ submit() {
const { title } = this;
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
@@ -76,7 +82,7 @@ export default {
},
reset() {
this.title = '';
- eventHub.$emit(`toggle-issue-form-${this.list.id}`);
+ eventHub.$emit(`${toggleFormEventPrefix.issue}${this.list.id}`);
},
},
};
@@ -85,7 +91,7 @@ export default {
<template>
<div class="board-new-issue-form">
<div class="board-card position-relative p-3 rounded">
- <form ref="submitForm" @submit="submit">
+ <form ref="submitForm" @submit.prevent="submit">
<label :for="inputFieldId" class="label-bold">{{ __('Title') }}</label>
<input
:id="inputFieldId"
@@ -96,7 +102,7 @@ export default {
name="issue_title"
autocomplete="off"
/>
- <project-select v-if="isGroupBoard" :group-id="groupId" :list="list" />
+ <project-select v-if="isGroupBoard && !isEpicBoard" :group-id="groupId" :list="list" />
<div class="clearfix gl-mt-3">
<gl-button
ref="submitButton"
@@ -106,7 +112,7 @@ export default {
category="primary"
type="submit"
>
- {{ $options.i18n.submit }}
+ {{ submitButtonTitle }}
</gl-button>
<gl-button
ref="cancelButton"
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 75975c77df5..c089a6a39af 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlDrawer, GlLabel } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
import { mapActions, mapState, mapGetters } from 'vuex';
import { LIST, ListType, ListTypeTitles } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
@@ -9,14 +10,13 @@ import eventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-// NOTE: need to revisit how we handle headerHeight, because we have so many different header and footer options.
export default {
- headerHeight: process.env.NODE_ENV === 'development' ? '75px' : '40px',
listSettingsText: __('List settings'),
components: {
GlButton,
GlDrawer,
GlLabel,
+ MountingPortal,
BoardSettingsSidebarWipLimit: () =>
import('ee_component/boards/components/board_settings_wip_limit.vue'),
BoardSettingsListTypes: () =>
@@ -24,6 +24,7 @@ export default {
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
inject: ['canAdminList'],
+ inheritAttrs: false,
data() {
return {
ListType,
@@ -86,43 +87,45 @@ export default {
</script>
<template>
- <gl-drawer
- v-if="showSidebar"
- class="js-board-settings-sidebar"
- :open="isSidebarOpen"
- :header-height="$options.headerHeight"
- @close="unsetActiveId"
- >
- <template #header>{{ $options.listSettingsText }}</template>
- <template v-if="isSidebarOpen">
- <div v-if="boardListType === ListType.label">
- <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
- <gl-label
- :title="activeListLabel.title"
- :background-color="activeListLabel.color"
- :scoped="showScopedLabels(activeListLabel)"
- />
- </div>
+ <mounting-portal mount-to="#js-right-sidebar-portal" name="board-settings-sidebar" append>
+ <gl-drawer
+ v-if="showSidebar"
+ v-bind="$attrs"
+ class="js-board-settings-sidebar gl-absolute"
+ :open="isSidebarOpen"
+ @close="unsetActiveId"
+ >
+ <template #title>{{ $options.listSettingsText }}</template>
+ <template v-if="isSidebarOpen">
+ <div v-if="boardListType === ListType.label">
+ <label class="js-list-label gl-display-block">{{ listTypeTitle }}</label>
+ <gl-label
+ :title="activeListLabel.title"
+ :background-color="activeListLabel.color"
+ :scoped="showScopedLabels(activeListLabel)"
+ />
+ </div>
- <board-settings-list-types
- v-else
- :active-list="activeList"
- :board-list-type="boardListType"
- />
- <board-settings-sidebar-wip-limit
- v-if="isWipLimitsOn"
- :max-issue-count="activeList.maxIssueCount"
- />
- <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4">
- <gl-button
- variant="danger"
- category="secondary"
- icon="remove"
- data-testid="remove-list"
- @click.stop="deleteBoard"
- >{{ __('Remove list') }}
- </gl-button>
- </div>
- </template>
- </gl-drawer>
+ <board-settings-list-types
+ v-else
+ :active-list="activeList"
+ :board-list-type="boardListType"
+ />
+ <board-settings-sidebar-wip-limit
+ v-if="isWipLimitsOn"
+ :max-issue-count="activeList.maxIssueCount"
+ />
+ <div v-if="canAdminList && !activeList.preset && activeList.id" class="gl-mt-4">
+ <gl-button
+ variant="danger"
+ category="secondary"
+ icon="remove"
+ data-testid="remove-list"
+ @click.stop="deleteBoard"
+ >{{ __('Remove list') }}
+ </gl-button>
+ </div>
+ </template>
+ </gl-drawer>
+ </mounting-portal>
</template>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index 55bc91cbcff..21a34182369 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -105,7 +105,7 @@ export default Vue.extend({
closeSidebar() {
this.detail.issue = {};
},
- setAssignees(assignees) {
+ setAssignees({ assignees }) {
boardsStore.detail.issue.setAssignees(assignees);
},
showScopedLabels(label) {
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index 5124467136e..98027917221 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -327,7 +327,7 @@ export default {
:class="scrollFadeClass"
></div>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
diff --git a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
index 85c7b27336b..c1536dff2c6 100644
--- a/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
+++ b/app/assets/javascripts/boards/components/boards_selector_deprecated.vue
@@ -316,7 +316,7 @@ export default {
:class="scrollFadeClass"
></div>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<div v-if="canAdminBoard">
<gl-dropdown-divider />
diff --git a/app/assets/javascripts/boards/components/issue_board_filtered_search.vue b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
new file mode 100644
index 00000000000..d8dac17d326
--- /dev/null
+++ b/app/assets/javascripts/boards/components/issue_board_filtered_search.vue
@@ -0,0 +1,102 @@
+<script>
+import BoardFilteredSearch from '~/boards/components/board_filtered_search.vue';
+import issueBoardFilters from '~/boards/issue_board_filters';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
+import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+
+export default {
+ i18n: {
+ search: __('Search'),
+ label: __('Label'),
+ author: __('Author'),
+ assignee: __('Assignee'),
+ is: __('is'),
+ isNot: __('is not'),
+ },
+ components: { BoardFilteredSearch },
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ boardType: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ tokens() {
+ const { label, is, isNot, author, assignee } = this.$options.i18n;
+ const { fetchAuthors, fetchLabels } = issueBoardFilters(
+ this.$apollo,
+ this.fullPath,
+ this.boardType,
+ );
+
+ return [
+ {
+ icon: 'labels',
+ title: label,
+ type: 'label_name',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ token: LabelToken,
+ unique: false,
+ symbol: '~',
+ fetchLabels,
+ },
+ {
+ icon: 'pencil',
+ title: author,
+ type: 'author_username',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ symbol: '@',
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: this.preloadedAuthors(),
+ },
+ {
+ icon: 'user',
+ title: assignee,
+ type: 'assignee_username',
+ operators: [
+ { value: '=', description: is },
+ { value: '!=', description: isNot },
+ ],
+ token: AuthorToken,
+ unique: true,
+ fetchAuthors,
+ preloadedAuthors: this.preloadedAuthors(),
+ },
+ ];
+ },
+ },
+ methods: {
+ preloadedAuthors() {
+ return gon?.current_user_id
+ ? [
+ {
+ id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatarUrl: gon.current_user_avatar_url,
+ },
+ ]
+ : [];
+ },
+ },
+};
+</script>
+
+<template>
+ <board-filtered-search data-testid="issue-board-filtered-search" :tokens="tokens" />
+</template>
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js b/app/assets/javascripts/boards/components/new_list_dropdown.js
index 2fd16f06455..6eb1dbfb46a 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import store from '~/boards/stores';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -53,7 +53,9 @@ export default function initNewListDropdown() {
data(term, callback) {
const reqFailed = () => {
$dropdownToggle.data('bs.dropdown').hide();
- flash(__('Error fetching labels.'));
+ createFlash({
+ message: __('Error fetching labels.'),
+ });
};
if (store.getters.shouldUseGraphQL) {
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 77b6af77652..1412411c275 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -126,7 +126,7 @@ export default {
v-show="groupProjectsFlags.isLoading"
data-testid="dropdown-text-loading-icon"
>
- <gl-loading-icon class="gl-mx-auto" />
+ <gl-loading-icon class="gl-mx-auto" size="sm" />
</gl-dropdown-text>
<gl-dropdown-text
v-if="isFetchResultEmpty && !groupProjectsFlags.isLoading"
diff --git a/app/assets/javascripts/boards/components/project_select_deprecated.vue b/app/assets/javascripts/boards/components/project_select_deprecated.vue
index afe161d9c54..fc95ba0461d 100644
--- a/app/assets/javascripts/boards/components/project_select_deprecated.vue
+++ b/app/assets/javascripts/boards/components/project_select_deprecated.vue
@@ -136,7 +136,7 @@ export default {
{{ project.namespacedName }}
</gl-dropdown-item>
<gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
- <gl-loading-icon class="gl-mx-auto" />
+ <gl-loading-icon class="gl-mx-auto" size="sm" />
</gl-dropdown-text>
<gl-dropdown-text v-if="isFetchResultEmpty && !isFetching" data-testid="empty-result-message">
<span class="gl-text-gray-500">{{ $options.i18n.emptySearchResult }}</span>
diff --git a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
index 352a25ef6d9..84802650dad 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_editable_item.vue
@@ -93,7 +93,7 @@ export default {
<slot name="title">
<span data-testid="title">{{ title }}</span>
</slot>
- <gl-loading-icon v-if="loading" inline class="gl-ml-2" />
+ <gl-loading-icon v-if="loading" size="sm" inline class="gl-ml-2" />
</span>
<gl-button
v-if="canUpdate"
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 80a8fc99895..21ef70582a4 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -45,6 +45,11 @@ export const formType = {
edit: 'edit',
};
+export const toggleFormEventPrefix = {
+ epic: 'toggle-epic-form-',
+ issue: 'toggle-issue-form-',
+};
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
diff --git a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
index 3c5f4b3e3bd..70eb1dfbf7e 100644
--- a/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_set_labels.mutation.graphql
@@ -1,6 +1,7 @@
mutation issueSetLabels($input: UpdateIssueInput!) {
updateIssue(input: $input) {
issue {
+ id
labels {
nodes {
id
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index fb347ce852d..de7c8a3bd6b 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -1,4 +1,5 @@
import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import PortalVue from 'portal-vue';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mapActions, mapGetters } from 'vuex';
@@ -24,6 +25,7 @@ import '~/boards/filters/due_date_filters';
import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import FilteredSearchBoards from '~/boards/filtered_search_boards';
+import initBoardsFilteredSearch from '~/boards/mount_filtered_search_issue_boards';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
import toggleFocusMode from '~/boards/toggle_focus';
@@ -41,6 +43,7 @@ import boardConfigToggle from './config_toggle';
import mountMultipleBoardsSwitcher from './mount_multiple_boards_switcher';
Vue.use(VueApollo);
+Vue.use(PortalVue);
const fragmentMatcher = new IntrospectionFragmentMatcher({
introspectionQueryResultData,
@@ -76,6 +79,10 @@ export default () => {
issueBoardsApp.$destroy(true);
}
+ if (gon?.features?.issueBoardsFilteredSearch) {
+ initBoardsFilteredSearch(apolloProvider);
+ }
+
if (!gon?.features?.graphqlBoardLists) {
boardsStore.create();
boardsStore.setTimeTrackingLimitToHours($boardApp.dataset.timeTrackingLimitToHours);
@@ -182,9 +189,14 @@ export default () => {
eventHub.$off('initialBoardLoad', this.initialBoardLoad);
},
mounted() {
- this.filterManager = new FilteredSearchBoards(boardsStore.filter, true, boardsStore.cantEdit);
-
- this.filterManager.setup();
+ if (!gon?.features?.issueBoardsFilteredSearch) {
+ this.filterManager = new FilteredSearchBoards(
+ boardsStore.filter,
+ true,
+ boardsStore.cantEdit,
+ );
+ this.filterManager.setup();
+ }
this.performSearch();
@@ -304,9 +316,11 @@ export default () => {
// eslint-disable-next-line no-new, @gitlab/no-runtime-template-compiler
new Vue({
el: document.getElementById('js-add-list'),
- data: {
- filters: boardsStore.state.filters,
- ...getMilestoneTitle($boardApp),
+ data() {
+ return {
+ filters: boardsStore.state.filters,
+ ...getMilestoneTitle($boardApp),
+ };
},
mounted() {
initNewListDropdown();
diff --git a/app/assets/javascripts/boards/issue_board_filters.js b/app/assets/javascripts/boards/issue_board_filters.js
new file mode 100644
index 00000000000..699d7e12de4
--- /dev/null
+++ b/app/assets/javascripts/boards/issue_board_filters.js
@@ -0,0 +1,47 @@
+import groupBoardMembers from '~/boards/graphql/group_board_members.query.graphql';
+import projectBoardMembers from '~/boards/graphql/project_board_members.query.graphql';
+import { BoardType } from './constants';
+import boardLabels from './graphql/board_labels.query.graphql';
+
+export default function issueBoardFilters(apollo, fullPath, boardType) {
+ const isGroupBoard = boardType === BoardType.group;
+ const isProjectBoard = boardType === BoardType.project;
+ const transformLabels = ({ data }) => {
+ return isGroupBoard ? data.group?.labels.nodes || [] : data.project?.labels.nodes || [];
+ };
+
+ const boardAssigneesQuery = () => {
+ return isGroupBoard ? groupBoardMembers : projectBoardMembers;
+ };
+
+ const fetchAuthors = (authorsSearchTerm) => {
+ return apollo
+ .query({
+ query: boardAssigneesQuery(),
+ variables: {
+ fullPath,
+ search: authorsSearchTerm,
+ },
+ })
+ .then(({ data }) => data.workspace?.assignees.nodes.map(({ user }) => user));
+ };
+
+ const fetchLabels = (labelSearchTerm) => {
+ return apollo
+ .query({
+ query: boardLabels,
+ variables: {
+ fullPath,
+ searchTerm: labelSearchTerm,
+ isGroup: isGroupBoard,
+ isProject: isProjectBoard,
+ },
+ })
+ .then(transformLabels);
+ };
+
+ return {
+ fetchLabels,
+ fetchAuthors,
+ };
+}
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js b/app/assets/javascripts/boards/mixins/sortable_default_options.js
index a95d749d71c..1bb0ee5b7e3 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js
@@ -1,6 +1,6 @@
/* global DocumentTouch */
-import sortableConfig from 'ee_else_ce/sortable/sortable_config';
+import sortableConfig from '~/sortable/sortable_config';
export function sortableStart() {
document.body.classList.add('is-dragging');
diff --git a/app/assets/javascripts/boards/models/list.js b/app/assets/javascripts/boards/models/list.js
index 6c6e2522d92..ab24532d87f 100644
--- a/app/assets/javascripts/boards/models/list.js
+++ b/app/assets/javascripts/boards/models/list.js
@@ -1,5 +1,5 @@
/* eslint-disable class-methods-use-this */
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import boardsStore from '../stores/boards_store';
import ListAssignee from './assignee';
@@ -127,7 +127,11 @@ class List {
moveBeforeId,
moveAfterId,
})
- .catch(() => flash(__('Something went wrong while moving issues.')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while moving issues.'),
+ }),
+ );
}
updateIssueLabel(issue, listFrom, moveBeforeId, moveAfterId) {
@@ -145,7 +149,11 @@ class List {
moveBeforeId,
moveAfterId,
})
- .catch(() => flash(__('Something went wrong while moving issues.')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while moving issues.'),
+ }),
+ );
}
findIssue(id) {
diff --git a/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
new file mode 100644
index 00000000000..7732091ef34
--- /dev/null
+++ b/app/assets/javascripts/boards/mount_filtered_search_issue_boards.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
+import store from '~/boards/stores';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { queryToObject } from '~/lib/utils/url_utility';
+
+export default (apolloProvider) => {
+ const el = document.getElementById('js-issue-board-filtered-search');
+ const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
+
+ const initialFilterParams = {
+ ...convertObjectPropsToCamelCase(rawFilterParams, {}),
+ };
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ provide: {
+ initialFilterParams,
+ },
+ store, // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/324094
+ apolloProvider,
+ render: (createElement) =>
+ createElement(IssueBoardFilteredSearch, {
+ props: { fullPath: store.state?.fullPath || '', boardType: store.state?.boardType || '' },
+ }),
+ });
+};
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index d4893f9eca7..0f1b72146c9 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -18,7 +18,9 @@ import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create
import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
-import { convertObjectPropsToCamelCase, urlParamsToObject } from '~/lib/utils/common_utils';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { urlParamsToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import {
formatBoardLists,
@@ -74,6 +76,7 @@ export default {
performSearch({ dispatch }) {
dispatch(
'setFilters',
+ // eslint-disable-next-line import/no-deprecated
convertObjectPropsToCamelCase(urlParamsToObject(window.location.search)),
);
@@ -170,8 +173,9 @@ export default {
addList: ({ commit, dispatch, getters }, list) => {
commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
+
dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
},
@@ -237,7 +241,7 @@ export default {
},
updateList: (
- { commit, state: { issuableType } },
+ { commit, state: { issuableType, boardItemsByListId = {} }, dispatch },
{ listId, position, collapsed, backupList },
) => {
gqlClient
@@ -252,6 +256,12 @@ export default {
.then(({ data }) => {
if (data?.updateBoardList?.errors.length) {
commit(types.UPDATE_LIST_FAILURE, backupList);
+ return;
+ }
+
+ // Only fetch when board items havent been fetched on a collapsed list
+ if (!boardItemsByListId[listId]) {
+ dispatch('fetchItemsForList', { listId });
}
})
.catch(() => {
@@ -285,7 +295,7 @@ export default {
commit(types.REMOVE_LIST_FAILURE, listsBackup);
} else {
dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog).id,
+ listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
});
}
},
@@ -296,6 +306,8 @@ export default {
},
fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
+ if (!listId) return null;
+
if (!fetchNext) {
commit(types.RESET_ITEMS_FOR_LIST, listId);
}
@@ -469,11 +481,11 @@ export default {
}
},
- setAssignees: ({ commit, getters }, assigneeUsernames) => {
+ setAssignees: ({ commit }, { id, assignees }) => {
commit('UPDATE_BOARD_ITEM_BY_ID', {
- itemId: getters.activeBoardItem.id,
+ itemId: id,
prop: 'assignees',
- value: assigneeUsernames,
+ value: assignees,
});
},
@@ -701,4 +713,7 @@ export default {
unsetError: ({ commit }) => {
commit(types.SET_ERROR, undefined);
},
+
+ // EE action needs CE empty equivalent
+ setActiveItemWeight: () => {},
};
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 092f81ad279..49c40c7776a 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -7,13 +7,9 @@ import BoardsStoreEE from 'ee_else_ce/boards/stores/boards_store_ee';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import createDefaultClient from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
-import {
- urlParamsToObject,
- getUrlParamsArray,
- parseBoolean,
- convertObjectPropsToCamelCase,
-} from '~/lib/utils/common_utils';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { parseBoolean, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { mergeUrlParams, urlParamsToObject, getUrlParamsArray } from '~/lib/utils/url_utility';
import { ListType, flashAnimationDuration } from '../constants';
import eventHub from '../eventhub';
import ListAssignee from '../models/assignee';
@@ -601,6 +597,7 @@ const boardsStore = {
getListIssues(list, emptyIssues = true) {
const data = {
+ // eslint-disable-next-line import/no-deprecated
...urlParamsToObject(this.filter.path),
page: list.page,
};
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
index b61ecc5ccb6..140c9ef7ac4 100644
--- a/app/assets/javascripts/boards/stores/getters.js
+++ b/app/assets/javascripts/boards/stores/getters.js
@@ -16,7 +16,7 @@ export default {
},
activeBoardItem: (state) => {
- return state.boardItems[state.activeId] || {};
+ return state.boardItems[state.activeId] || { iid: '', id: '', fullId: '' };
},
groupPathForActiveIssue: (_, getters) => {
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
index 6cd0a62657e..a32a100fa11 100644
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ b/app/assets/javascripts/boards/stores/mutations.js
@@ -35,13 +35,23 @@ export const addItemToList = ({ state, listId, itemId, moveBeforeId, moveAfterId
export default {
[mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const { boardType, disabled, boardId, fullBoardId, fullPath, boardConfig, issuableType } = data;
+ const {
+ allowSubEpics,
+ boardConfig,
+ boardId,
+ boardType,
+ disabled,
+ fullBoardId,
+ fullPath,
+ issuableType,
+ } = data;
+ state.allowSubEpics = allowSubEpics;
+ state.boardConfig = boardConfig;
state.boardId = boardId;
- state.fullBoardId = fullBoardId;
- state.fullPath = fullPath;
state.boardType = boardType;
state.disabled = disabled;
- state.boardConfig = boardConfig;
+ state.fullBoardId = fullBoardId;
+ state.fullPath = fullPath;
state.issuableType = issuableType;
},
diff --git a/app/assets/javascripts/branches/components/delete_branch_button.vue b/app/assets/javascripts/branches/components/delete_branch_button.vue
index 5a5f49e25e7..6a6d4d48c52 100644
--- a/app/assets/javascripts/branches/components/delete_branch_button.vue
+++ b/app/assets/javascripts/branches/components/delete_branch_button.vue
@@ -47,12 +47,6 @@ export default {
},
},
computed: {
- variant() {
- if (this.disabled) {
- return 'default';
- }
- return 'danger';
- },
title() {
if (this.isProtectedBranch && this.disabled) {
return s__('Branches|Only a project maintainer or owner can delete a protected branch');
@@ -83,7 +77,7 @@ export default {
class="js-delete-branch-button"
data-qa-selector="delete_branch_button"
:disabled="disabled"
- :variant="variant"
+ variant="default"
:title="title"
:aria-label="title"
@click="openModal"
diff --git a/app/assets/javascripts/branches/divergence_graph.js b/app/assets/javascripts/branches/divergence_graph.js
index b88c056b00f..31cf9a18077 100644
--- a/app/assets/javascripts/branches/divergence_graph.js
+++ b/app/assets/javascripts/branches/divergence_graph.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import createFlash from '../flash';
+import createFlash from '~/flash';
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
import DivergenceGraph from './components/divergence_graph.vue';
diff --git a/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js b/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js
index c9eac44eb28..fdab188f6be 100644
--- a/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js
+++ b/app/assets/javascripts/captcha/captcha_modal_axios_interceptor.js
@@ -1,4 +1,33 @@
-const supportedMethods = ['patch', 'post', 'put'];
+const SUPPORTED_METHODS = ['patch', 'post', 'put'];
+
+function needsCaptchaResponse(err) {
+ return (
+ SUPPORTED_METHODS.includes(err?.config?.method) && err?.response?.data?.needs_captcha_response
+ );
+}
+
+const showCaptchaModalAndResubmit = async (axios, data, errConfig) => {
+ // NOTE: We asynchronously import and unbox the module. Since this is included globally, we don't
+ // do a regular import because that would increase the size of the webpack bundle.
+ const { waitForCaptchaToBeSolved } = await import('~/captcha/wait_for_captcha_to_be_solved');
+
+ // show the CAPTCHA modal and wait for it to be solved or closed
+ const captchaResponse = await waitForCaptchaToBeSolved(data.captcha_site_key);
+
+ // resubmit the original request with the captcha_response and spam_log_id in the headers
+ const originalData = JSON.parse(errConfig.data);
+ const originalHeaders = errConfig.headers;
+ return axios({
+ method: errConfig.method,
+ url: errConfig.url,
+ headers: {
+ ...originalHeaders,
+ 'X-GitLab-Captcha-Response': captchaResponse,
+ 'X-GitLab-Spam-Log-Id': data.spam_log_id,
+ },
+ data: originalData,
+ });
+};
export function registerCaptchaModalInterceptor(axios) {
return axios.interceptors.response.use(
@@ -6,29 +35,8 @@ export function registerCaptchaModalInterceptor(axios) {
return response;
},
(err) => {
- if (
- supportedMethods.includes(err?.config?.method) &&
- err?.response?.data?.needs_captcha_response
- ) {
- const { data } = err.response;
- const captchaSiteKey = data.captcha_site_key;
- const spamLogId = data.spam_log_id;
- // eslint-disable-next-line promise/no-promise-in-callback
- return import('~/captcha/wait_for_captcha_to_be_solved')
- .then(({ waitForCaptchaToBeSolved }) => waitForCaptchaToBeSolved(captchaSiteKey))
- .then((captchaResponse) => {
- const errConfig = err.config;
- const originalData = JSON.parse(errConfig.data);
- return axios({
- method: errConfig.method,
- url: errConfig.url,
- data: {
- ...originalData,
- captcha_response: captchaResponse,
- spam_log_id: spamLogId,
- },
- });
- });
+ if (needsCaptchaResponse(err)) {
+ return showCaptchaModalAndResubmit(axios, err.response.data, err.config);
}
return Promise.reject(err);
diff --git a/app/assets/javascripts/ci_lint/components/ci_lint.vue b/app/assets/javascripts/ci_lint/components/ci_lint.vue
index ced07dea7be..bc8a1f05ef5 100644
--- a/app/assets/javascripts/ci_lint/components/ci_lint.vue
+++ b/app/assets/javascripts/ci_lint/components/ci_lint.vue
@@ -2,7 +2,7 @@
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/lint_ci.mutation.graphql';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
components: {
@@ -12,7 +12,7 @@ export default {
GlLink,
GlAlert,
CiLintResults,
- EditorLite,
+ SourceEditor,
},
props: {
endpoint: {
@@ -93,7 +93,7 @@ export default {
<div class="js-file-title file-title clearfix">
{{ __('Contents of .gitlab-ci.yml') }}
</div>
- <editor-lite v-model="content" file-name="*.yml" />
+ <source-editor v-model="content" file-name="*.yml" />
</div>
</div>
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index 762b37a8216..c2c035963f4 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -1,18 +1,14 @@
import { GlToast } from '@gitlab/ui';
import Visibility from 'visibilityjs';
import Vue from 'vue';
+import createFlash from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import initProjectSelectDropdown from '~/project_select';
-import initServerlessSurveyBanner from '~/serverless/survey_banner';
-import { deprecatedCreateFlash as Flash } from '../flash';
import Poll from '../lib/utils/poll';
-import { s__, sprintf } from '../locale';
+import { s__ } from '../locale';
import PersistentUserCallout from '../persistent_user_callout';
import initSettingsPanels from '../settings_panels';
-import Applications from './components/applications.vue';
import RemoveClusterConfirmation from './components/remove_cluster_confirmation.vue';
-import { APPLICATION_STATUS, CROSSPLANE, KNATIVE } from './constants';
-import eventHub from './event_hub';
import ClustersService from './services/clusters_service';
import ClustersStore from './stores/clusters_store';
@@ -20,46 +16,20 @@ const Environments = () => import('ee_component/clusters/components/environments
Vue.use(GlToast);
-/**
- * Cluster page has 2 separate parts:
- * Toggle button and applications section
- *
- * - Polling status while creating or scheduled
- * - Update status area with the response result
- */
-
export default class Clusters {
constructor() {
const {
statusPath,
- installHelmPath,
- installIngressPath,
- installCertManagerPath,
- installRunnerPath,
- installJupyterPath,
- installKnativePath,
- updateKnativePath,
- installElasticStackPath,
- installCrossplanePath,
- installPrometheusPath,
- managePrometheusPath,
clusterEnvironmentsPath,
hasRbac,
providerType,
- preInstalledKnative,
- clusterType,
clusterStatus,
clusterStatusReason,
helpPath,
- helmHelpPath,
- ingressHelpPath,
- ingressDnsHelpPath,
environmentsHelpPath,
clustersHelpPath,
deployBoardsHelpPath,
- cloudRunHelpPath,
clusterId,
- ciliumHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.clusterId = clusterId;
@@ -69,38 +39,19 @@ export default class Clusters {
this.store = new ClustersStore();
this.store.setHelpPaths({
helpPath,
- helmHelpPath,
- ingressHelpPath,
- ingressDnsHelpPath,
environmentsHelpPath,
clustersHelpPath,
deployBoardsHelpPath,
- cloudRunHelpPath,
- ciliumHelpPath,
});
- this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
this.store.updateProviderType(providerType);
- this.store.updatePreInstalledKnative(preInstalledKnative);
this.store.updateRbac(hasRbac);
this.service = new ClustersService({
endpoint: statusPath,
- installHelmEndpoint: installHelmPath,
- installIngressEndpoint: installIngressPath,
- installCertManagerEndpoint: installCertManagerPath,
- installCrossplaneEndpoint: installCrossplanePath,
- installRunnerEndpoint: installRunnerPath,
- installPrometheusEndpoint: installPrometheusPath,
- installJupyterEndpoint: installJupyterPath,
- installKnativeEndpoint: installKnativePath,
- updateKnativeEndpoint: updateKnativePath,
- installElasticStackEndpoint: installElasticStackPath,
clusterEnvironmentsEndpoint: clusterEnvironmentsPath,
});
- this.installApplication = this.installApplication.bind(this);
-
this.errorContainer = document.querySelector('.js-cluster-error');
this.successContainer = document.querySelector('.js-cluster-success');
this.creatingContainer = document.querySelector('.js-cluster-creating');
@@ -109,14 +60,12 @@ export default class Clusters {
'.js-cluster-authentication-failure',
);
this.errorReasonContainer = this.errorContainer.querySelector('.js-error-reason');
- this.successApplicationContainer = document.querySelector('.js-cluster-application-notice');
this.tokenField = document.querySelector('.js-cluster-token');
initProjectSelectDropdown();
Clusters.initDismissableCallout();
initSettingsPanels();
- this.initApplications(clusterType);
this.initEnvironments();
if (clusterEnvironmentsPath && this.environments) {
@@ -143,38 +92,6 @@ export default class Clusters {
this.initRemoveClusterActions();
}
- initApplications(type) {
- const { store } = this;
- const el = document.querySelector('#js-cluster-applications');
-
- this.applications = new Vue({
- el,
- data() {
- return {
- state: store.state,
- };
- },
- render(createElement) {
- return createElement(Applications, {
- props: {
- type,
- applications: this.state.applications,
- helpPath: this.state.helpPath,
- helmHelpPath: this.state.helmHelpPath,
- ingressHelpPath: this.state.ingressHelpPath,
- managePrometheusPath: this.state.managePrometheusPath,
- ingressDnsHelpPath: this.state.ingressDnsHelpPath,
- cloudRunHelpPath: this.state.cloudRunHelpPath,
- providerType: this.state.providerType,
- preInstalledKnative: this.state.preInstalledKnative,
- rbac: this.state.rbac,
- ciliumHelpPath: this.state.ciliumHelpPath,
- },
- });
- },
- });
- }
-
initEnvironments() {
const { store } = this;
const el = document.querySelector('#js-cluster-environments');
@@ -242,30 +159,11 @@ export default class Clusters {
}
addListeners() {
- eventHub.$on('installApplication', this.installApplication);
- eventHub.$on('updateApplication', (data) => this.updateApplication(data));
- eventHub.$on('saveKnativeDomain', (data) => this.saveKnativeDomain(data));
- eventHub.$on('setKnativeDomain', (data) => this.setKnativeDomain(data));
- eventHub.$on('uninstallApplication', (data) => this.uninstallApplication(data));
- eventHub.$on('setCrossplaneProviderStack', (data) => this.setCrossplaneProviderStack(data));
// Add event listener to all the banner close buttons
this.addBannerCloseHandler(this.unreachableContainer, 'unreachable');
this.addBannerCloseHandler(this.authenticationFailureContainer, 'authentication_failure');
}
- removeListeners() {
- eventHub.$off('installApplication', this.installApplication);
- eventHub.$off('updateApplication', this.updateApplication);
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('saveKnativeDomain');
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('setKnativeDomain');
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('setCrossplaneProviderStack');
- // eslint-disable-next-line @gitlab/no-global-event-off
- eventHub.$off('uninstallApplication');
- }
-
initPolling(method, successCallback, errorCallback) {
this.poll = new Poll({
resource: this.service,
@@ -298,21 +196,17 @@ export default class Clusters {
}
static handleError() {
- Flash(s__('ClusterIntegration|Something went wrong on our end.'));
+ createFlash({
+ message: s__('ClusterIntegration|Something went wrong on our end.'),
+ });
}
handleClusterStatusSuccess(data) {
const prevStatus = this.store.state.status;
- const prevApplicationMap = { ...this.store.state.applications };
this.store.updateStateFromServer(data.data);
- this.checkForNewInstalls(prevApplicationMap, this.store.state.applications);
this.updateContainer(prevStatus, this.store.state.status, this.store.state.statusReason);
-
- if (this.store.state.applications[KNATIVE]?.status === APPLICATION_STATUS.INSTALLED) {
- initServerlessSurveyBanner();
- }
}
hideAll() {
@@ -323,27 +217,6 @@ export default class Clusters {
this.authenticationFailureContainer.classList.add('hidden');
}
- checkForNewInstalls(prevApplicationMap, newApplicationMap) {
- const appTitles = Object.keys(newApplicationMap)
- .filter(
- (appId) =>
- newApplicationMap[appId].status === APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== APPLICATION_STATUS.INSTALLED &&
- prevApplicationMap[appId].status !== null,
- )
- .map((appId) => newApplicationMap[appId].title);
-
- if (appTitles.length > 0) {
- const text = sprintf(
- s__('ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster'),
- {
- appList: appTitles.join(', '),
- },
- );
- Flash(text, 'notice', this.successApplicationContainer);
- }
- }
-
setBannerDismissedState(status, isDismissed) {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
@@ -416,91 +289,9 @@ export default class Clusters {
}
}
- installApplication({ id: appId, params }) {
- return Clusters.validateInstallation(appId, params)
- .then(() => {
- this.store.updateAppProperty(appId, 'requestReason', null);
- this.store.updateAppProperty(appId, 'statusReason', null);
- this.store.installApplication(appId);
-
- // eslint-disable-next-line promise/no-nesting
- this.service.installApplication(appId, params).catch(() => {
- this.store.notifyInstallFailure(appId);
- this.store.updateAppProperty(
- appId,
- 'requestReason',
- s__('ClusterIntegration|Request to begin installing failed'),
- );
- });
- })
- .catch((error) => this.store.updateAppProperty(appId, 'validationError', error));
- }
-
- static validateInstallation(appId, params) {
- return new Promise((resolve, reject) => {
- if (appId === CROSSPLANE && !params.stack) {
- reject(s__('ClusterIntegration|Select a stack to install Crossplane.'));
- return;
- }
-
- if (appId === KNATIVE && !params.hostname && !params.pages_domain_id) {
- reject(s__('ClusterIntegration|You must specify a domain before you can install Knative.'));
- return;
- }
-
- resolve();
- });
- }
-
- uninstallApplication({ id: appId }) {
- this.store.updateAppProperty(appId, 'requestReason', null);
- this.store.updateAppProperty(appId, 'statusReason', null);
-
- this.store.uninstallApplication(appId);
-
- return this.service.uninstallApplication(appId).catch(() => {
- this.store.notifyUninstallFailure(appId);
- this.store.updateAppProperty(
- appId,
- 'requestReason',
- s__('ClusterIntegration|Request to begin uninstalling failed'),
- );
- });
- }
-
- updateApplication({ id: appId, params }) {
- this.store.updateApplication(appId);
- this.service.installApplication(appId, params).catch(() => {
- this.store.notifyUpdateFailure(appId);
- });
- }
-
- saveKnativeDomain(data) {
- const appId = data.id;
- this.store.updateApplication(appId);
- this.service.updateApplication(appId, data.params).catch(() => {
- this.store.notifyUpdateFailure(appId);
- });
- }
-
- setKnativeDomain({ id: appId, domain, domainId }) {
- this.store.updateAppProperty(appId, 'isEditingDomain', true);
- this.store.updateAppProperty(appId, 'hostname', domain);
- this.store.updateAppProperty(appId, 'pagesDomain', domainId ? { id: domainId, domain } : null);
- this.store.updateAppProperty(appId, 'validationError', null);
- }
-
- setCrossplaneProviderStack(data) {
- const appId = data.id;
- this.store.updateAppProperty(appId, 'stack', data.stack.code);
- this.store.updateAppProperty(appId, 'validationError', null);
- }
-
destroy() {
this.destroyed = true;
- this.removeListeners();
-
if (this.poll) {
this.poll.stop();
}
@@ -508,7 +299,5 @@ export default class Clusters {
if (this.environments) {
this.environments.$destroy();
}
-
- this.applications.$destroy();
}
}
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
deleted file mode 100644
index a53b63ea592..00000000000
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ /dev/null
@@ -1,478 +0,0 @@
-<script>
-import { GlLink, GlModalDirective, GlSprintf, GlButton, GlAlert } from '@gitlab/ui';
-import { s__, __, sprintf } from '~/locale';
-import identicon from '../../vue_shared/components/identicon.vue';
-import { APPLICATION_STATUS, ELASTIC_STACK } from '../constants';
-import eventHub from '../event_hub';
-import UninstallApplicationButton from './uninstall_application_button.vue';
-import UninstallApplicationConfirmationModal from './uninstall_application_confirmation_modal.vue';
-import UpdateApplicationConfirmationModal from './update_application_confirmation_modal.vue';
-
-export default {
- components: {
- GlButton,
- identicon,
- GlLink,
- GlAlert,
- GlSprintf,
- UninstallApplicationButton,
- UninstallApplicationConfirmationModal,
- UpdateApplicationConfirmationModal,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- id: {
- type: String,
- required: true,
- },
- title: {
- type: String,
- required: true,
- },
- titleLink: {
- type: String,
- required: false,
- default: '',
- },
- manageLink: {
- type: String,
- required: false,
- default: '',
- },
- logoUrl: {
- type: String,
- required: false,
- default: '',
- },
- disabled: {
- type: Boolean,
- required: false,
- default: false,
- },
- installable: {
- type: Boolean,
- required: false,
- default: true,
- },
- uninstallable: {
- type: Boolean,
- required: false,
- default: false,
- },
- status: {
- type: String,
- required: false,
- default: '',
- },
- statusReason: {
- type: String,
- required: false,
- default: '',
- },
- requestReason: {
- type: String,
- required: false,
- default: '',
- },
- installed: {
- type: Boolean,
- required: false,
- default: false,
- },
- installFailed: {
- type: Boolean,
- required: false,
- default: false,
- },
- version: {
- type: String,
- required: false,
- default: '',
- },
- chartRepo: {
- type: String,
- required: false,
- default: '',
- },
- updateAvailable: {
- type: Boolean,
- required: false,
- },
- updateable: {
- type: Boolean,
- default: true,
- required: false,
- },
- updateSuccessful: {
- type: Boolean,
- required: false,
- default: false,
- },
- updateFailed: {
- type: Boolean,
- required: false,
- default: false,
- },
- uninstallFailed: {
- type: Boolean,
- required: false,
- default: false,
- },
- uninstallSuccessful: {
- type: Boolean,
- required: false,
- default: false,
- },
- installApplicationRequestParams: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- },
- computed: {
- isUnknownStatus() {
- return !this.isKnownStatus && this.status !== null;
- },
- isKnownStatus() {
- return Object.values(APPLICATION_STATUS).includes(this.status);
- },
- isInstalling() {
- return this.status === APPLICATION_STATUS.INSTALLING;
- },
- isExternallyInstalled() {
- return this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED;
- },
- canInstall() {
- return (
- this.status === APPLICATION_STATUS.NOT_INSTALLABLE ||
- this.status === APPLICATION_STATUS.INSTALLABLE ||
- this.status === APPLICATION_STATUS.UNINSTALLED ||
- this.isUnknownStatus
- );
- },
- hasLogo() {
- return Boolean(this.logoUrl);
- },
- identiconId() {
- // generate a deterministic integer id for the identicon background
- return this.id.charCodeAt(0);
- },
- rowJsClass() {
- return `js-cluster-application-row-${this.id}`;
- },
- displayUninstallButton() {
- return this.installed && this.uninstallable;
- },
- displayInstallButton() {
- return !this.installed || !this.uninstallable;
- },
- installButtonLoading() {
- return !this.status || this.isInstalling;
- },
- installButtonDisabled() {
- // Applications installed through the management project can
- // only be installed through the CI pipeline. Installation should
- // be disable in all states.
- if (!this.installable) return true;
-
- // Avoid the potential for the real-time data to say APPLICATION_STATUS.INSTALLABLE but
- // we already made a request to install and are just waiting for the real-time
- // to sync up.
- if (this.isInstalling) return true;
-
- if (!this.isKnownStatus) return false;
-
- return (
- this.status !== APPLICATION_STATUS.INSTALLABLE && this.status !== APPLICATION_STATUS.ERROR
- );
- },
- installButtonLabel() {
- let label;
- if (this.canInstall) {
- label = __('Install');
- } else if (this.isInstalling) {
- label = __('Installing');
- } else if (this.installed) {
- label = __('Installed');
- } else if (this.isExternallyInstalled) {
- label = __('Externally installed');
- }
-
- return label;
- },
- buttonGridCellClass() {
- return this.showManageButton || this.status === APPLICATION_STATUS.EXTERNALLY_INSTALLED
- ? 'section-25'
- : 'section-15';
- },
- showManageButton() {
- return this.manageLink && this.status === APPLICATION_STATUS.INSTALLED;
- },
- manageButtonLabel() {
- return __('Manage');
- },
- hasError() {
- return this.installFailed || this.uninstallFailed;
- },
- generalErrorDescription() {
- let errorDescription;
-
- if (this.installFailed) {
- errorDescription = s__('ClusterIntegration|Something went wrong while installing %{title}');
- } else if (this.uninstallFailed) {
- errorDescription = s__(
- 'ClusterIntegration|Something went wrong while uninstalling %{title}',
- );
- }
-
- return sprintf(errorDescription, { title: this.title });
- },
- updateFailureDescription() {
- return s__('ClusterIntegration|Update failed. Please check the logs and try again.');
- },
- updateSuccessDescription() {
- return sprintf(s__('ClusterIntegration|%{title} updated successfully.'), {
- title: this.title,
- });
- },
- updateButtonLabel() {
- let label;
- if (this.updateAvailable && !this.updateFailed && !this.isUpdating) {
- label = __('Update');
- } else if (this.isUpdating) {
- label = __('Updating');
- } else if (this.updateFailed) {
- label = __('Retry update');
- }
-
- return label;
- },
- updatingNeedsConfirmation() {
- if (this.version) {
- const majorVersion = parseInt(this.version.split('.')[0], 10);
-
- if (!Number.isNaN(majorVersion)) {
- return this.id === ELASTIC_STACK && majorVersion < 3;
- }
- }
-
- return false;
- },
- isUpdating() {
- // Since upgrading is handled asynchronously on the backend we need this check to prevent any delay on the frontend
- return this.status === APPLICATION_STATUS.UPDATING;
- },
- shouldShowUpdateDetails() {
- // This method only returns true when;
- // Update was successful OR Update failed
- // AND new update is unavailable AND version information is present.
- return (this.updateSuccessful || this.updateFailed) && !this.updateAvailable && this.version;
- },
- uninstallSuccessDescription() {
- return sprintf(s__('ClusterIntegration|%{title} uninstalled successfully.'), {
- title: this.title,
- });
- },
- updateModalId() {
- return `update-${this.id}`;
- },
- uninstallModalId() {
- return `uninstall-${this.id}`;
- },
- },
- watch: {
- updateSuccessful(updateSuccessful) {
- if (updateSuccessful) {
- this.$toast.show(this.updateSuccessDescription);
- }
- },
- uninstallSuccessful(uninstallSuccessful) {
- if (uninstallSuccessful) {
- this.$toast.show(this.uninstallSuccessDescription);
- }
- },
- },
- methods: {
- installClicked() {
- if (this.disabled || this.installButtonDisabled) return;
-
- eventHub.$emit('installApplication', {
- id: this.id,
- params: this.installApplicationRequestParams,
- });
- },
- updateConfirmed() {
- if (this.isUpdating) return;
-
- eventHub.$emit('updateApplication', {
- id: this.id,
- params: this.installApplicationRequestParams,
- });
- },
- uninstallConfirmed() {
- eventHub.$emit('uninstallApplication', {
- id: this.id,
- });
- },
- },
-};
-</script>
-
-<template>
- <div
- :class="[
- rowJsClass,
- installed && 'cluster-application-installed',
- disabled && 'cluster-application-disabled',
- ]"
- class="cluster-application-row gl-responsive-table-row gl-responsive-table-row-col-span"
- :data-qa-selector="id"
- >
- <div class="gl-responsive-table-row-layout" role="row">
- <div class="table-section gl-mr-3 section-align-top" role="gridcell">
- <img
- v-if="hasLogo"
- :src="logoUrl"
- :alt="`${title} logo`"
- class="cluster-application-logo avatar s40"
- />
- <identicon v-else :entity-id="identiconId" :entity-name="title" size-class="s40" />
- </div>
- <div class="table-section cluster-application-description section-wrap" role="gridcell">
- <strong>
- <a
- v-if="titleLink"
- :href="titleLink"
- target="_blank"
- rel="noopener noreferrer"
- class="js-cluster-application-title"
- >{{ title }}</a
- >
- <span v-else class="js-cluster-application-title">{{ title }}</span>
- </strong>
- <slot name="installed-via"></slot>
- <div>
- <slot name="description"></slot>
- </div>
- <div v-if="hasError" class="cluster-application-error text-danger gl-mt-3">
- <p class="js-cluster-application-general-error-message gl-mb-0">
- {{ generalErrorDescription }}
- </p>
- <ul v-if="statusReason || requestReason">
- <li v-if="statusReason" class="js-cluster-application-status-error-message">
- {{ statusReason }}
- </li>
- <li v-if="requestReason" class="js-cluster-application-request-error-message">
- {{ requestReason }}
- </li>
- </ul>
- </div>
-
- <div v-if="updateable">
- <div
- v-if="shouldShowUpdateDetails"
- class="form-text text-muted label p-0 js-cluster-application-update-details"
- >
- <template v-if="updateFailed">{{ __('Update failed') }}</template>
- <template v-else-if="isUpdating">{{ __('Updating') }}</template>
- <template v-else>
- <gl-sprintf :message="__('Updated to %{linkStart}chart v%{linkEnd}')">
- <template #link="{ content }">
- <gl-link
- :href="chartRepo"
- target="_blank"
- class="js-cluster-application-update-version"
- >{{ content }}{{ version }}</gl-link
- >
- </template>
- </gl-sprintf>
- </template>
- </div>
-
- <gl-alert
- v-if="updateFailed && !isUpdating"
- variant="danger"
- :dismissible="false"
- class="gl-mt-3 gl-mb-0 js-cluster-application-update-details"
- >
- {{ updateFailureDescription }}
- </gl-alert>
- <template v-if="updateAvailable || updateFailed || isUpdating">
- <template v-if="updatingNeedsConfirmation">
- <gl-button
- v-gl-modal-directive="updateModalId"
- class="js-cluster-application-update-button mt-2"
- variant="info"
- category="primary"
- :loading="isUpdating"
- :disabled="isUpdating"
- data-qa-selector="update_button_with_confirmation"
- :data-qa-application="id"
- >
- {{ updateButtonLabel }}
- </gl-button>
- <update-application-confirmation-modal
- :application="id"
- :application-title="title"
- @confirm="updateConfirmed()"
- />
- </template>
-
- <gl-button
- v-else
- class="js-cluster-application-update-button mt-2"
- variant="info"
- category="primary"
- :loading="isUpdating"
- :disabled="isUpdating"
- data-qa-selector="update_button"
- :data-qa-application="id"
- @click="updateConfirmed"
- >
- {{ updateButtonLabel }}
- </gl-button>
- </template>
- </div>
- </div>
- <div
- :class="[buttonGridCellClass, 'table-section', 'table-button-footer', 'section-align-top']"
- role="gridcell"
- >
- <div v-if="showManageButton" class="btn-group table-action-buttons">
- <a :href="manageLink" :class="{ disabled: disabled }" class="btn">{{
- manageButtonLabel
- }}</a>
- </div>
- <div class="btn-group table-action-buttons">
- <gl-button
- v-if="displayInstallButton"
- :loading="installButtonLoading"
- :disabled="disabled || installButtonDisabled"
- class="js-cluster-application-install-button"
- variant="default"
- data-qa-selector="install_button"
- :data-qa-application="id"
- @click="installClicked"
- >
- {{ installButtonLabel }}
- </gl-button>
- <uninstall-application-button
- v-if="displayUninstallButton"
- v-gl-modal-directive="uninstallModalId"
- :status="status"
- data-qa-selector="uninstall_button"
- :data-qa-application="id"
- class="js-cluster-application-uninstall-button"
- />
- <uninstall-application-confirmation-modal
- :application="id"
- :application-title="title"
- @confirm="uninstallConfirmed()"
- />
- </div>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
deleted file mode 100644
index ddee1711975..00000000000
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ /dev/null
@@ -1,662 +0,0 @@
-<script>
-import { GlLoadingIcon, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
-import certManagerLogo from 'images/cluster_app_logos/cert_manager.png';
-import crossplaneLogo from 'images/cluster_app_logos/crossplane.png';
-import elasticStackLogo from 'images/cluster_app_logos/elastic_stack.png';
-import gitlabLogo from 'images/cluster_app_logos/gitlab.png';
-import helmLogo from 'images/cluster_app_logos/helm.png';
-import jupyterhubLogo from 'images/cluster_app_logos/jupyterhub.png';
-import knativeLogo from 'images/cluster_app_logos/knative.png';
-import kubernetesLogo from 'images/cluster_app_logos/kubernetes.png';
-import prometheusLogo from 'images/cluster_app_logos/prometheus.png';
-import eventHub from '~/clusters/event_hub';
-import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
-import { CLUSTER_TYPE, PROVIDER_TYPE, APPLICATION_STATUS, INGRESS } from '../constants';
-import applicationRow from './application_row.vue';
-import CrossplaneProviderStack from './crossplane_provider_stack.vue';
-import KnativeDomainEditor from './knative_domain_editor.vue';
-
-export default {
- components: {
- applicationRow,
- clipboardButton,
- GlLoadingIcon,
- GlSprintf,
- GlLink,
- KnativeDomainEditor,
- CrossplaneProviderStack,
- GlAlert,
- },
- props: {
- type: {
- type: String,
- required: false,
- default: CLUSTER_TYPE.PROJECT,
- },
- applications: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- helpPath: {
- type: String,
- required: false,
- default: '',
- },
- helmHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- ingressDnsHelpPath: {
- type: String,
- required: false,
- default: '',
- },
-
- cloudRunHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- managePrometheusPath: {
- type: String,
- required: false,
- default: '',
- },
- providerType: {
- type: String,
- required: false,
- default: '',
- },
- preInstalledKnative: {
- type: Boolean,
- required: false,
- default: false,
- },
- rbac: {
- type: Boolean,
- required: false,
- default: false,
- },
- ciliumHelpPath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- ingressId() {
- return INGRESS;
- },
- ingressInstalled() {
- return this.applications.ingress.status === APPLICATION_STATUS.INSTALLED;
- },
- ingressExternalEndpoint() {
- return this.applications.ingress.externalIp || this.applications.ingress.externalHostname;
- },
- certManagerInstalled() {
- return this.applications.cert_manager.status === APPLICATION_STATUS.INSTALLED;
- },
- jupyterInstalled() {
- return this.applications.jupyter.status === APPLICATION_STATUS.INSTALLED;
- },
- jupyterHostname() {
- return this.applications.jupyter.hostname;
- },
- knative() {
- return this.applications.knative;
- },
- crossplane() {
- return this.applications.crossplane;
- },
- cloudRun() {
- return this.providerType === PROVIDER_TYPE.GCP && this.preInstalledKnative;
- },
- ingress() {
- return this.applications.ingress;
- },
- },
- methods: {
- saveKnativeDomain() {
- eventHub.$emit('saveKnativeDomain', {
- id: 'knative',
- params: {
- hostname: this.applications.knative.hostname,
- pages_domain_id: this.applications.knative.pagesDomain?.id,
- },
- });
- },
- setKnativeDomain({ domainId, domain }) {
- eventHub.$emit('setKnativeDomain', {
- id: 'knative',
- domainId,
- domain,
- });
- },
- setCrossplaneProviderStack(stack) {
- eventHub.$emit('setCrossplaneProviderStack', {
- id: 'crossplane',
- stack,
- });
- },
- },
- logos: {
- gitlabLogo,
- helmLogo,
- jupyterhubLogo,
- kubernetesLogo,
- certManagerLogo,
- crossplaneLogo,
- knativeLogo,
- prometheusLogo,
- elasticStackLogo,
- },
-};
-</script>
-
-<template>
- <section id="cluster-applications">
- <p class="gl-mb-0">
- {{
- s__(`ClusterIntegration|Choose which applications to install on your Kubernetes cluster.`)
- }}
- <gl-link :href="helpPath">{{ __('More information') }}</gl-link>
- </p>
-
- <div class="cluster-application-list gl-mt-3">
- <application-row
- v-if="applications.helm.installed || applications.helm.uninstalling"
- id="helm"
- :logo-url="$options.logos.helmLogo"
- :title="applications.helm.title"
- :status="applications.helm.status"
- :status-reason="applications.helm.statusReason"
- :request-status="applications.helm.requestStatus"
- :request-reason="applications.helm.requestReason"
- :installed="applications.helm.installed"
- :install-failed="applications.helm.installFailed"
- :uninstallable="applications.helm.uninstallable"
- :uninstall-successful="applications.helm.uninstallSuccessful"
- :uninstall-failed="applications.helm.uninstallFailed"
- title-link="https://v2.helm.sh/"
- >
- <template #description>
- <p>
- {{
- s__(`ClusterIntegration|Can be safely removed. Prior to GitLab
- 13.2, GitLab used a remote Tiller server to manage the
- applications. GitLab no longer uses this server.
- Uninstalling this server will not affect your other
- applications. This row will disappear afterwards.`)
- }}
- <gl-link :href="helmHelpPath">{{ __('More information') }}</gl-link>
- </p>
- </template>
- </application-row>
- <application-row
- :id="ingressId"
- :logo-url="$options.logos.kubernetesLogo"
- :title="applications.ingress.title"
- :status="applications.ingress.status"
- :status-reason="applications.ingress.statusReason"
- :request-status="applications.ingress.requestStatus"
- :request-reason="applications.ingress.requestReason"
- :installed="applications.ingress.installed"
- :install-failed="applications.ingress.installFailed"
- :uninstallable="applications.ingress.uninstallable"
- :uninstall-successful="applications.ingress.uninstallSuccessful"
- :uninstall-failed="applications.ingress.uninstallFailed"
- :updateable="false"
- title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
- >
- <template #description>
- <p>
- {{
- s__(`ClusterIntegration|Ingress gives you a way to route
- requests to services based on the request host or path,
- centralizing a number of services into a single entrypoint.`)
- }}
- </p>
-
- <template v-if="ingressInstalled">
- <div class="form-group">
- <label for="ingress-endpoint">{{ s__('ClusterIntegration|Ingress Endpoint') }}</label>
- <div class="input-group">
- <template v-if="ingressExternalEndpoint">
- <input
- id="ingress-endpoint"
- :value="ingressExternalEndpoint"
- type="text"
- class="form-control js-endpoint"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="ingressExternalEndpoint"
- :title="s__('ClusterIntegration|Copy Ingress Endpoint')"
- class="input-group-text js-clipboard-btn"
- />
- </span>
- </template>
- <template v-else>
- <input type="text" class="form-control js-endpoint" readonly />
- <gl-loading-icon
- class="position-absolute align-self-center ml-2 js-ingress-ip-loading-icon"
- />
- </template>
- </div>
- <p class="form-text text-muted">
- {{
- s__(`ClusterIntegration|Point a wildcard DNS to this
- generated endpoint in order to access
- your application after it has been deployed.`)
- }}
- <gl-link :href="ingressDnsHelpPath" target="_blank">
- {{ __('More information') }}
- </gl-link>
- </p>
- </div>
-
- <p v-if="!ingressExternalEndpoint" class="settings-message js-no-endpoint-message">
- {{
- s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
- }}
- <gl-link :href="ingressDnsHelpPath" target="_blank">
- {{ __('More information') }}
- </gl-link>
- </p>
- </template>
- <template v-else>
- <gl-alert variant="info" :dismissible="false">
- <span data-testid="ingressCostWarning">
- <gl-sprintf
- :message="
- s__(
- 'ClusterIntegration|Installing Ingress may incur additional costs. Learn more about %{linkStart}pricing%{linkEnd}.',
- )
- "
- >
- <template #link="{ content }">
- <gl-link href="https://cloud.google.com/compute/pricing#lb" target="_blank">{{
- content
- }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- </gl-alert>
- </template>
- </template>
- </application-row>
- <application-row
- id="cert_manager"
- :logo-url="$options.logos.certManagerLogo"
- :title="applications.cert_manager.title"
- :status="applications.cert_manager.status"
- :status-reason="applications.cert_manager.statusReason"
- :request-status="applications.cert_manager.requestStatus"
- :request-reason="applications.cert_manager.requestReason"
- :installed="applications.cert_manager.installed"
- :install-failed="applications.cert_manager.installFailed"
- :install-application-request-params="{ email: applications.cert_manager.email }"
- :uninstallable="applications.cert_manager.uninstallable"
- :uninstall-successful="applications.cert_manager.uninstallSuccessful"
- :uninstall-failed="applications.cert_manager.uninstallFailed"
- title-link="https://cert-manager.readthedocs.io/en/latest/#"
- >
- <template #description>
- <p data-testid="certManagerDescription">
- <gl-sprintf
- :message="
- s__(`ClusterIntegration|Cert-Manager is a native Kubernetes certificate management controller that helps with issuing certificates.
- Installing Cert-Manager on your cluster will issue a certificate by %{linkStart}Let's Encrypt%{linkEnd} and ensure that certificates
- are valid and up-to-date.`)
- "
- >
- <template #link="{ content }">
- <gl-link href="https://letsencrypt.org/" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <div class="form-group">
- <label for="cert-manager-issuer-email">
- {{ s__('ClusterIntegration|Issuer Email') }}
- </label>
- <div class="input-group">
- <!-- eslint-disable vue/no-mutating-props -->
- <input
- id="cert-manager-issuer-email"
- v-model="applications.cert_manager.email"
- :readonly="certManagerInstalled"
- type="text"
- class="form-control js-email"
- />
- <!-- eslint-enable vue/no-mutating-props -->
- </div>
- <p class="form-text text-muted">
- {{
- s__(`ClusterIntegration|Issuers represent a certificate authority.
- You must provide an email address for your Issuer.`)
- }}
- <gl-link
- href="http://docs.cert-manager.io/en/latest/reference/issuers.html?highlight=email"
- target="_blank"
- >{{ __('More information') }}</gl-link
- >
- </p>
- </div>
- </template>
- </application-row>
- <application-row
- id="prometheus"
- :logo-url="$options.logos.prometheusLogo"
- :title="applications.prometheus.title"
- :manage-link="managePrometheusPath"
- :status="applications.prometheus.status"
- :status-reason="applications.prometheus.statusReason"
- :request-status="applications.prometheus.requestStatus"
- :request-reason="applications.prometheus.requestReason"
- :installed="applications.prometheus.installed"
- :install-failed="applications.prometheus.installFailed"
- :uninstallable="applications.prometheus.uninstallable"
- :uninstall-successful="applications.prometheus.uninstallSuccessful"
- :uninstall-failed="applications.prometheus.uninstallFailed"
- title-link="https://prometheus.io/docs/introduction/overview/"
- >
- <template #description>
- <span data-testid="prometheusDescription">
- <gl-sprintf
- :message="
- s__(`ClusterIntegration|Prometheus is an open-source monitoring system
- with %{linkStart}GitLab Integration%{linkEnd} to monitor deployed applications.`)
- "
- >
- <template #link="{ content }">
- <gl-link
- href="https://docs.gitlab.com/ee/user/project/integrations/prometheus.html"
- target="_blank"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
- </template>
- </application-row>
- <application-row
- id="runner"
- :logo-url="$options.logos.gitlabLogo"
- :title="applications.runner.title"
- :status="applications.runner.status"
- :status-reason="applications.runner.statusReason"
- :request-status="applications.runner.requestStatus"
- :request-reason="applications.runner.requestReason"
- :version="applications.runner.version"
- :chart-repo="applications.runner.chartRepo"
- :update-available="applications.runner.updateAvailable"
- :installed="applications.runner.installed"
- :install-failed="applications.runner.installFailed"
- :update-successful="applications.runner.updateSuccessful"
- :update-failed="applications.runner.updateFailed"
- :uninstallable="applications.runner.uninstallable"
- :uninstall-successful="applications.runner.uninstallSuccessful"
- :uninstall-failed="applications.runner.uninstallFailed"
- title-link="https://docs.gitlab.com/runner/"
- >
- <template #description>
- {{
- s__(`ClusterIntegration|GitLab Runner connects to the
- repository and executes CI/CD jobs,
- pushing results back and deploying
- applications to production.`)
- }}
- </template>
- </application-row>
- <application-row
- id="crossplane"
- :logo-url="$options.logos.crossplaneLogo"
- :title="applications.crossplane.title"
- :status="applications.crossplane.status"
- :status-reason="applications.crossplane.statusReason"
- :request-status="applications.crossplane.requestStatus"
- :request-reason="applications.crossplane.requestReason"
- :installed="applications.crossplane.installed"
- :install-failed="applications.crossplane.installFailed"
- :uninstallable="applications.crossplane.uninstallable"
- :uninstall-successful="applications.crossplane.uninstallSuccessful"
- :uninstall-failed="applications.crossplane.uninstallFailed"
- :install-application-request-params="{ stack: applications.crossplane.stack }"
- title-link="https://crossplane.io"
- >
- <template #description>
- <p data-testid="crossplaneDescription">
- <gl-sprintf
- :message="
- s__(
- `ClusterIntegration|Crossplane enables declarative provisioning of managed services from your cloud of choice using %{codeStart}kubectl%{codeEnd} or %{linkStart}GitLab Integration%{linkEnd}.
- Crossplane runs inside your Kubernetes cluster and supports secure connectivity and secrets management between app containers and the cloud services they depend on.`,
- )
- "
- >
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #link="{ content }">
- <gl-link
- href="https://docs.gitlab.com/ee/user/clusters/applications.html#crossplane"
- target="_blank"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </p>
- <div class="form-group">
- <CrossplaneProviderStack :crossplane="crossplane" @set="setCrossplaneProviderStack" />
- </div>
- </template>
- </application-row>
-
- <application-row
- id="jupyter"
- :logo-url="$options.logos.jupyterhubLogo"
- :title="applications.jupyter.title"
- :status="applications.jupyter.status"
- :status-reason="applications.jupyter.statusReason"
- :request-status="applications.jupyter.requestStatus"
- :request-reason="applications.jupyter.requestReason"
- :installed="applications.jupyter.installed"
- :install-failed="applications.jupyter.installFailed"
- :uninstallable="applications.jupyter.uninstallable"
- :uninstall-successful="applications.jupyter.uninstallSuccessful"
- :uninstall-failed="applications.jupyter.uninstallFailed"
- :install-application-request-params="{ hostname: applications.jupyter.hostname }"
- title-link="https://jupyterhub.readthedocs.io/en/stable/"
- >
- <template #description>
- <p>
- {{
- s__(`ClusterIntegration|JupyterHub, a multi-user Hub, spawns,
- manages, and proxies multiple instances of the single-user
- Jupyter notebook server. JupyterHub can be used to serve
- notebooks to a class of students, a corporate data science group,
- or a scientific research group.`)
- }}
- <gl-sprintf
- :message="
- s__(
- 'ClusterIntegration|%{boldStart}Note:%{boldEnd} Requires Ingress to be installed.',
- )
- "
- >
- <template #bold="{ content }">
- <b>{{ content }}</b>
- </template>
- </gl-sprintf>
- </p>
-
- <template v-if="ingressExternalEndpoint">
- <div class="form-group">
- <label for="jupyter-hostname">{{ s__('ClusterIntegration|Jupyter Hostname') }}</label>
-
- <div class="input-group">
- <!-- eslint-disable vue/no-mutating-props -->
- <input
- id="jupyter-hostname"
- v-model="applications.jupyter.hostname"
- :readonly="jupyterInstalled"
- type="text"
- class="form-control js-hostname"
- />
- <!-- eslint-enable vue/no-mutating-props -->
- <span class="input-group-append">
- <clipboard-button
- :text="jupyterHostname"
- :title="s__('ClusterIntegration|Copy Jupyter Hostname')"
- class="js-clipboard-btn"
- />
- </span>
- </div>
-
- <p v-if="ingressInstalled" class="form-text text-muted">
- {{
- s__(`ClusterIntegration|Replace this with your own hostname if you want.
- If you do so, point hostname to Ingress IP Address from above.`)
- }}
- <gl-link :href="ingressDnsHelpPath" target="_blank">
- {{ __('More information') }}
- </gl-link>
- </p>
- </div>
- </template>
- </template>
- </application-row>
- <application-row
- id="knative"
- :logo-url="$options.logos.knativeLogo"
- :title="applications.knative.title"
- :status="applications.knative.status"
- :status-reason="applications.knative.statusReason"
- :request-status="applications.knative.requestStatus"
- :request-reason="applications.knative.requestReason"
- :installed="applications.knative.installed"
- :install-failed="applications.knative.installFailed"
- :install-application-request-params="{
- hostname: applications.knative.hostname,
- pages_domain_id: applications.knative.pagesDomain && applications.knative.pagesDomain.id,
- }"
- :uninstallable="applications.knative.uninstallable"
- :uninstall-successful="applications.knative.uninstallSuccessful"
- :uninstall-failed="applications.knative.uninstallFailed"
- :updateable="false"
- v-bind="applications.knative"
- title-link="https://github.com/knative/docs"
- >
- <template #description>
- <gl-alert v-if="!rbac" variant="info" class="rbac-notice gl-my-3" :dismissible="false">
- {{
- s__(`ClusterIntegration|You must have an RBAC-enabled cluster
- to install Knative.`)
- }}
- <gl-link :href="helpPath" target="_blank">{{ __('More information') }}</gl-link>
- </gl-alert>
- <p>
- {{
- s__(`ClusterIntegration|Knative extends Kubernetes to provide
- a set of middleware components that are essential to build modern,
- source-centric, and container-based applications that can run
- anywhere: on premises, in the cloud, or even in a third-party data center.`)
- }}
- </p>
-
- <knative-domain-editor
- v-if="(knative.installed || rbac) && !preInstalledKnative"
- :knative="knative"
- :ingress-dns-help-path="ingressDnsHelpPath"
- @save="saveKnativeDomain"
- @set="setKnativeDomain"
- />
- </template>
- <template v-if="cloudRun" #installed-via>
- <span data-testid="installed-via">
- <gl-sprintf
- :message="s__('ClusterIntegration|installed via %{linkStart}Cloud Run%{linkEnd}')"
- >
- <template #link="{ content }">
- <gl-link :href="cloudRunHelpPath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </span>
- </template>
- </application-row>
- <application-row
- id="elastic_stack"
- :logo-url="$options.logos.elasticStackLogo"
- :title="applications.elastic_stack.title"
- :status="applications.elastic_stack.status"
- :status-reason="applications.elastic_stack.statusReason"
- :request-status="applications.elastic_stack.requestStatus"
- :request-reason="applications.elastic_stack.requestReason"
- :version="applications.elastic_stack.version"
- :chart-repo="applications.elastic_stack.chartRepo"
- :update-available="applications.elastic_stack.updateAvailable"
- :installed="applications.elastic_stack.installed"
- :install-failed="applications.elastic_stack.installFailed"
- :update-successful="applications.elastic_stack.updateSuccessful"
- :update-failed="applications.elastic_stack.updateFailed"
- :uninstallable="applications.elastic_stack.uninstallable"
- :uninstall-successful="applications.elastic_stack.uninstallSuccessful"
- :uninstall-failed="applications.elastic_stack.uninstallFailed"
- title-link="https://gitlab.com/gitlab-org/charts/elastic-stack"
- >
- <template #description>
- <p>
- {{
- s__(
- `ClusterIntegration|The elastic stack collects logs from all pods in your cluster`,
- )
- }}
- </p>
- </template>
- </application-row>
-
- <div class="gl-mt-7 gl-border-1 gl-border-t-solid gl-border-gray-100">
- <!-- This empty div serves as a separator. The applications below can be externally installed using a cluster-management project. -->
- </div>
-
- <application-row
- id="cilium"
- :title="applications.cilium.title"
- :logo-url="$options.logos.gitlabLogo"
- :status="applications.cilium.status"
- :status-reason="applications.cilium.statusReason"
- :installable="applications.cilium.installable"
- :uninstallable="applications.cilium.uninstallable"
- :installed="applications.cilium.installed"
- :install-failed="applications.cilium.installFailed"
- :title-link="ciliumHelpPath"
- >
- <template #description>
- <p data-testid="ciliumDescription">
- <gl-sprintf
- :message="
- s__(
- 'ClusterIntegration|Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints. %{linkStart}Learn more about configuring Network Policies here.%{linkEnd}',
- )
- "
- >
- <template #link="{ content }">
- <gl-link :href="ciliumHelpPath" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </template>
- </application-row>
- </div>
- </section>
-</template>
diff --git a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue b/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
deleted file mode 100644
index 6b99bb09504..00000000000
--- a/app/assets/javascripts/clusters/components/crossplane_provider_stack.vue
+++ /dev/null
@@ -1,93 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { s__ } from '../../locale';
-
-export default {
- name: 'CrossplaneProviderStack',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlIcon,
- },
- props: {
- stacks: {
- type: Array,
- required: false,
- default: () => [
- {
- name: s__('Google Cloud Platform'),
- code: 'gcp',
- },
- {
- name: s__('Amazon Web Services'),
- code: 'aws',
- },
- {
- name: s__('Microsoft Azure'),
- code: 'azure',
- },
- {
- name: s__('Rook'),
- code: 'rook',
- },
- ],
- },
- crossplane: {
- type: Object,
- required: true,
- },
- },
- computed: {
- dropdownText() {
- const result = this.stacks.reduce((map, obj) => {
- // eslint-disable-next-line no-param-reassign
- map[obj.code] = obj.name;
- return map;
- }, {});
- const { stack } = this.crossplane;
- if (stack !== '') {
- return result[stack];
- }
- return s__('Select Stack');
- },
- validationError() {
- return this.crossplane.validationError;
- },
- },
- methods: {
- selectStack(stack) {
- this.$emit('set', stack);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <label>
- {{ s__('ClusterIntegration|Enabled stack') }}
- </label>
- <gl-dropdown
- :disabled="crossplane.installed"
- :text="dropdownText"
- toggle-class="dropdown-menu-toggle gl-field-error-outline"
- class="w-100"
- :class="{ 'gl-show-field-errors': validationError }"
- >
- <gl-dropdown-item v-for="stack in stacks" :key="stack.code" @click="selectStack(stack)">
- <span class="ml-1">{{ stack.name }}</span>
- </gl-dropdown-item>
- </gl-dropdown>
- <span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
- <p class="form-text text-muted">
- {{ s__(`You must select a stack for configuring your cloud provider. Learn more about`) }}
- <a
- href="https://crossplane.io/docs/master/stacks-guide.html"
- target="_blank"
- rel="noopener noreferrer"
- >{{ __('Crossplane') }}
- <gl-icon name="external-link" class="vertical-align-middle" />
- </a>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/clusters/components/knative_domain_editor.vue b/app/assets/javascripts/clusters/components/knative_domain_editor.vue
deleted file mode 100644
index 89446680173..00000000000
--- a/app/assets/javascripts/clusters/components/knative_domain_editor.vue
+++ /dev/null
@@ -1,232 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
- GlSprintf,
- GlButton,
- GlAlert,
-} from '@gitlab/ui';
-import { APPLICATION_STATUS } from '~/clusters/constants';
-import { __, s__ } from '~/locale';
-
-import ClipboardButton from '../../vue_shared/components/clipboard_button.vue';
-
-const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
-
-export default {
- components: {
- GlButton,
- ClipboardButton,
- GlLoadingIcon,
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlSearchBoxByType,
- GlSprintf,
- GlAlert,
- },
- props: {
- knative: {
- type: Object,
- required: true,
- },
- ingressDnsHelpPath: {
- type: String,
- default: '',
- required: false,
- },
- },
- data() {
- return {
- searchQuery: '',
- };
- },
- computed: {
- saveButtonDisabled() {
- return [UNINSTALLING, UPDATING].includes(this.knative.status);
- },
- saving() {
- return [UPDATING].includes(this.knative.status);
- },
- saveButtonLabel() {
- return this.saving ? __('Saving') : __('Save changes');
- },
- knativeInstalled() {
- return this.knative.installed;
- },
- knativeExternalEndpoint() {
- return this.knative.externalIp || this.knative.externalHostname;
- },
- knativeUpdateSuccessful() {
- return this.knative.updateSuccessful;
- },
- knativeHostname: {
- get() {
- return this.knative.hostname;
- },
- set(hostname) {
- this.selectCustomDomain(hostname);
- },
- },
- domainDropdownText() {
- return this.knativeHostname || s__('ClusterIntegration|Select existing domain or use new');
- },
- availableDomains() {
- return this.knative.availableDomains || [];
- },
- filteredDomains() {
- const query = this.searchQuery.toLowerCase();
- return this.availableDomains.filter(({ domain }) => domain.toLowerCase().includes(query));
- },
- showDomainsDropdown() {
- return this.availableDomains.length > 0;
- },
- validationError() {
- return this.knative.validationError;
- },
- },
- watch: {
- knativeUpdateSuccessful(updateSuccessful) {
- if (updateSuccessful) {
- this.$toast.show(s__('ClusterIntegration|Knative domain name was updated successfully.'));
- }
- },
- },
- methods: {
- selectDomain({ id, domain }) {
- this.$emit('set', { domain, domainId: id });
- },
- selectCustomDomain(domain) {
- this.$emit('set', { domain, domainId: null });
- },
- },
-};
-</script>
-
-<template>
- <div class="row">
- <gl-alert
- v-if="knative.updateFailed"
- class="gl-mb-5 col-12 js-cluster-knative-domain-name-failure-message"
- variant="danger"
- >
- {{ s__('ClusterIntegration|Something went wrong while updating Knative domain name.') }}
- </gl-alert>
-
- <div
- :class="{ 'col-md-6': knativeInstalled, 'col-12': !knativeInstalled }"
- class="form-group col-sm-12 mb-0"
- >
- <label for="knative-domainname">
- <strong>{{ s__('ClusterIntegration|Knative Domain Name:') }}</strong>
- </label>
-
- <gl-dropdown
- v-if="showDomainsDropdown"
- :text="domainDropdownText"
- toggle-class="dropdown-menu-toggle"
- class="w-100 mb-2"
- >
- <gl-search-box-by-type
- v-model.trim="searchQuery"
- :placeholder="s__('ClusterIntegration|Search domains')"
- />
- <gl-dropdown-item
- v-for="domain in filteredDomains"
- :key="domain.id"
- @click="selectDomain(domain)"
- >
- <span class="ml-1">{{ domain.domain }}</span>
- </gl-dropdown-item>
- <template v-if="searchQuery">
- <gl-dropdown-divider />
- <gl-dropdown-item key="custom-domain" @click="selectCustomDomain(searchQuery)">
- <span class="ml-1">
- <gl-sprintf :message="s__('ClusterIntegration|Use %{query}')">
- <template #query>
- <code>{{ searchQuery }}</code>
- </template>
- </gl-sprintf>
- </span>
- </gl-dropdown-item>
- </template>
- </gl-dropdown>
-
- <input
- v-else
- id="knative-domainname"
- v-model="knativeHostname"
- type="text"
- class="form-control js-knative-domainname"
- />
-
- <span v-if="validationError" class="gl-field-error">{{ validationError }}</span>
- </div>
-
- <template v-if="knativeInstalled">
- <div class="form-group col-sm-12 col-md-6 pl-md-0 mb-0 mt-3 mt-md-0">
- <label for="knative-endpoint">
- <strong>{{ s__('ClusterIntegration|Knative Endpoint:') }}</strong>
- </label>
- <div v-if="knativeExternalEndpoint" class="input-group">
- <input
- id="knative-endpoint"
- :value="knativeExternalEndpoint"
- type="text"
- class="form-control js-knative-endpoint"
- readonly
- />
- <span class="input-group-append">
- <clipboard-button
- :text="knativeExternalEndpoint"
- :title="s__('ClusterIntegration|Copy Knative Endpoint')"
- class="input-group-text js-knative-endpoint-clipboard-btn"
- />
- </span>
- </div>
- <div v-else class="input-group">
- <input type="text" class="form-control js-endpoint" readonly />
- <gl-loading-icon
- class="position-absolute align-self-center ml-2 js-knative-ip-loading-icon"
- />
- </div>
- </div>
-
- <p class="form-text text-muted col-12">
- {{
- s__(
- `ClusterIntegration|To access your application after deployment, point a wildcard DNS to the Knative Endpoint.`,
- )
- }}
- <a :href="ingressDnsHelpPath" target="_blank" rel="noopener noreferrer">{{
- __('More information')
- }}</a>
- </p>
-
- <p
- v-if="!knativeExternalEndpoint"
- class="settings-message js-no-knative-endpoint-message mt-2 mr-3 mb-0 ml-3"
- >
- {{
- s__(`ClusterIntegration|The endpoint is in
- the process of being assigned. Please check your Kubernetes
- cluster or Quotas on Google Kubernetes Engine if it takes a long time.`)
- }}
- </p>
-
- <gl-button
- class="js-knative-save-domain-button gl-mt-5 gl-ml-5"
- variant="success"
- category="primary"
- :loading="saving"
- :disabled="saveButtonDisabled"
- @click="$emit('save')"
- >
- {{ saveButtonLabel }}
- </gl-button>
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_button.vue b/app/assets/javascripts/clusters/components/uninstall_application_button.vue
deleted file mode 100644
index 73191d6d84d..00000000000
--- a/app/assets/javascripts/clusters/components/uninstall_application_button.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { APPLICATION_STATUS } from '~/clusters/constants';
-import { __ } from '~/locale';
-
-const { UPDATING, UNINSTALLING } = APPLICATION_STATUS;
-
-export default {
- components: {
- GlButton,
- },
- props: {
- status: {
- type: String,
- required: true,
- },
- },
- computed: {
- disabled() {
- return [UNINSTALLING, UPDATING].includes(this.status);
- },
- loading() {
- return this.status === UNINSTALLING;
- },
- label() {
- return this.loading ? __('Uninstalling') : __('Uninstall');
- },
- },
-};
-</script>
-
-<template>
- <gl-button :disabled="disabled" variant="default" :loading="loading">
- {{ label }}
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
deleted file mode 100644
index 2a197e40b60..00000000000
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
-import { GlModal, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
-import { sprintf, s__ } from '~/locale';
-import {
- HELM,
- INGRESS,
- CERT_MANAGER,
- PROMETHEUS,
- RUNNER,
- KNATIVE,
- JUPYTER,
- ELASTIC_STACK,
-} from '../constants';
-
-const CUSTOM_APP_WARNING_TEXT = {
- [HELM]: sprintf(
- s__(
- 'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored. Your other applications will remain unaffected.',
- ),
- {
- gitlabManagedAppsNamespace: '<code>gitlab-managed-apps</code>',
- },
- false,
- ),
- [INGRESS]: s__(
- 'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
- ),
- [CERT_MANAGER]: s__(
- 'ClusterIntegration|The associated private key will be deleted and cannot be restored.',
- ),
- [PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
- [RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
- [KNATIVE]: s__(
- 'ClusterIntegration|The associated IP and all deployed services will be deleted and cannot be restored. Uninstalling Knative will also remove Istio from your cluster. This will not effect any other applications.',
- ),
- [JUPYTER]: s__(
- 'ClusterIntegration|All data not committed to GitLab will be deleted and cannot be restored.',
- ),
- [ELASTIC_STACK]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
-};
-
-export default {
- components: {
- GlModal,
- },
- directives: {
- SafeHtml,
- },
- mixins: [trackUninstallButtonClickMixin],
- props: {
- application: {
- type: String,
- required: true,
- },
- applicationTitle: {
- type: String,
- required: true,
- },
- },
- computed: {
- title() {
- return sprintf(s__('ClusterIntegration|Uninstall %{appTitle}'), {
- appTitle: this.applicationTitle,
- });
- },
- warningText() {
- return sprintf(
- s__('ClusterIntegration|You are about to uninstall %{appTitle} from your cluster.'),
- {
- appTitle: this.applicationTitle,
- },
- );
- },
- customAppWarningText() {
- return CUSTOM_APP_WARNING_TEXT[this.application];
- },
- modalId() {
- return `uninstall-${this.application}`;
- },
- },
- methods: {
- confirmUninstall() {
- this.trackUninstallButtonClick(this.application);
- this.$emit('confirm');
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ok-variant="danger"
- cancel-variant="light"
- :ok-title="title"
- :modal-id="modalId"
- :title="title"
- @ok="confirmUninstall()"
- >
- {{ warningText }} <span v-safe-html="customAppWarningText"></span>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
deleted file mode 100644
index 0aedc6e84fa..00000000000
--- a/app/assets/javascripts/clusters/components/update_application_confirmation_modal.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<script>
-/* eslint-disable vue/no-v-html */
-import { GlModal } from '@gitlab/ui';
-import { sprintf, s__ } from '~/locale';
-import { ELASTIC_STACK } from '../constants';
-
-const CUSTOM_APP_WARNING_TEXT = {
- [ELASTIC_STACK]: s__(
- 'ClusterIntegration|Your Elasticsearch cluster will be re-created during this upgrade. Your logs will be re-indexed, and you will lose historical logs from hosts terminated in the last 30 days.',
- ),
-};
-
-export default {
- components: {
- GlModal,
- },
- props: {
- application: {
- type: String,
- required: true,
- },
- applicationTitle: {
- type: String,
- required: true,
- },
- },
- computed: {
- title() {
- return sprintf(s__('ClusterIntegration|Update %{appTitle}'), {
- appTitle: this.applicationTitle,
- });
- },
- warningText() {
- return sprintf(
- s__('ClusterIntegration|You are about to update %{appTitle} on your cluster.'),
- {
- appTitle: this.applicationTitle,
- },
- );
- },
- customAppWarningText() {
- return CUSTOM_APP_WARNING_TEXT[this.application];
- },
- modalId() {
- return `update-${this.application}`;
- },
- },
- methods: {
- confirmUpdate() {
- this.$emit('confirm');
- },
- },
-};
-</script>
-<template>
- <gl-modal
- ok-variant="danger"
- cancel-variant="light"
- :ok-title="title"
- :modal-id="modalId"
- :title="title"
- @ok="confirmUpdate()"
- >
- {{ warningText }} <span v-html="customAppWarningText"></span>
- </gl-modal>
-</template>
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 846e5950b8b..c6ca895778d 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -10,64 +10,7 @@ export const PROVIDER_TYPE = {
GCP: 'gcp',
};
-// These need to match what is returned from the server
-export const APPLICATION_STATUS = {
- NO_STATUS: null,
- NOT_INSTALLABLE: 'not_installable',
- INSTALLABLE: 'installable',
- SCHEDULED: 'scheduled',
- INSTALLING: 'installing',
- INSTALLED: 'installed',
- UPDATING: 'updating',
- UPDATED: 'updated',
- UPDATE_ERRORED: 'update_errored',
- UNINSTALLING: 'uninstalling',
- UNINSTALL_ERRORED: 'uninstall_errored',
- ERROR: 'errored',
- PRE_INSTALLED: 'pre_installed',
- UNINSTALLED: 'uninstalled',
- EXTERNALLY_INSTALLED: 'externally_installed',
-};
-
-/*
- * The application cannot be in any of the following states without
- * not being installed.
- */
-export const APPLICATION_INSTALLED_STATUSES = [
- APPLICATION_STATUS.INSTALLED,
- APPLICATION_STATUS.UPDATING,
- APPLICATION_STATUS.UNINSTALLING,
- APPLICATION_STATUS.PRE_INSTALLED,
-];
-
// These are only used client-side
-export const UPDATE_EVENT = 'update';
-export const INSTALL_EVENT = 'install';
-export const UNINSTALL_EVENT = 'uninstall';
-
-export const HELM = 'helm';
-export const INGRESS = 'ingress';
-export const JUPYTER = 'jupyter';
-export const KNATIVE = 'knative';
-export const RUNNER = 'runner';
-export const CERT_MANAGER = 'cert_manager';
-export const CROSSPLANE = 'crossplane';
-export const PROMETHEUS = 'prometheus';
-export const ELASTIC_STACK = 'elastic_stack';
-
-export const APPLICATIONS = [
- HELM,
- INGRESS,
- JUPYTER,
- KNATIVE,
- RUNNER,
- CERT_MANAGER,
- PROMETHEUS,
- ELASTIC_STACK,
-];
-
-export const INGRESS_DOMAIN_SUFFIX = '.nip.io';
-
export const LOGGING_MODE = 'logging';
export const BLOCKING_MODE = 'blocking';
diff --git a/app/assets/javascripts/clusters/services/application_state_machine.js b/app/assets/javascripts/clusters/services/application_state_machine.js
deleted file mode 100644
index 2ff604af9a7..00000000000
--- a/app/assets/javascripts/clusters/services/application_state_machine.js
+++ /dev/null
@@ -1,250 +0,0 @@
-import { APPLICATION_STATUS, UPDATE_EVENT, INSTALL_EVENT, UNINSTALL_EVENT } from '../constants';
-
-const {
- NO_STATUS,
- SCHEDULED,
- NOT_INSTALLABLE,
- INSTALLABLE,
- INSTALLING,
- INSTALLED,
- ERROR,
- UPDATING,
- UPDATED,
- UPDATE_ERRORED,
- UNINSTALLING,
- UNINSTALL_ERRORED,
- PRE_INSTALLED,
- UNINSTALLED,
- EXTERNALLY_INSTALLED,
-} = APPLICATION_STATUS;
-
-const applicationStateMachine = {
- /* When the application initially loads, it will have `NO_STATUS`
- * It will transition from `NO_STATUS` once the async backend call is completed
- */
- [NO_STATUS]: {
- on: {
- [SCHEDULED]: {
- target: INSTALLING,
- },
- [NOT_INSTALLABLE]: {
- target: NOT_INSTALLABLE,
- },
- [INSTALLABLE]: {
- target: INSTALLABLE,
- },
- [INSTALLING]: {
- target: INSTALLING,
- },
- [INSTALLED]: {
- target: INSTALLED,
- },
- [ERROR]: {
- target: INSTALLABLE,
- effects: {
- installFailed: true,
- },
- },
- [UPDATING]: {
- target: UPDATING,
- },
- [UPDATED]: {
- target: INSTALLED,
- },
- [UPDATE_ERRORED]: {
- target: INSTALLED,
- effects: {
- updateFailed: true,
- },
- },
- [UNINSTALLING]: {
- target: UNINSTALLING,
- },
- [UNINSTALL_ERRORED]: {
- target: INSTALLED,
- effects: {
- uninstallFailed: true,
- },
- },
- [PRE_INSTALLED]: {
- target: PRE_INSTALLED,
- },
- [UNINSTALLED]: {
- target: UNINSTALLED,
- },
- [EXTERNALLY_INSTALLED]: {
- target: EXTERNALLY_INSTALLED,
- },
- },
- },
- [NOT_INSTALLABLE]: {
- on: {
- [INSTALLABLE]: {
- target: INSTALLABLE,
- },
- },
- },
- [INSTALLABLE]: {
- on: {
- [INSTALL_EVENT]: {
- target: INSTALLING,
- effects: {
- installFailed: false,
- },
- },
- [NOT_INSTALLABLE]: {
- target: NOT_INSTALLABLE,
- },
- [INSTALLED]: {
- target: INSTALLED,
- effects: {
- installFailed: false,
- },
- },
- [UNINSTALLED]: {
- target: UNINSTALLED,
- effects: {
- installFailed: false,
- },
- },
- },
- },
- [INSTALLING]: {
- on: {
- [INSTALLED]: {
- target: INSTALLED,
- },
- [ERROR]: {
- target: INSTALLABLE,
- effects: {
- installFailed: true,
- },
- },
- },
- },
- [INSTALLED]: {
- on: {
- [UPDATE_EVENT]: {
- target: UPDATING,
- effects: {
- updateFailed: false,
- updateSuccessful: false,
- },
- },
- [NOT_INSTALLABLE]: {
- target: NOT_INSTALLABLE,
- },
- [UNINSTALL_EVENT]: {
- target: UNINSTALLING,
- effects: {
- uninstallFailed: false,
- uninstallSuccessful: false,
- },
- },
- [UNINSTALLED]: {
- target: UNINSTALLED,
- },
- [ERROR]: {
- target: INSTALLABLE,
- effects: {
- installFailed: true,
- },
- },
- },
- },
- [PRE_INSTALLED]: {
- on: {
- [UPDATE_EVENT]: {
- target: UPDATING,
- effects: {
- updateFailed: false,
- updateSuccessful: false,
- },
- },
- [NOT_INSTALLABLE]: {
- target: NOT_INSTALLABLE,
- },
- [UNINSTALL_EVENT]: {
- target: UNINSTALLING,
- effects: {
- uninstallFailed: false,
- uninstallSuccessful: false,
- },
- },
- },
- },
- [UPDATING]: {
- on: {
- [UPDATED]: {
- target: INSTALLED,
- effects: {
- updateSuccessful: true,
- },
- },
- [UPDATE_ERRORED]: {
- target: INSTALLED,
- effects: {
- updateFailed: true,
- },
- },
- },
- },
- [UNINSTALLING]: {
- on: {
- [INSTALLABLE]: {
- target: INSTALLABLE,
- effects: {
- uninstallSuccessful: true,
- },
- },
- [NOT_INSTALLABLE]: {
- target: NOT_INSTALLABLE,
- effects: {
- uninstallSuccessful: true,
- },
- },
- [UNINSTALL_ERRORED]: {
- target: INSTALLED,
- effects: {
- uninstallFailed: true,
- },
- },
- },
- },
- [UNINSTALLED]: {
- on: {
- [INSTALLED]: {
- target: INSTALLED,
- },
- [ERROR]: {
- target: INSTALLABLE,
- effects: {
- installFailed: true,
- },
- },
- },
- },
-};
-
-/**
- * Determines an application new state based on the application current state
- * and an event. If the application current state cannot handle a given event,
- * the current state is returned.
- *
- * @param {*} application
- * @param {*} event
- */
-const transitionApplicationState = (application, event) => {
- const stateMachine = applicationStateMachine[application.status];
- const newState = stateMachine !== undefined ? stateMachine.on[event] : false;
-
- return newState
- ? {
- ...application,
- status: newState.target,
- ...newState.effects,
- }
- : application;
-};
-
-export default transitionApplicationState;
diff --git a/app/assets/javascripts/clusters/services/clusters_service.js b/app/assets/javascripts/clusters/services/clusters_service.js
index 333fb293a15..7300bb3137a 100644
--- a/app/assets/javascripts/clusters/services/clusters_service.js
+++ b/app/assets/javascripts/clusters/services/clusters_service.js
@@ -3,38 +3,12 @@ import axios from '../../lib/utils/axios_utils';
export default class ClusterService {
constructor(options = {}) {
this.options = options;
- this.appInstallEndpointMap = {
- helm: this.options.installHelmEndpoint,
- ingress: this.options.installIngressEndpoint,
- cert_manager: this.options.installCertManagerEndpoint,
- crossplane: this.options.installCrossplaneEndpoint,
- runner: this.options.installRunnerEndpoint,
- prometheus: this.options.installPrometheusEndpoint,
- jupyter: this.options.installJupyterEndpoint,
- knative: this.options.installKnativeEndpoint,
- elastic_stack: this.options.installElasticStackEndpoint,
- };
- this.appUpdateEndpointMap = {
- knative: this.options.updateKnativeEndpoint,
- };
}
fetchClusterStatus() {
return axios.get(this.options.endpoint);
}
- installApplication(appId, params) {
- return axios.post(this.appInstallEndpointMap[appId], params);
- }
-
- updateApplication(appId, params) {
- return axios.patch(this.appUpdateEndpointMap[appId], params);
- }
-
- uninstallApplication(appId, params) {
- return axios.delete(this.appInstallEndpointMap[appId], params);
- }
-
fetchClusterEnvironments() {
return axios.get(this.options.clusterEnvironmentsEndpoint);
}
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 50689a6142f..db6e7bad6cc 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,112 +1,16 @@
import { parseBoolean } from '../../lib/utils/common_utils';
-import { s__ } from '../../locale';
-import {
- INGRESS,
- JUPYTER,
- KNATIVE,
- CERT_MANAGER,
- CROSSPLANE,
- RUNNER,
- APPLICATION_INSTALLED_STATUSES,
- APPLICATION_STATUS,
- INSTALL_EVENT,
- UPDATE_EVENT,
- UNINSTALL_EVENT,
- ELASTIC_STACK,
-} from '../constants';
-import transitionApplicationState from '../services/application_state_machine';
-
-const isApplicationInstalled = (appStatus) => APPLICATION_INSTALLED_STATUSES.includes(appStatus);
-
-const applicationInitialState = {
- status: null,
- statusReason: null,
- requestReason: null,
- installable: true,
- installed: false,
- installFailed: false,
- uninstallable: false,
- uninstallFailed: false,
- uninstallSuccessful: false,
- validationError: null,
-};
export default class ClusterStore {
constructor() {
this.state = {
helpPath: null,
- helmHelpPath: null,
- ingressHelpPath: null,
environmentsHelpPath: null,
clustersHelpPath: null,
deployBoardsHelpPath: null,
- cloudRunHelpPath: null,
status: null,
providerType: null,
- preInstalledKnative: false,
rbac: false,
statusReason: null,
- applications: {
- helm: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Legacy Helm Tiller server'),
- },
- ingress: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Ingress'),
- externalIp: null,
- externalHostname: null,
- updateFailed: false,
- updateAvailable: false,
- },
- cert_manager: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Cert-Manager'),
- email: null,
- },
- crossplane: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Crossplane'),
- stack: null,
- },
- runner: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|GitLab Runner'),
- version: null,
- chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
- updateAvailable: null,
- updateSuccessful: false,
- updateFailed: false,
- },
- prometheus: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Prometheus'),
- },
- jupyter: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|JupyterHub'),
- hostname: null,
- },
- knative: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Knative'),
- hostname: null,
- isEditingDomain: false,
- externalIp: null,
- externalHostname: null,
- updateSuccessful: false,
- updateFailed: false,
- },
- elastic_stack: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|Elastic Stack'),
- },
- cilium: {
- ...applicationInitialState,
- title: s__('ClusterIntegration|GitLab Container Network Policies'),
- installable: false,
- },
- },
environments: [],
fetchingEnvironments: false,
};
@@ -118,10 +22,6 @@ export default class ClusterStore {
});
}
- setManagePrometheusPath(managePrometheusPath) {
- this.state.managePrometheusPath = managePrometheusPath;
- }
-
updateStatus(status) {
this.state.status = status;
}
@@ -130,10 +30,6 @@ export default class ClusterStore {
this.state.providerType = providerType;
}
- updatePreInstalledKnative(preInstalledKnative) {
- this.state.preInstalledKnative = parseBoolean(preInstalledKnative);
- }
-
updateRbac(rbac) {
this.state.rbac = parseBoolean(rbac);
}
@@ -142,112 +38,9 @@ export default class ClusterStore {
this.state.statusReason = reason;
}
- installApplication(appId) {
- this.handleApplicationEvent(appId, INSTALL_EVENT);
- }
-
- notifyInstallFailure(appId) {
- this.handleApplicationEvent(appId, APPLICATION_STATUS.ERROR);
- }
-
- updateApplication(appId) {
- this.handleApplicationEvent(appId, UPDATE_EVENT);
- }
-
- notifyUpdateFailure(appId) {
- this.handleApplicationEvent(appId, APPLICATION_STATUS.UPDATE_ERRORED);
- }
-
- uninstallApplication(appId) {
- this.handleApplicationEvent(appId, UNINSTALL_EVENT);
- }
-
- notifyUninstallFailure(appId) {
- this.handleApplicationEvent(appId, APPLICATION_STATUS.UNINSTALL_ERRORED);
- }
-
- handleApplicationEvent(appId, event) {
- const currentAppState = this.state.applications[appId];
-
- this.state.applications[appId] = transitionApplicationState(currentAppState, event);
- }
-
- updateAppProperty(appId, prop, value) {
- this.state.applications[appId][prop] = value;
- }
-
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
-
- serverState.applications.forEach((serverAppEntry) => {
- const {
- name: appId,
- status,
- status_reason: statusReason,
- version,
- update_available: updateAvailable,
- can_uninstall: uninstallable,
- } = serverAppEntry;
- const currentApplicationState = this.state.applications[appId] || {};
- const nextApplicationState = transitionApplicationState(currentApplicationState, status);
-
- this.state.applications[appId] = {
- ...currentApplicationState,
- ...nextApplicationState,
- statusReason,
- installed: isApplicationInstalled(nextApplicationState.status),
- uninstallable,
- };
-
- if (appId === INGRESS) {
- this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
- this.state.applications.ingress.externalHostname = serverAppEntry.external_hostname;
- this.state.applications.ingress.updateAvailable = updateAvailable;
- } else if (appId === CERT_MANAGER) {
- this.state.applications.cert_manager.email =
- this.state.applications.cert_manager.email || serverAppEntry.email;
- } else if (appId === CROSSPLANE) {
- this.state.applications.crossplane.stack =
- this.state.applications.crossplane.stack || serverAppEntry.stack;
- } else if (appId === JUPYTER) {
- this.state.applications.jupyter.hostname = this.updateHostnameIfUnset(
- this.state.applications.jupyter.hostname,
- serverAppEntry.hostname,
- 'jupyter',
- );
- } else if (appId === KNATIVE) {
- if (serverAppEntry.available_domains) {
- this.state.applications.knative.availableDomains = serverAppEntry.available_domains;
- }
- if (!this.state.applications.knative.isEditingDomain) {
- this.state.applications.knative.pagesDomain =
- serverAppEntry.pages_domain || this.state.applications.knative.pagesDomain;
- this.state.applications.knative.hostname =
- serverAppEntry.hostname || this.state.applications.knative.hostname;
- }
- this.state.applications.knative.externalIp =
- serverAppEntry.external_ip || this.state.applications.knative.externalIp;
- this.state.applications.knative.externalHostname =
- serverAppEntry.external_hostname || this.state.applications.knative.externalHostname;
- } else if (appId === RUNNER) {
- this.state.applications.runner.version = version;
- this.state.applications.runner.updateAvailable = updateAvailable;
- } else if (appId === ELASTIC_STACK) {
- this.state.applications.elastic_stack.version = version;
- this.state.applications.elastic_stack.updateAvailable = updateAvailable;
- }
- });
- }
-
- updateHostnameIfUnset(current, updated, fallback) {
- return (
- current ||
- updated ||
- (this.state.applications.ingress.externalIp
- ? `${fallback}.${this.state.applications.ingress.externalIp}.nip.io`
- : '')
- );
}
toggleFetchEnvironments(isFetching) {
diff --git a/app/assets/javascripts/clusters_list/store/actions.js b/app/assets/javascripts/clusters_list/store/actions.js
index 40a86a1e58c..5f35a0b26f3 100644
--- a/app/assets/javascripts/clusters_list/store/actions.js
+++ b/app/assets/javascripts/clusters_list/store/actions.js
@@ -1,5 +1,5 @@
import * as Sentry from '@sentry/browser';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
import Poll from '~/lib/utils/poll';
@@ -64,7 +64,9 @@ export const fetchClusters = ({ state, commit, dispatch }) => {
commit(types.SET_LOADING_CLUSTERS, false);
commit(types.SET_LOADING_NODES, false);
- flash(__('Clusters|An error occurred while loading clusters'));
+ createFlash({
+ message: __('Clusters|An error occurred while loading clusters'),
+ });
dispatch('reportSentryError', { error: response, tag: 'fetchClustersErrorCallback' });
},
diff --git a/app/assets/javascripts/code_quality_walkthrough/utils.js b/app/assets/javascripts/code_quality_walkthrough/utils.js
index 97c80f6eff7..894ec9a171d 100644
--- a/app/assets/javascripts/code_quality_walkthrough/utils.js
+++ b/app/assets/javascripts/code_quality_walkthrough/utils.js
@@ -1,6 +1,7 @@
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { getExperimentData } from '~/experimentation/utils';
-import { setCookie, getCookie, getParameterByName } from '~/lib/utils/common_utils';
+import { setCookie, getCookie } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import { EXPERIMENT_NAME } from './constants';
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 29ad6cc4125..8d88b682df2 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import CommitPipelinesTable from './pipelines_table.vue';
/**
* Used in:
@@ -14,25 +13,24 @@ export default () => {
if (pipelineTableViewEl) {
// Update MR and Commits tabs
pipelineTableViewEl.addEventListener('update-pipelines-count', (event) => {
- if (
- event.detail.pipelines &&
- event.detail.pipelines.count &&
- event.detail.pipelines.count.all
- ) {
+ if (event.detail.pipelineCount) {
const badge = document.querySelector('.js-pipelines-mr-count');
- badge.textContent = event.detail.pipelines.count.all;
+ badge.textContent = event.detail.pipelineCount;
}
});
if (pipelineTableViewEl.dataset.disableInitialization === undefined) {
const table = new Vue({
+ components: {
+ CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ },
provide: {
artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
},
render(createElement) {
- return createElement(CommitPipelinesTable, {
+ return createElement('commit-pipelines-table', {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index ddca5bc7d4f..42d46dc3d5d 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlEmptyState, GlLoadingIcon, GlModal, GlLink } from '@gitlab/ui';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import eventHub from '~/pipelines/event_hub';
import PipelinesMixin from '~/pipelines/mixins/pipelines_mixin';
@@ -133,15 +133,15 @@ export default {
this.store.storePagination(resp.headers);
this.setCommonData(pipelines);
- const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
- detail: {
- pipelines: resp.data,
- },
- });
+ if (resp.headers?.['x-total']) {
+ const updatePipelinesEvent = new CustomEvent('update-pipelines-count', {
+ detail: { pipelineCount: resp.headers['x-total'] },
+ });
- // notifiy to update the count in tabs
- if (this.$el.parentElement) {
- this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
+ // notifiy to update the count in tabs
+ if (this.$el.parentElement) {
+ this.$el.parentElement.dispatchEvent(updatePipelinesEvent);
+ }
}
},
/**
@@ -251,7 +251,7 @@ export default {
}}
</p>
<gl-link
- href="/help/ci/merge_request_pipelines/index.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
+ href="/help/ci/pipelines/merge_request_pipelines.html#run-pipelines-in-the-parent-project-for-merge-requests-from-a-forked-project"
target="_blank"
>
{{ s__('Pipelines|More Information') }}
diff --git a/app/assets/javascripts/commit_merge_requests.js b/app/assets/javascripts/commit_merge_requests.js
index e382356841c..f973bf51b57 100644
--- a/app/assets/javascripts/commit_merge_requests.js
+++ b/app/assets/javascripts/commit_merge_requests.js
@@ -1,6 +1,5 @@
-/* global Flash */
-
import $ from 'jquery';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { n__, s__ } from './locale';
@@ -71,5 +70,9 @@ export function fetchCommitMergeRequests() {
$container.html($content);
})
- .catch(() => Flash(s__('Commits|An error occurred while fetching merge requests data.')));
+ .catch(() =>
+ createFlash({
+ message: s__('Commits|An error occurred while fetching merge requests data.'),
+ }),
+ );
}
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index da7fc88d8ac..39dc4a4e9e5 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -93,7 +93,7 @@ export default class CommitsList {
.text(n__('%d commit', '%d commits', commitsCount));
}
- localTimeAgo($processedData.find('.js-timeago'));
+ localTimeAgo($processedData.find('.js-timeago').get());
return processedData;
}
diff --git a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
index 6b07b7e3772..5f778af1dbb 100644
--- a/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
+++ b/app/assets/javascripts/confidential_merge_request/components/project_form_group.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import createFlash from '~/flash';
import Api from '../../api';
-import createFlash from '../../flash';
import { __ } from '../../locale';
import state from '../state';
import Dropdown from './dropdown.vue';
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index c6ab2e189ef..9a51def7075 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,10 +1,12 @@
<script>
+import { GlAlert } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
import { ContentEditor } from '../services/content_editor';
import TopToolbar from './top_toolbar.vue';
export default {
components: {
+ GlAlert,
TiptapEditorContent,
TopToolbar,
},
@@ -14,15 +16,30 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ error: '',
+ };
+ },
+ mounted() {
+ this.contentEditor.tiptapEditor.on('error', (error) => {
+ this.error = error;
+ });
+ },
};
</script>
<template>
- <div
- data-testid="content-editor"
- class="md-area"
- :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
- >
- <top-toolbar class="gl-mb-4" :content-editor="contentEditor" />
- <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ <div>
+ <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="error = ''">
+ {{ error }}
+ </gl-alert>
+ <div
+ data-testid="content-editor"
+ class="md-area"
+ :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
+ >
+ <top-toolbar ref="toolbar" class="gl-mb-4" :content-editor="contentEditor" />
+ <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_image_button.vue b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
new file mode 100644
index 00000000000..ebeee16dbec
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
@@ -0,0 +1,110 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownForm,
+ GlButton,
+ GlFormInputGroup,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import { acceptedMimes } from '../extensions/image';
+import { getImageAlt } from '../services/utils';
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownForm,
+ GlFormInputGroup,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlButton,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ tiptapEditor: {
+ type: TiptapEditor,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ imgSrc: '',
+ };
+ },
+ methods: {
+ resetFields() {
+ this.imgSrc = '';
+ this.$refs.fileSelector.value = '';
+ },
+ insertImage() {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .setImage({
+ src: this.imgSrc,
+ canonicalSrc: this.imgSrc,
+ alt: getImageAlt(this.imgSrc),
+ })
+ .run();
+
+ this.resetFields();
+ this.emitExecute();
+ },
+ emitExecute(source = 'url') {
+ this.$emit('execute', { contentType: 'image', value: source });
+ },
+ openFileUpload() {
+ this.$refs.fileSelector.click();
+ },
+ onFileSelect(e) {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .uploadImage({
+ file: e.target.files[0],
+ })
+ .run();
+
+ this.resetFields();
+ this.emitExecute('upload');
+ },
+ },
+ acceptedMimes,
+};
+</script>
+<template>
+ <gl-dropdown
+ v-gl-tooltip
+ :aria-label="__('Insert image')"
+ :title="__('Insert image')"
+ size="small"
+ category="tertiary"
+ icon="media"
+ @hidden="resetFields()"
+ >
+ <gl-dropdown-form class="gl-px-3!">
+ <gl-form-input-group v-model="imgSrc" :placeholder="__('Image URL')">
+ <template #append>
+ <gl-button variant="confirm" @click="insertImage">{{ __('Insert') }}</gl-button>
+ </template>
+ </gl-form-input-group>
+ </gl-dropdown-form>
+ <gl-dropdown-divider />
+ <gl-dropdown-item @click="openFileUpload">
+ {{ __('Upload image') }}
+ </gl-dropdown-item>
+
+ <input
+ ref="fileSelector"
+ type="file"
+ name="content_editor_image"
+ :accept="$options.acceptedMimes"
+ class="gl-display-none"
+ @change="onFileSelect"
+ />
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
index f706080eaa1..8f57959a73f 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
@@ -43,14 +43,22 @@ export default {
},
mounted() {
this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
- const { href } = editor.getAttributes(linkContentType);
+ const { canonicalSrc, href } = editor.getAttributes(linkContentType);
- this.linkHref = href;
+ this.linkHref = canonicalSrc || href;
});
},
methods: {
updateLink() {
- this.tiptapEditor.chain().focus().unsetLink().setLink({ href: this.linkHref }).run();
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .unsetLink()
+ .setLink({
+ href: this.linkHref,
+ canonicalSrc: this.linkHref,
+ })
+ .run();
this.$emit('execute', { contentType: linkContentType });
},
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
new file mode 100644
index 00000000000..49d3006e9bf
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui';
+import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import { __, sprintf } from '~/locale';
+import { clamp } from '../services/utils';
+
+export const tableContentType = 'table';
+
+const MIN_ROWS = 3;
+const MIN_COLS = 3;
+const MAX_ROWS = 8;
+const MAX_COLS = 8;
+
+export default {
+ components: {
+ GlDropdown,
+ GlDropdownDivider,
+ GlDropdownForm,
+ GlButton,
+ },
+ props: {
+ tiptapEditor: {
+ type: TiptapEditor,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ maxRows: MIN_ROWS,
+ maxCols: MIN_COLS,
+ rows: 1,
+ cols: 1,
+ };
+ },
+ methods: {
+ list(n) {
+ return new Array(n).fill().map((_, i) => i + 1);
+ },
+ setRowsAndCols(rows, cols) {
+ this.rows = rows;
+ this.cols = cols;
+ this.maxRows = clamp(rows + 1, MIN_ROWS, MAX_ROWS);
+ this.maxCols = clamp(cols + 1, MIN_COLS, MAX_COLS);
+ },
+ resetState() {
+ this.rows = 1;
+ this.cols = 1;
+ },
+ insertTable() {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .insertTable({
+ rows: this.rows,
+ cols: this.cols,
+ withHeaderRow: true,
+ })
+ .run();
+
+ this.resetState();
+
+ this.$emit('execute', { contentType: 'table' });
+ },
+ getButtonLabel(rows, cols) {
+ return sprintf(__('Insert a %{rows}x%{cols} table.'), { rows, cols });
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown size="small" category="tertiary" icon="table">
+ <gl-dropdown-form class="gl-px-3! gl-w-auto!">
+ <div class="gl-w-auto!">
+ <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
+ <gl-button
+ v-for="c of list(maxCols)"
+ :key="c"
+ :data-testid="`table-${r}-${c}`"
+ :class="{ 'gl-bg-blue-50!': r <= rows && c <= cols }"
+ :aria-label="getButtonLabel(r, c)"
+ class="gl-display-inline! gl-px-0! gl-w-5! gl-h-5! gl-rounded-0!"
+ @mouseover="setRowsAndCols(r, c)"
+ @click="insertTable()"
+ />
+ </div>
+ <gl-dropdown-divider />
+ {{ getButtonLabel(rows, cols) }}
+ </div>
+ </gl-dropdown-form>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index d3363ce092b..fafc7a660e7 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -4,7 +4,9 @@ import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '
import { ContentEditor } from '../services/content_editor';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
+import ToolbarImageButton from './toolbar_image_button.vue';
import ToolbarLinkButton from './toolbar_link_button.vue';
+import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
const trackingMixin = Tracking.mixin({
@@ -16,6 +18,8 @@ export default {
ToolbarButton,
ToolbarTextStyleDropdown,
ToolbarLinkButton,
+ ToolbarTableButton,
+ ToolbarImageButton,
Divider,
},
mixins: [trackingMixin],
@@ -87,6 +91,12 @@ export default {
@execute="trackToolbarControlExecution"
/>
<divider />
+ <toolbar-image-button
+ ref="imageButton"
+ data-testid="image"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
<toolbar-button
data-testid="blockquote"
content-type="blockquote"
@@ -123,5 +133,23 @@ export default {
:tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-button
+ data-testid="horizontal-rule"
+ content-type="horizontalRule"
+ icon-name="dash"
+ editor-command="setHorizontalRule"
+ :label="__('Add a horizontal rule')"
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-table-button
+ :tiptap-editor="contentEditor.tiptapEditor"
+ @execute="trackToolbarControlExecution"
+ />
</div>
</template>
+<style>
+.gl-spinner-container {
+ text-align: left;
+}
+</style>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
new file mode 100644
index 00000000000..3762324a431
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+
+export default {
+ name: 'ImageWrapper',
+ components: {
+ NodeViewWrapper,
+ GlLoadingIcon,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-inline-block">
+ <span class="gl-relative">
+ <img
+ data-testid="image"
+ class="gl-max-w-full gl-h-auto"
+ :class="{ 'gl-opacity-5': node.attrs.uploading }"
+ :src="node.attrs.src"
+ />
+ <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" />
+ </span>
+ </node-view-wrapper>
+</template>
diff --git a/app/assets/javascripts/content_editor/extensions/hard_break.js b/app/assets/javascripts/content_editor/extensions/hard_break.js
index dc1ba431151..756eefa875c 100644
--- a/app/assets/javascripts/content_editor/extensions/hard_break.js
+++ b/app/assets/javascripts/content_editor/extensions/hard_break.js
@@ -1,5 +1,13 @@
import { HardBreak } from '@tiptap/extension-hard-break';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-export const tiptapExtension = HardBreak;
+const ExtendedHardBreak = HardBreak.extend({
+ addKeyboardShortcuts() {
+ return {
+ 'Shift-Enter': () => this.editor.commands.setHardBreak(),
+ };
+ },
+});
+
+export const tiptapExtension = ExtendedHardBreak;
export const serializer = defaultMarkdownSerializer.nodes.hard_break;
diff --git a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
index dcc59476518..c287938af5c 100644
--- a/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
+++ b/app/assets/javascripts/content_editor/extensions/horizontal_rule.js
@@ -1,5 +1,12 @@
+import { nodeInputRule } from '@tiptap/core';
import { HorizontalRule } from '@tiptap/extension-horizontal-rule';
import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
-export const tiptapExtension = HorizontalRule;
+export const hrInputRuleRegExp = /^---$/;
+
+export const tiptapExtension = HorizontalRule.extend({
+ addInputRules() {
+ return [nodeInputRule(hrInputRuleRegExp, this.type)];
+ },
+});
export const serializer = defaultMarkdownSerializer.nodes.horizontal_rule;
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index 287216e68d5..4dd8a1376ad 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -1,10 +1,65 @@
import { Image } from '@tiptap/extension-image';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import { Plugin, PluginKey } from 'prosemirror-state';
+import { __ } from '~/locale';
+import ImageWrapper from '../components/wrappers/image.vue';
+import { uploadFile } from '../services/upload_file';
+import { getImageAlt, readFileAsDataURL } from '../services/utils';
+
+export const acceptedMimes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg'];
+
+const resolveImageEl = (element) =>
+ element.nodeName === 'IMG' ? element : element.querySelector('img');
+
+const startFileUpload = async ({ editor, file, uploadsPath, renderMarkdown }) => {
+ const encodedSrc = await readFileAsDataURL(file);
+ const { view } = editor;
+
+ editor.commands.setImage({ uploading: true, src: encodedSrc });
+
+ const { state } = view;
+ const position = state.selection.from - 1;
+ const { tr } = state;
+
+ try {
+ const { src, canonicalSrc } = await uploadFile({ file, uploadsPath, renderMarkdown });
+
+ view.dispatch(
+ tr.setNodeMarkup(position, undefined, {
+ uploading: false,
+ src: encodedSrc,
+ alt: getImageAlt(src),
+ canonicalSrc,
+ }),
+ );
+ } catch (e) {
+ editor.commands.deleteRange({ from: position, to: position + 1 });
+ editor.emit('error', __('An error occurred while uploading the image. Please try again.'));
+ }
+};
+
+const handleFileEvent = ({ editor, file, uploadsPath, renderMarkdown }) => {
+ if (acceptedMimes.includes(file?.type)) {
+ startFileUpload({ editor, file, uploadsPath, renderMarkdown });
+
+ return true;
+ }
+
+ return false;
+};
const ExtendedImage = Image.extend({
+ defaultOptions: {
+ ...Image.options,
+ uploadsPath: null,
+ renderMarkdown: null,
+ },
addAttributes() {
return {
...this.parent?.(),
+ uploading: {
+ default: false,
+ },
src: {
default: null,
/*
@@ -14,17 +69,25 @@ const ExtendedImage = Image.extend({
* attribute.
*/
parseHTML: (element) => {
- const img = element.querySelector('img');
+ const img = resolveImageEl(element);
return {
src: img.dataset.src || img.getAttribute('src'),
};
},
},
+ canonicalSrc: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ canonicalSrc: element.dataset.canonicalSrc,
+ };
+ },
+ },
alt: {
default: null,
parseHTML: (element) => {
- const img = element.querySelector('img');
+ const img = resolveImageEl(element);
return {
alt: img.getAttribute('alt'),
@@ -44,7 +107,62 @@ const ExtendedImage = Image.extend({
},
];
},
-}).configure({ inline: true });
+ addCommands() {
+ return {
+ ...this.parent(),
+ uploadImage: ({ file }) => () => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ handleFileEvent({ file, uploadsPath, renderMarkdown, editor: this.editor });
+ },
+ };
+ },
+ addProseMirrorPlugins() {
+ const { editor } = this;
+
+ return [
+ new Plugin({
+ key: new PluginKey('handleDropAndPasteImages'),
+ props: {
+ handlePaste: (_, event) => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ return handleFileEvent({
+ editor,
+ file: event.clipboardData.files[0],
+ uploadsPath,
+ renderMarkdown,
+ });
+ },
+ handleDrop: (_, event) => {
+ const { uploadsPath, renderMarkdown } = this.options;
+
+ return handleFileEvent({
+ editor,
+ file: event.dataTransfer.files[0],
+ uploadsPath,
+ renderMarkdown,
+ });
+ },
+ },
+ }),
+ ];
+ },
+ addNodeView() {
+ return VueNodeViewRenderer(ImageWrapper);
+ },
+});
+
+const serializer = (state, node) => {
+ const { alt, canonicalSrc, src, title } = node.attrs;
+ const quotedTitle = title ? ` ${state.quote(title)}` : '';
+
+ state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
+};
-export const tiptapExtension = ExtendedImage;
-export const serializer = defaultMarkdownSerializer.nodes.image;
+export const configure = ({ renderMarkdown, uploadsPath }) => {
+ return {
+ tiptapExtension: ExtendedImage.configure({ inline: true, renderMarkdown, uploadsPath }),
+ serializer,
+ };
+};
diff --git a/app/assets/javascripts/content_editor/extensions/link.js b/app/assets/javascripts/content_editor/extensions/link.js
index 6f5f81cbf93..12019ab4636 100644
--- a/app/assets/javascripts/content_editor/extensions/link.js
+++ b/app/assets/javascripts/content_editor/extensions/link.js
@@ -1,9 +1,7 @@
import { markInputRule } from '@tiptap/core';
import { Link } from '@tiptap/extension-link';
-import { defaultMarkdownSerializer } from 'prosemirror-markdown/src/to_markdown';
export const markdownLinkSyntaxInputRuleRegExp = /(?:^|\s)\[([\w|\s|-]+)\]\((?<href>.+?)\)$/gm;
-
export const urlSyntaxRegExp = /(?:^|\s)(?<href>(?:https?:\/\/|www\.)[\S]+)(?:\s|\n)$/gim;
const extractHrefFromMatch = (match) => {
@@ -29,8 +27,37 @@ export const tiptapExtension = Link.extend({
markInputRule(urlSyntaxRegExp, this.type, extractHrefFromMatch),
];
},
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ href: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ href: element.getAttribute('href'),
+ };
+ },
+ },
+ canonicalSrc: {
+ default: null,
+ parseHTML: (element) => {
+ return {
+ canonicalSrc: element.dataset.canonicalSrc,
+ };
+ },
+ },
+ };
+ },
}).configure({
openOnClick: false,
});
-export const serializer = defaultMarkdownSerializer.marks.link;
+export const serializer = {
+ open() {
+ return '[';
+ },
+ close(state, mark) {
+ const href = mark.attrs.canonicalSrc || mark.attrs.href;
+ return `](${state.esc(href)}${mark.attrs.title ? ` ${state.quote(mark.attrs.title)}` : ''})`;
+ },
+};
diff --git a/app/assets/javascripts/content_editor/extensions/table.js b/app/assets/javascripts/content_editor/extensions/table.js
new file mode 100644
index 00000000000..566f7a21a85
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/table.js
@@ -0,0 +1,7 @@
+import { Table } from '@tiptap/extension-table';
+
+export const tiptapExtension = Table;
+
+export function serializer(state, node) {
+ state.renderContent(node);
+}
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
new file mode 100644
index 00000000000..6c25b867466
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -0,0 +1,9 @@
+import { TableCell } from '@tiptap/extension-table-cell';
+
+export const tiptapExtension = TableCell.extend({
+ content: 'inline*',
+});
+
+export function serializer(state, node) {
+ state.renderInline(node);
+}
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
new file mode 100644
index 00000000000..3475857b9e6
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -0,0 +1,9 @@
+import { TableHeader } from '@tiptap/extension-table-header';
+
+export const tiptapExtension = TableHeader.extend({
+ content: 'inline*',
+});
+
+export function serializer(state, node) {
+ state.renderInline(node);
+}
diff --git a/app/assets/javascripts/content_editor/extensions/table_row.js b/app/assets/javascripts/content_editor/extensions/table_row.js
new file mode 100644
index 00000000000..07d2eb4faa2
--- /dev/null
+++ b/app/assets/javascripts/content_editor/extensions/table_row.js
@@ -0,0 +1,51 @@
+import { TableRow } from '@tiptap/extension-table-row';
+
+export const tiptapExtension = TableRow.extend({
+ allowGapCursor: false,
+});
+
+export function serializer(state, node) {
+ const isHeaderRow = node.child(0).type.name === 'tableHeader';
+
+ const renderRow = () => {
+ const cellWidths = [];
+
+ state.flushClose(1);
+
+ state.write('| ');
+ node.forEach((cell, _, i) => {
+ if (i) state.write(' | ');
+
+ const { length } = state.out;
+ state.render(cell, node, i);
+ cellWidths.push(state.out.length - length);
+ });
+ state.write(' |');
+
+ state.closeBlock(node);
+
+ return cellWidths;
+ };
+
+ const renderHeaderRow = (cellWidths) => {
+ state.flushClose(1);
+
+ state.write('|');
+ node.forEach((cell, _, i) => {
+ if (i) state.write('|');
+
+ state.write(cell.attrs.align === 'center' ? ':' : '-');
+ state.write(state.repeat('-', cellWidths[i]));
+ state.write(cell.attrs.align === 'center' || cell.attrs.align === 'right' ? ':' : '-');
+ });
+ state.write('|');
+
+ state.closeBlock(node);
+ };
+
+ if (isHeaderRow) {
+ renderHeaderRow(renderRow());
+ } else {
+ renderRow();
+ }
+}
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 8a54da6f57d..9251fdbbdc5 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -20,35 +20,16 @@ import * as ListItem from '../extensions/list_item';
import * as OrderedList from '../extensions/ordered_list';
import * as Paragraph from '../extensions/paragraph';
import * as Strike from '../extensions/strike';
+import * as Table from '../extensions/table';
+import * as TableCell from '../extensions/table_cell';
+import * as TableHeader from '../extensions/table_header';
+import * as TableRow from '../extensions/table_row';
import * as Text from '../extensions/text';
import buildSerializerConfig from './build_serializer_config';
import { ContentEditor } from './content_editor';
import createMarkdownSerializer from './markdown_serializer';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
-const builtInContentEditorExtensions = [
- Blockquote,
- Bold,
- BulletList,
- Code,
- CodeBlockHighlight,
- Document,
- Dropcursor,
- Gapcursor,
- HardBreak,
- Heading,
- History,
- HorizontalRule,
- Image,
- Italic,
- Link,
- ListItem,
- OrderedList,
- Paragraph,
- Strike,
- Text,
-];
-
const collectTiptapExtensions = (extensions = []) =>
extensions.map(({ tiptapExtension }) => tiptapExtension);
@@ -63,11 +44,43 @@ const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
...options,
});
-export const createContentEditor = ({ renderMarkdown, extensions = [], tiptapOptions } = {}) => {
+export const createContentEditor = ({
+ renderMarkdown,
+ uploadsPath,
+ extensions = [],
+ tiptapOptions,
+} = {}) => {
if (!isFunction(renderMarkdown)) {
throw new Error(PROVIDE_SERIALIZER_OR_RENDERER_ERROR);
}
+ const builtInContentEditorExtensions = [
+ Blockquote,
+ Bold,
+ BulletList,
+ Code,
+ CodeBlockHighlight,
+ Document,
+ Dropcursor,
+ Gapcursor,
+ HardBreak,
+ Heading,
+ History,
+ HorizontalRule,
+ Image.configure({ uploadsPath, renderMarkdown }),
+ Italic,
+ Link,
+ ListItem,
+ OrderedList,
+ Paragraph,
+ Strike,
+ TableCell,
+ TableHeader,
+ TableRow,
+ Table,
+ Text,
+ ];
+
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
const tiptapExtensions = collectTiptapExtensions(allExtensions).map(trackInputRulesAndShortcuts);
const tiptapEditor = createTiptapEditor({ extensions: tiptapExtensions, ...tiptapOptions });
diff --git a/app/assets/javascripts/content_editor/services/upload_file.js b/app/assets/javascripts/content_editor/services/upload_file.js
new file mode 100644
index 00000000000..613c53144a1
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/upload_file.js
@@ -0,0 +1,44 @@
+import axios from '~/lib/utils/axios_utils';
+
+const extractAttachmentLinkUrl = (html) => {
+ const parser = new DOMParser();
+ const { body } = parser.parseFromString(html, 'text/html');
+ const link = body.querySelector('a');
+ const src = link.getAttribute('href');
+ const { canonicalSrc } = link.dataset;
+
+ return { src, canonicalSrc };
+};
+
+/**
+ * Uploads a file with a post request to the URL indicated
+ * in the uploadsPath parameter. The expected response of the
+ * uploads service is a JSON object that contains, at least, a
+ * link property. The link property should contain markdown link
+ * definition (i.e. [GitLab](https://gitlab.com)).
+ *
+ * This Markdown will be rendered to extract its canonical and full
+ * URLs using GitLab Flavored Markdown renderer in the backend.
+ *
+ * @param {Object} params
+ * @param {String} params.uploadsPath An absolute URL that points to a service
+ * that allows sending a file for uploading via POST request.
+ * @param {String} params.renderMarkdown A function that accepts a markdown string
+ * and returns a rendered version in HTML format.
+ * @param {File} params.file The file to upload
+ *
+ * @returns Returns an object with two properties:
+ *
+ * canonicalSrc: The URL as defined in the Markdown
+ * src: The absolute URL that points to the resource in the server
+ */
+export const uploadFile = async ({ uploadsPath, renderMarkdown, file }) => {
+ const formData = new FormData();
+ formData.append('file', file, file.name);
+
+ const { data } = await axios.post(uploadsPath, formData);
+ const { markdown } = data.link;
+ const rendered = await renderMarkdown(markdown);
+
+ return extractAttachmentLinkUrl(rendered);
+};
diff --git a/app/assets/javascripts/content_editor/services/utils.js b/app/assets/javascripts/content_editor/services/utils.js
index cf5234bbff8..2a2c7f617da 100644
--- a/app/assets/javascripts/content_editor/services/utils.js
+++ b/app/assets/javascripts/content_editor/services/utils.js
@@ -3,3 +3,17 @@ export const hasSelection = (tiptapEditor) => {
return from < to;
};
+
+export const getImageAlt = (src) => {
+ return src.replace(/^.*\/|\..*$/g, '').replace(/\W+/g, ' ');
+};
+
+export const readFileAsDataURL = (file) => {
+ return new Promise((resolve) => {
+ const reader = new FileReader();
+ reader.addEventListener('load', (e) => resolve(e.target.result), { once: true });
+ reader.readAsDataURL(file);
+ });
+};
+
+export const clamp = (n, min, max) => Math.max(Math.min(n, max), min);
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 25ce6500094..512f060e2ea 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -204,15 +204,16 @@ export default {
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- class="gl-mb-5"
- :width="width"
- :data="masterChartData"
- :option="masterChartOptions"
- :height="masterChartHeight"
- @created="onMasterChartCreated"
- />
+ <template #default="{ width }">
+ <gl-area-chart
+ class="gl-mb-5"
+ :width="width"
+ :data="masterChartData"
+ :option="masterChartOptions"
+ :height="masterChartHeight"
+ @created="onMasterChartCreated"
+ />
+ </template>
</resizable-chart-container>
<div class="row">
@@ -226,14 +227,15 @@ export default {
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
</p>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- :width="width"
- :data="contributor.dates"
- :option="individualChartOptions"
- :height="individualChartHeight"
- @created="onIndividualChartCreated"
- />
+ <template #default="{ width }">
+ <gl-area-chart
+ :width="width"
+ :data="contributor.dates"
+ :option="individualChartOptions"
+ :height="individualChartHeight"
+ @created="onIndividualChartCreated"
+ />
+ </template>
</resizable-chart-container>
</div>
</div>
diff --git a/app/assets/javascripts/contributors/stores/actions.js b/app/assets/javascripts/contributors/stores/actions.js
index 72aae3af692..4cc0a6a6509 100644
--- a/app/assets/javascripts/contributors/stores/actions.js
+++ b/app/assets/javascripts/contributors/stores/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import service from '../services/contributors_service';
import * as types from './mutation_types';
@@ -13,5 +13,9 @@ export const fetchChartData = ({ commit }, endpoint) => {
commit(types.SET_CHART_DATA, data);
commit(types.SET_LOADING_STATE, false);
})
- .catch(() => flash(__('An error occurred while loading chart data')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while loading chart data'),
+ }),
+ );
};
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
index 6b18455bfcc..23c477bfbfd 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue
@@ -95,7 +95,7 @@ export default {
</li>
</ul>
</div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
+ <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
index b6f0bdbf01d..aba6dd4b493 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue
@@ -160,7 +160,7 @@ export default {
</li>
</ul>
</div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
+ <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
index daab42c7e60..027ce74753e 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
+++ b/app/assets/javascripts/create_cluster/gke_cluster/components/gke_zone_dropdown.vue
@@ -84,7 +84,7 @@ export default {
</li>
</ul>
</div>
- <div class="dropdown-loading"><gl-loading-icon /></div>
+ <div class="dropdown-loading"><gl-loading-icon size="sm" /></div>
</div>
</div>
<span
diff --git a/app/assets/javascripts/create_cluster/gke_cluster/index.js b/app/assets/javascripts/create_cluster/gke_cluster/index.js
index 4eafbdb7265..3a42b460e1c 100644
--- a/app/assets/javascripts/create_cluster/gke_cluster/index.js
+++ b/app/assets/javascripts/create_cluster/gke_cluster/index.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import GkeMachineTypeDropdown from './components/gke_machine_type_dropdown.vue';
import GkeProjectIdDropdown from './components/gke_project_id_dropdown.vue';
import GkeSubmitButton from './components/gke_submit_button.vue';
@@ -59,7 +59,9 @@ const mountGkeSubmitButton = () => {
};
const gkeDropdownErrorHandler = () => {
- Flash(CONSTANTS.GCP_API_ERROR);
+ createFlash({
+ message: CONSTANTS.GCP_API_ERROR,
+ });
};
const initializeGapiClient = (gapi) => () => {
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 0aae63e1648..411e482b0ce 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
@@ -218,7 +218,7 @@ export default {
@input="debouncedValidateQuery"
/>
<span v-if="queryValidateInFlight" class="form-text text-muted">
- <gl-loading-icon :inline="true" class="mr-1 align-middle" />
+ <gl-loading-icon size="sm" :inline="true" class="mr-1 align-middle" />
{{ s__('Metrics|Validating query') }}
</span>
<slot v-if="!queryValidateInFlight" name="valid-feedback">
diff --git a/app/assets/javascripts/cycle_analytics/components/filter_bar.vue b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
new file mode 100644
index 00000000000..5140b05e189
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/filter_bar.vue
@@ -0,0 +1,142 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import { __ } from '~/locale';
+import {
+ OPERATOR_IS_ONLY,
+ DEFAULT_NONE_ANY,
+} from '~/vue_shared/components/filtered_search_bar/constants';
+import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import {
+ prepareTokens,
+ 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 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';
+
+export default {
+ name: 'FilterBar',
+ components: {
+ FilteredSearchBar,
+ UrlSync,
+ },
+ props: {
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState('filters', {
+ selectedMilestone: (state) => state.milestones.selected,
+ selectedAuthor: (state) => state.authors.selected,
+ selectedLabelList: (state) => state.labels.selectedList,
+ selectedAssigneeList: (state) => state.assignees.selectedList,
+ milestonesData: (state) => state.milestones.data,
+ labelsData: (state) => state.labels.data,
+ authorsData: (state) => state.authors.data,
+ assigneesData: (state) => state.assignees.data,
+ }),
+ tokens() {
+ return [
+ {
+ icon: 'clock',
+ title: __('Milestone'),
+ type: 'milestone',
+ token: MilestoneToken,
+ initialMilestones: this.milestonesData,
+ unique: true,
+ symbol: '%',
+ operators: OPERATOR_IS_ONLY,
+ fetchMilestones: this.fetchMilestones,
+ },
+ {
+ icon: 'labels',
+ title: __('Label'),
+ type: 'labels',
+ token: LabelToken,
+ defaultLabels: DEFAULT_NONE_ANY,
+ initialLabels: this.labelsData,
+ unique: false,
+ symbol: '~',
+ operators: OPERATOR_IS_ONLY,
+ fetchLabels: this.fetchLabels,
+ },
+ {
+ icon: 'pencil',
+ title: __('Author'),
+ type: 'author',
+ token: AuthorToken,
+ initialAuthors: this.authorsData,
+ unique: true,
+ operators: OPERATOR_IS_ONLY,
+ fetchAuthors: this.fetchAuthors,
+ },
+ {
+ icon: 'user',
+ title: __('Assignees'),
+ type: 'assignees',
+ token: AuthorToken,
+ defaultAuthors: [],
+ initialAuthors: this.assigneesData,
+ unique: false,
+ operators: OPERATOR_IS_ONLY,
+ fetchAuthors: this.fetchAssignees,
+ },
+ ];
+ },
+ query() {
+ return filterToQueryObject({
+ milestone_title: this.selectedMilestone,
+ author_username: this.selectedAuthor,
+ label_name: this.selectedLabelList,
+ assignee_username: this.selectedAssigneeList,
+ });
+ },
+ },
+ methods: {
+ ...mapActions('filters', [
+ 'setFilters',
+ 'fetchMilestones',
+ 'fetchLabels',
+ 'fetchAuthors',
+ 'fetchAssignees',
+ ]),
+ initialFilterValue() {
+ return prepareTokens({
+ milestone: this.selectedMilestone,
+ author: this.selectedAuthor,
+ assignees: this.selectedAssigneeList,
+ labels: this.selectedLabelList,
+ });
+ },
+ handleFilter(filters) {
+ const { labels, milestone, author, assignees } = processFilters(filters);
+
+ this.setFilters({
+ selectedAuthor: author ? author[0] : null,
+ selectedMilestone: milestone ? milestone[0] : null,
+ selectedAssigneeList: assignees || [],
+ selectedLabelList: labels || [],
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <filtered-search-bar
+ class="gl-flex-grow-1"
+ :namespace="groupPath"
+ recent-searches-storage-key="value-stream-analytics"
+ :search-input-placeholder="__('Filter results')"
+ :tokens="tokens"
+ :initial-filter-value="initialFilterValue()"
+ @onFilter="handleFilter"
+ />
+ <url-sync :query="query" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
new file mode 100644
index 00000000000..b622b0441e2
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/formatted_stage_count.vue
@@ -0,0 +1,32 @@
+<script>
+import { s__, n__, sprintf, formatNumber } from '~/locale';
+
+export default {
+ props: {
+ stageCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ formattedStageCount() {
+ if (!this.stageCount) {
+ return '-';
+ } else if (this.stageCount > 1000) {
+ return sprintf(s__('ValueStreamAnalytics|%{stageCount}+ items'), {
+ stageCount: formatNumber(1000),
+ });
+ }
+
+ return sprintf(n__('%{count} item', '%{count} items', this.stageCount), {
+ count: formatNumber(this.stageCount),
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <span>{{ formattedStageCount }}</span>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
index c1e33f73b13..47fafc3b90c 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -7,6 +7,7 @@ import {
} from '@gitlab/ui';
import Tracking from '~/tracking';
import { OVERVIEW_STAGE_ID } from '../constants';
+import FormattedStageCount from './formatted_stage_count.vue';
export default {
name: 'PathNavigation',
@@ -14,6 +15,7 @@ export default {
GlPath,
GlSkeletonLoading,
GlPopover,
+ FormattedStageCount,
},
directives: {
SafeHtml,
@@ -44,9 +46,6 @@ export default {
showPopover({ id }) {
return id && id !== OVERVIEW_STAGE_ID;
},
- hasStageCount({ stageCount = null }) {
- return stageCount !== null;
- },
onSelectStage($event) {
this.$emit('selected', $event);
this.track('click_path_navigation', {
@@ -88,10 +87,7 @@ export default {
{{ s__('ValueStreamEvent|Items in stage') }}
</div>
<div class="gl-pb-4 gl-font-weight-bold">
- <template v-if="hasStageCount(pathItem)">{{
- n__('%d item', '%d items', pathItem.stageCount)
- }}</template>
- <template v-else>-</template>
+ <formatted-stage-count :stage-count="pathItem.stageCount" />
</div>
</div>
</div>
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
new file mode 100644
index 00000000000..6b1e537dc77
--- /dev/null
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
@@ -0,0 +1,93 @@
+<script>
+import DateRange from '~/analytics/shared/components/daterange.vue';
+import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
+import { DATE_RANGE_LIMIT, PROJECTS_PER_PAGE } from '~/analytics/shared/constants';
+import FilterBar from './filter_bar.vue';
+
+export default {
+ name: 'ValueStreamFilters',
+ components: {
+ DateRange,
+ ProjectsDropdownFilter,
+ FilterBar,
+ },
+ props: {
+ selectedProjects: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ hasProjectFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ hasDateRangeFilter: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ groupId: {
+ type: Number,
+ required: true,
+ },
+ groupPath: {
+ type: String,
+ required: true,
+ },
+ startDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ endDate: {
+ type: Date,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ projectsQueryParams() {
+ return {
+ first: PROJECTS_PER_PAGE,
+ includeSubgroups: true,
+ };
+ },
+ },
+ multiProjectSelect: true,
+ maxDateRange: DATE_RANGE_LIMIT,
+};
+</script>
+<template>
+ <div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom">
+ <filter-bar
+ class="js-filter-bar filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
+ :group-path="groupPath"
+ />
+ <div
+ v-if="hasDateRangeFilter || hasProjectFilter"
+ class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-justify-content-space-between"
+ >
+ <projects-dropdown-filter
+ v-if="hasProjectFilter"
+ :key="groupId"
+ class="js-projects-dropdown-filter project-select gl-mb-2 gl-lg-mb-0"
+ :group-id="groupId"
+ :group-namespace="groupPath"
+ :query-params="projectsQueryParams"
+ :multi-select="$options.multiProjectSelect"
+ :default-projects="selectedProjects"
+ @selected="$emit('selectProject', $event)"
+ />
+ <date-range
+ v-if="hasDateRangeFilter"
+ :start-date="startDate"
+ :end-date="endDate"
+ :max-date-range="$options.maxDateRange"
+ :include-selected-date="true"
+ class="js-daterange-picker"
+ @change="$emit('setDateRange', $event)"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/constants.js b/app/assets/javascripts/cycle_analytics/constants.js
index 96c89049e90..97f502326e5 100644
--- a/app/assets/javascripts/cycle_analytics/constants.js
+++ b/app/assets/javascripts/cycle_analytics/constants.js
@@ -1,3 +1,4 @@
+export const DEFAULT_DAYS_IN_PAST = 30;
export const DEFAULT_DAYS_TO_DISPLAY = 30;
export const OVERVIEW_STAGE_ID = 'overview';
diff --git a/app/assets/javascripts/cycle_analytics/index.js b/app/assets/javascripts/cycle_analytics/index.js
index 57cb220d9c9..615f96c3860 100644
--- a/app/assets/javascripts/cycle_analytics/index.js
+++ b/app/assets/javascripts/cycle_analytics/index.js
@@ -8,11 +8,24 @@ Vue.use(Translate);
export default () => {
const store = createStore();
const el = document.querySelector('#js-cycle-analytics');
- const { noAccessSvgPath, noDataSvgPath, requestPath, fullPath } = el.dataset;
+ const {
+ noAccessSvgPath,
+ noDataSvgPath,
+ requestPath,
+ fullPath,
+ projectId,
+ groupPath,
+ } = el.dataset;
store.dispatch('initializeVsa', {
+ projectId: parseInt(projectId, 10),
+ groupPath,
requestPath,
fullPath,
+ features: {
+ cycleAnalyticsForGroups:
+ (groupPath && gon?.licensed_features?.cycleAnalyticsForGroups) || false,
+ },
});
// eslint-disable-next-line no-new
diff --git a/app/assets/javascripts/cycle_analytics/store/actions.js b/app/assets/javascripts/cycle_analytics/store/actions.js
index faf1c37d86a..955f0c7271e 100644
--- a/app/assets/javascripts/cycle_analytics/store/actions.js
+++ b/app/assets/javascripts/cycle_analytics/store/actions.js
@@ -3,6 +3,7 @@ import {
getProjectValueStreams,
getProjectValueStreamStageData,
getProjectValueStreamMetrics,
+ getValueStreamStageMedian,
} from '~/api/analytics_api';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -35,21 +36,33 @@ export const receiveValueStreamsSuccess = ({ commit, dispatch }, data = []) => {
};
export const fetchValueStreams = ({ commit, dispatch, state }) => {
- const { fullPath } = state;
+ const {
+ fullPath,
+ features: { cycleAnalyticsForGroups },
+ } = state;
commit(types.REQUEST_VALUE_STREAMS);
+ const stageRequests = ['setSelectedStage'];
+ if (cycleAnalyticsForGroups) {
+ stageRequests.push('fetchStageMedians');
+ }
+
return getProjectValueStreams(fullPath)
.then(({ data }) => dispatch('receiveValueStreamsSuccess', data))
- .then(() => dispatch('setSelectedStage'))
+ .then(() => Promise.all(stageRequests.map((r) => dispatch(r))))
.catch(({ response: { status } }) => {
commit(types.RECEIVE_VALUE_STREAMS_ERROR, status);
});
};
-export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, commit }) => {
+export const fetchCycleAnalyticsData = ({
+ state: { requestPath },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_CYCLE_ANALYTICS_DATA);
- return getProjectValueStreamMetrics(requestPath, { 'cycle_analytics[start_date]': startDate })
+ return getProjectValueStreamMetrics(requestPath, legacyFilterParams)
.then(({ data }) => commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS, data))
.catch(() => {
commit(types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR);
@@ -59,13 +72,17 @@ export const fetchCycleAnalyticsData = ({ state: { requestPath, startDate }, com
});
};
-export const fetchStageData = ({ state: { requestPath, selectedStage, startDate }, commit }) => {
+export const fetchStageData = ({
+ state: { requestPath, selectedStage },
+ getters: { legacyFilterParams },
+ commit,
+}) => {
commit(types.REQUEST_STAGE_DATA);
return getProjectValueStreamStageData({
requestPath,
stageId: selectedStage.id,
- params: { 'cycle_analytics[start_date]': startDate },
+ params: legacyFilterParams,
})
.then(({ data }) => {
// when there's a query timeout, the request succeeds but the error is encoded in the response data
@@ -78,6 +95,37 @@ export const fetchStageData = ({ state: { requestPath, selectedStage, startDate
.catch(() => commit(types.RECEIVE_STAGE_DATA_ERROR));
};
+const getStageMedians = ({ stageId, vsaParams, filterParams = {} }) => {
+ return getValueStreamStageMedian({ ...vsaParams, stageId }, filterParams).then(({ data }) => ({
+ id: stageId,
+ value: data?.value || null,
+ }));
+};
+
+export const fetchStageMedians = ({
+ state: { stages },
+ getters: { requestParams: vsaParams, filterParams },
+ commit,
+}) => {
+ commit(types.REQUEST_STAGE_MEDIANS);
+ return Promise.all(
+ stages.map(({ id: stageId }) =>
+ getStageMedians({
+ vsaParams,
+ stageId,
+ filterParams,
+ }),
+ ),
+ )
+ .then((data) => commit(types.RECEIVE_STAGE_MEDIANS_SUCCESS, data))
+ .catch((error) => {
+ commit(types.RECEIVE_STAGE_MEDIANS_ERROR, error);
+ createFlash({
+ message: __('There was an error fetching median data for stages'),
+ });
+ });
+};
+
export const setSelectedStage = ({ dispatch, commit, state: { stages } }, selectedStage = null) => {
const stage = selectedStage || stages[0];
commit(types.SET_SELECTED_STAGE, stage);
@@ -92,6 +140,8 @@ const refetchData = (dispatch, commit) => {
.finally(() => commit(types.SET_LOADING, false));
};
+export const setFilters = ({ dispatch, commit }) => refetchData(dispatch, commit);
+
export const setDateRange = ({ dispatch, commit }, { startDate = DEFAULT_DAYS_TO_DISPLAY }) => {
commit(types.SET_DATE_RANGE, { startDate });
return refetchData(dispatch, commit);
diff --git a/app/assets/javascripts/cycle_analytics/store/getters.js b/app/assets/javascripts/cycle_analytics/store/getters.js
index c60a70ef147..66971ea8a2e 100644
--- a/app/assets/javascripts/cycle_analytics/store/getters.js
+++ b/app/assets/javascripts/cycle_analytics/store/getters.js
@@ -1,3 +1,5 @@
+import dateFormat from 'dateformat';
+import { dateFormats } from '~/analytics/shared/constants';
import { transformStagesForPathNavigation, filterStagesByHiddenStatus } from '../utils';
export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage }) => {
@@ -8,3 +10,30 @@ export const pathNavigationData = ({ stages, medians, stageCounts, selectedStage
selectedStage,
});
};
+
+export const requestParams = (state) => {
+ const {
+ selectedStage: { id: stageId = null },
+ groupPath: groupId,
+ selectedValueStream: { id: valueStreamId },
+ } = state;
+ return { valueStreamId, groupId, stageId };
+};
+
+const dateRangeParams = ({ createdAfter, createdBefore }) => ({
+ created_after: createdAfter ? dateFormat(createdAfter, dateFormats.isoDate) : null,
+ created_before: createdBefore ? dateFormat(createdBefore, dateFormats.isoDate) : null,
+});
+
+export const legacyFilterParams = ({ startDate }) => {
+ return {
+ 'cycle_analytics[start_date]': startDate,
+ };
+};
+
+export const filterParams = ({ id, ...rest }) => {
+ return {
+ project_ids: [id],
+ ...dateRangeParams(rest),
+ };
+};
diff --git a/app/assets/javascripts/cycle_analytics/store/index.js b/app/assets/javascripts/cycle_analytics/store/index.js
index c6ca88ea492..76e3e835016 100644
--- a/app/assets/javascripts/cycle_analytics/store/index.js
+++ b/app/assets/javascripts/cycle_analytics/store/index.js
@@ -7,6 +7,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
+import filters from '~/vue_shared/components/filtered_search_bar/store/modules/filters';
import * as actions from './actions';
import * as getters from './getters';
import mutations from './mutations';
@@ -20,4 +21,5 @@ export default () =>
getters,
mutations,
state,
+ modules: { filters },
});
diff --git a/app/assets/javascripts/cycle_analytics/store/mutation_types.js b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
index 4f3d430ec9f..11ed62a4081 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutation_types.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutation_types.js
@@ -20,3 +20,7 @@ export const RECEIVE_CYCLE_ANALYTICS_DATA_ERROR = 'RECEIVE_CYCLE_ANALYTICS_DATA_
export const REQUEST_STAGE_DATA = 'REQUEST_STAGE_DATA';
export const RECEIVE_STAGE_DATA_SUCCESS = 'RECEIVE_STAGE_DATA_SUCCESS';
export const RECEIVE_STAGE_DATA_ERROR = 'RECEIVE_STAGE_DATA_ERROR';
+
+export const REQUEST_STAGE_MEDIANS = 'REQUEST_STAGE_MEDIANS';
+export const RECEIVE_STAGE_MEDIANS_SUCCESS = 'RECEIVE_STAGE_MEDIANS_SUCCESS';
+export const RECEIVE_STAGE_MEDIANS_ERROR = 'RECEIVE_STAGE_MEDIANS_ERROR';
diff --git a/app/assets/javascripts/cycle_analytics/store/mutations.js b/app/assets/javascripts/cycle_analytics/store/mutations.js
index 0ae80116cd2..a8b7a607b66 100644
--- a/app/assets/javascripts/cycle_analytics/store/mutations.js
+++ b/app/assets/javascripts/cycle_analytics/store/mutations.js
@@ -1,11 +1,23 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { decorateData, decorateEvents, formatMedianValues } from '../utils';
+import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
+import {
+ decorateData,
+ decorateEvents,
+ formatMedianValues,
+ calculateFormattedDayInPast,
+} from '../utils';
import * as types from './mutation_types';
export default {
- [types.INITIALIZE_VSA](state, { requestPath, fullPath }) {
+ [types.INITIALIZE_VSA](state, { requestPath, fullPath, groupPath, projectId, features }) {
state.requestPath = requestPath;
state.fullPath = fullPath;
+ state.groupPath = groupPath;
+ state.id = projectId;
+ const { now, past } = calculateFormattedDayInPast(DEFAULT_DAYS_TO_DISPLAY);
+ state.createdBefore = now;
+ state.createdAfter = past;
+ state.features = features;
},
[types.SET_LOADING](state, loadingState) {
state.isLoading = loadingState;
@@ -18,6 +30,9 @@ export default {
},
[types.SET_DATE_RANGE](state, { startDate }) {
state.startDate = startDate;
+ const { now, past } = calculateFormattedDayInPast(startDate);
+ state.createdBefore = now;
+ state.createdAfter = past;
},
[types.REQUEST_VALUE_STREAMS](state) {
state.valueStreams = [];
@@ -46,17 +61,25 @@ export default {
[types.REQUEST_CYCLE_ANALYTICS_DATA](state) {
state.isLoading = true;
state.hasError = false;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_SUCCESS](state, data) {
const { summary, medians } = decorateData(data);
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = formatMedianValues(medians);
+ }
state.permissions = data.permissions;
state.summary = summary;
- state.medians = formatMedianValues(medians);
state.hasError = false;
},
[types.RECEIVE_CYCLE_ANALYTICS_DATA_ERROR](state) {
state.isLoading = false;
state.hasError = true;
+ if (!state.features.cycleAnalyticsForGroups) {
+ state.medians = {};
+ }
},
[types.REQUEST_STAGE_DATA](state) {
state.isLoadingStage = true;
@@ -78,4 +101,13 @@ export default {
state.hasError = true;
state.selectedStageError = error;
},
+ [types.REQUEST_STAGE_MEDIANS](state) {
+ state.medians = {};
+ },
+ [types.RECEIVE_STAGE_MEDIANS_SUCCESS](state, medians) {
+ state.medians = formatMedianValues(medians);
+ },
+ [types.RECEIVE_STAGE_MEDIANS_ERROR](state) {
+ state.medians = {};
+ },
};
diff --git a/app/assets/javascripts/cycle_analytics/store/state.js b/app/assets/javascripts/cycle_analytics/store/state.js
index 02f953d9517..4d61077fb99 100644
--- a/app/assets/javascripts/cycle_analytics/store/state.js
+++ b/app/assets/javascripts/cycle_analytics/store/state.js
@@ -1,9 +1,13 @@
import { DEFAULT_DAYS_TO_DISPLAY } from '../constants';
export default () => ({
+ features: {},
+ id: null,
requestPath: '',
fullPath: '',
startDate: DEFAULT_DAYS_TO_DISPLAY,
+ createdAfter: null,
+ createdBefore: null,
stages: [],
summary: [],
analytics: [],
@@ -19,4 +23,5 @@ export default () => ({
isLoadingStage: false,
isEmptyStage: false,
permissions: {},
+ parentPath: null,
});
diff --git a/app/assets/javascripts/cycle_analytics/utils.js b/app/assets/javascripts/cycle_analytics/utils.js
index 40ad7d8b2fc..a1690dd1513 100644
--- a/app/assets/javascripts/cycle_analytics/utils.js
+++ b/app/assets/javascripts/cycle_analytics/utils.js
@@ -1,6 +1,9 @@
+import dateFormat from 'dateformat';
import { unescape } from 'lodash';
+import { dateFormats } from '~/analytics/shared/constants';
import { sanitize } from '~/lib/dompurify';
import { roundToNearestHalf, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { getDateInPast } from '~/lib/utils/datetime/date_calculation_utility';
import { parseSeconds } from '~/lib/utils/datetime_utility';
import { s__, sprintf } from '../locale';
import DEFAULT_EVENT_OBJECTS from './default_event_objects';
@@ -115,3 +118,20 @@ export const formatMedianValues = (medians = []) =>
export const filterStagesByHiddenStatus = (stages = [], isHidden = true) =>
stages.filter(({ hidden = false }) => hidden === isHidden);
+
+const toIsoFormat = (d) => dateFormat(d, dateFormats.isoDate);
+
+/**
+ * Takes an integer specifying the number of days to subtract
+ * from the date specified will return the 2 dates, formatted as ISO dates
+ *
+ * @param {Number} daysInPast - Number of days in the past to subtract
+ * @param {Date} [today=new Date] - Date to subtract days from, defaults to today
+ * @returns {Object} Returns 'now' and the 'past' date formatted as ISO dates
+ */
+export const calculateFormattedDayInPast = (daysInPast, today = new Date()) => {
+ return {
+ now: toIsoFormat(today),
+ past: toIsoFormat(getDateInPast(today, daysInPast)),
+ };
+};
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index 02c57164f47..36d54f586f1 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '../eventhub';
@@ -93,14 +93,20 @@ export default {
.catch(() => {
this.isLoading = false;
this.store.keys = {};
- return new Flash(s__('DeployKeys|Error getting deploy keys'));
+ return createFlash({
+ message: s__('DeployKeys|Error getting deploy keys'),
+ });
});
},
enableKey(deployKey) {
this.service
.enableKey(deployKey.id)
.then(this.fetchKeys)
- .catch(() => new Flash(s__('DeployKeys|Error enabling deploy key')));
+ .catch(() =>
+ createFlash({
+ message: s__('DeployKeys|Error enabling deploy key'),
+ }),
+ );
},
confirmRemoveKey(deployKey, callback) {
const hideModal = () => {
@@ -112,7 +118,11 @@ export default {
.disableKey(deployKey.id)
.then(this.fetchKeys)
.then(hideModal)
- .catch(() => new Flash(s__('DeployKeys|Error removing deploy key')));
+ .catch(() =>
+ createFlash({
+ message: s__('DeployKeys|Error removing deploy key'),
+ }),
+ );
};
this.cancel = hideModal;
this.confirmModalVisible = true;
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 b1c37b0687f..78ba586ce37 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
@@ -221,7 +221,7 @@ export default {
@click.stop="toggleResolvedStatus"
>
<gl-icon v-if="!isResolving" :name="resolveIconName" data-testid="resolve-icon" />
- <gl-loading-icon v-else inline />
+ <gl-loading-icon v-else size="sm" inline />
</button>
</template>
<template v-if="discussion.resolved" #resolved-status>
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 833d7081a2c..1e1f5135290 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,6 +1,7 @@
<script>
import { GlTooltipDirective, GlIcon, GlLink, GlSafeHtmlDirective } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -48,6 +49,9 @@ export default {
author() {
return this.note.author;
},
+ authorId() {
+ return getIdFromGraphQLId(this.author.id);
+ },
noteAnchorId() {
return findNoteId(this.note.id);
},
@@ -94,7 +98,7 @@ export default {
v-once
:href="author.webUrl"
class="js-user-link"
- :data-user-id="author.id"
+ :data-user-id="authorId"
:data-username="author.username"
>
<span class="note-header-author-name gl-font-weight-bold">{{ author.name }}</span>
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 da492f03801..013dd1d89f3 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/todo_button.vue';
+import TodoButton from '~/vue_shared/components/sidebar/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';
@@ -60,22 +60,6 @@ export default {
},
},
methods: {
- updateGlobalTodoCount(additionalTodoCount) {
- const currentCount = parseInt(document.querySelector('.js-todos-count').innerText, 10);
- const todoToggleEvent = new CustomEvent('todo:toggle', {
- detail: {
- count: Math.max(currentCount + additionalTodoCount, 0),
- },
- });
-
- document.dispatchEvent(todoToggleEvent);
- },
- incrementGlobalTodoCount() {
- this.updateGlobalTodoCount(1);
- },
- decrementGlobalTodoCount() {
- this.updateGlobalTodoCount(-1);
- },
createTodo() {
this.todoLoading = true;
return this.$apollo
@@ -92,9 +76,6 @@ export default {
}
},
})
- .then(() => {
- this.incrementGlobalTodoCount();
- })
.catch((err) => {
this.$emit('error', Error(CREATE_DESIGN_TODO_ERROR));
throw err;
@@ -130,9 +111,6 @@ export default {
}
},
})
- .then(() => {
- this.decrementGlobalTodoCount();
- })
.catch((err) => {
this.$emit('error', Error(DELETE_DESIGN_TODO_ERROR));
throw err;
diff --git a/app/assets/javascripts/design_management/pages/design/index.vue b/app/assets/javascripts/design_management/pages/design/index.vue
index ad78433c7ce..19bfa123487 100644
--- a/app/assets/javascripts/design_management/pages/design/index.vue
+++ b/app/assets/javascripts/design_management/pages/design/index.vue
@@ -41,7 +41,7 @@ import {
TOGGLE_TODO_ERROR,
designDeletionError,
} from '../../utils/error_messages';
-import { trackDesignDetailView, usagePingDesignDetailView } from '../../utils/tracking';
+import { trackDesignDetailView, servicePingDesignDetailView } from '../../utils/tracking';
const DEFAULT_SCALE = 1;
@@ -292,7 +292,7 @@ export default {
);
if (this.glFeatures.usageDataDesignAction) {
- usagePingDesignDetailView();
+ servicePingDesignDetailView();
}
},
updateActiveDiscussion(id, source = ACTIVE_DISCUSSION_SOURCE_TYPES.discussion) {
diff --git a/app/assets/javascripts/design_management/utils/tracking.js b/app/assets/javascripts/design_management/utils/tracking.js
index 905134fa985..23aec46c152 100644
--- a/app/assets/javascripts/design_management/utils/tracking.js
+++ b/app/assets/javascripts/design_management/utils/tracking.js
@@ -14,7 +14,7 @@ export const DESIGN_SNOWPLOW_EVENT_TYPES = {
UPDATE_DESIGN: 'update_design',
};
-export const DESIGN_USAGE_PING_EVENT_TYPES = {
+export const DESIGN_SERVICE_PING_EVENT_TYPES = {
DESIGN_ACTION: 'design_action',
};
@@ -52,8 +52,8 @@ export function trackDesignUpdate() {
}
/**
- * Track "design detail" view via usage ping
+ * Track "design detail" view via service ping
*/
-export function usagePingDesignDetailView() {
- Api.trackRedisHllUserEvent(DESIGN_USAGE_PING_EVENT_TYPES.DESIGN_ACTION);
+export function servicePingDesignDetailView() {
+ Api.trackRedisHllUserEvent(DESIGN_SERVICE_PING_EVENT_TYPES.DESIGN_ACTION);
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 7200e6c2e3a..14d6e2db09d 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import FilesCommentButton from './files_comment_button';
@@ -77,7 +77,11 @@ export default class Diff {
axios
.get(link, { params })
.then(({ data }) => $target.parent().replaceWith(data))
- .catch(() => flash(__('An error occurred while loading diff')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while loading diff'),
+ }),
+ );
}
openAnchoredDiff(cb) {
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 61946d345e3..e33b60f8ab5 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -14,7 +14,7 @@ import {
} from '~/behaviors/shortcuts/keybindings';
import createFlash from '~/flash';
import { isSingleViewStyle } from '~/helpers/diffs_helper';
-import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import PanelResizer from '~/vue_shared/components/panel_resizer.vue';
@@ -42,6 +42,7 @@ import {
TRACKING_MULTIPLE_FILES_MODE,
} from '../constants';
+import diffsEventHub from '../event_hub';
import { reviewStatuses } from '../utils/file_reviews';
import { diffsApp } from '../utils/performance';
import { fileByFile } from '../utils/preferences';
@@ -52,7 +53,9 @@ import DiffFile from './diff_file.vue';
import HiddenFilesWarning from './hidden_files_warning.vue';
import MergeConflictWarning from './merge_conflict_warning.vue';
import NoChanges from './no_changes.vue';
+import PreRenderer from './pre_renderer.vue';
import TreeList from './tree_list.vue';
+import VirtualScrollerScrollSync from './virtual_scroller_scroll_sync';
export default {
name: 'DiffsApp',
@@ -71,6 +74,8 @@ export default {
GlSprintf,
DynamicScroller,
DynamicScrollerItem,
+ PreRenderer,
+ VirtualScrollerScrollSync,
},
alerts: {
ALERT_OVERFLOW_HIDDEN,
@@ -166,6 +171,8 @@ export default {
return {
treeWidth,
diffFilesLength: 0,
+ virtualScrollCurrentIndex: -1,
+ disableVirtualScroller: false,
};
},
computed: {
@@ -186,6 +193,7 @@ export default {
'showTreeList',
'isLoading',
'startVersion',
+ 'latestDiff',
'currentDiffFileId',
'isTreeLoaded',
'conflictResolutionPath',
@@ -228,8 +236,8 @@ export default {
isLimitedContainer() {
return !this.renderFileTree && !this.isParallelView && !this.isFluidLayout;
},
- isDiffHead() {
- return parseBoolean(getParameterByName('diff_head'));
+ isFullChangeset() {
+ return this.startVersion === null && this.latestDiff;
},
showFileByFileNavigation() {
return this.diffFiles.length > 1 && this.viewDiffsFileByFile;
@@ -252,7 +260,7 @@ export default {
if (this.renderOverflowWarning) {
visible = this.$options.alerts.ALERT_OVERFLOW_HIDDEN;
- } else if (this.isDiffHead && this.hasConflicts) {
+ } else if (this.isFullChangeset && this.hasConflicts) {
visible = this.$options.alerts.ALERT_MERGE_CONFLICT;
} else if (this.whichCollapsedTypes.automatic && !this.viewDiffsFileByFile) {
visible = this.$options.alerts.ALERT_COLLAPSED_FILES;
@@ -323,6 +331,11 @@ export default {
this.setHighlightedRow(id.split('diff-content').pop().slice(1));
}
+ if (window.gon?.features?.diffsVirtualScrolling) {
+ diffsEventHub.$on('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
+ diffsEventHub.$on('scrollToIndex', this.scrollVirtualScrollerToIndex);
+ }
+
if (window.gon?.features?.diffSettingsUsageData) {
if (this.renderTreeList) {
api.trackRedisHllUserEvent(TRACKING_FILE_BROWSER_TREE);
@@ -377,6 +390,11 @@ export default {
diffsApp.deinstrument();
this.unsubscribeFromEvents();
this.removeEventListeners();
+
+ if (window.gon?.features?.diffsVirtualScrolling) {
+ diffsEventHub.$off('scrollToFileHash', this.scrollVirtualScrollerToFileHash);
+ diffsEventHub.$off('scrollToIndex', this.scrollVirtualScrollerToIndex);
+ }
},
methods: {
...mapActions(['startTaskList']),
@@ -458,7 +476,11 @@ export default {
},
setDiscussions() {
requestIdleCallback(
- () => this.assignDiscussionsToDiff().then(this.$nextTick).then(this.startTaskList),
+ () =>
+ this.assignDiscussionsToDiff()
+ .then(this.$nextTick)
+ .then(this.startTaskList)
+ .then(this.scrollVirtualScrollerToDiffNote),
{ timeout: 1000 },
);
},
@@ -483,12 +505,17 @@ export default {
this.moveToNeighboringCommit({ direction: 'previous' }),
);
}
+
+ Mousetrap.bind(['ctrl+f', 'command+f'], () => {
+ this.disableVirtualScroller = true;
+ });
},
removeEventListeners() {
Mousetrap.unbind(keysFor(MR_PREVIOUS_FILE_IN_DIFF));
Mousetrap.unbind(keysFor(MR_NEXT_FILE_IN_DIFF));
Mousetrap.unbind(keysFor(MR_COMMITS_NEXT_COMMIT));
Mousetrap.unbind(keysFor(MR_COMMITS_PREVIOUS_COMMIT));
+ Mousetrap.unbind(['ctrl+f', 'command+f']);
},
jumpToFile(step) {
const targetIndex = this.currentDiffIndex + step;
@@ -508,6 +535,36 @@ export default {
return this.setShowTreeList({ showTreeList, saving: false });
},
+ async scrollVirtualScrollerToFileHash(hash) {
+ const index = this.diffFiles.findIndex((f) => f.file_hash === hash);
+
+ if (index !== -1) {
+ this.scrollVirtualScrollerToIndex(index);
+ }
+ },
+ async scrollVirtualScrollerToIndex(index) {
+ this.virtualScrollCurrentIndex = index;
+
+ await this.$nextTick();
+
+ this.virtualScrollCurrentIndex = -1;
+ },
+ scrollVirtualScrollerToDiffNote() {
+ if (!window.gon?.features?.diffsVirtualScrolling) return;
+
+ const id = window?.location?.hash;
+
+ if (id.startsWith('#note_')) {
+ const noteId = id.replace('#note_', '');
+ const discussion = this.$store.state.notes.discussions.find(
+ (d) => d.diff_file && d.notes.find((n) => n.id === noteId),
+ );
+
+ if (discussion) {
+ this.scrollVirtualScrollerToFileHash(discussion.diff_file.file_hash);
+ }
+ }
+ },
},
minTreeWidth: MIN_TREE_WIDTH,
maxTreeWidth: MAX_TREE_WIDTH,
@@ -571,7 +628,8 @@ export default {
<div v-if="isBatchLoading" class="loading"><gl-loading-icon size="lg" /></div>
<template v-else-if="renderDiffFiles">
<dynamic-scroller
- v-if="isVirtualScrollingEnabled"
+ v-if="!disableVirtualScroller && isVirtualScrollingEnabled"
+ ref="virtualScroller"
:items="diffs"
:min-item-size="70"
:buffer="1000"
@@ -579,7 +637,7 @@ export default {
page-mode
>
<template #default="{ item, index, active }">
- <dynamic-scroller-item :item="item" :active="active">
+ <dynamic-scroller-item :item="item" :active="active" :class="{ active }">
<diff-file
:file="item"
:reviewed="fileReviews[item.id]"
@@ -588,9 +646,29 @@ export default {
:help-page-path="helpPagePath"
:can-current-user-fork="canCurrentUserFork"
:view-diffs-file-by-file="viewDiffsFileByFile"
+ :active="active"
/>
</dynamic-scroller-item>
</template>
+ <template #before>
+ <pre-renderer :max-length="diffFilesLength">
+ <template #default="{ item, index, active }">
+ <dynamic-scroller-item :item="item" :active="active">
+ <diff-file
+ :file="item"
+ :reviewed="fileReviews[item.id]"
+ :is-first-file="index === 0"
+ :is-last-file="index === diffFilesLength - 1"
+ :help-page-path="helpPagePath"
+ :can-current-user-fork="canCurrentUserFork"
+ :view-diffs-file-by-file="viewDiffsFileByFile"
+ pre-render
+ />
+ </dynamic-scroller-item>
+ </template>
+ </pre-renderer>
+ <virtual-scroller-scroll-sync :index="virtualScrollCurrentIndex" />
+ </template>
</dynamic-scroller>
<template v-else>
<diff-file
diff --git a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
index 0cf1cdb17f8..240f102e600 100644
--- a/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
+++ b/app/assets/javascripts/diffs/components/collapsed_files_warning.vue
@@ -1,5 +1,6 @@
<script>
import { GlAlert, GlButton } from '@gitlab/ui';
+import { mapState } from 'vuex';
import { CENTERED_LIMITED_CONTAINER_CLASSES, EVT_EXPAND_ALL_FILES } from '../constants';
import eventHub from '../event_hub';
@@ -27,11 +28,15 @@ export default {
};
},
computed: {
+ ...mapState('diffs', ['diffFiles']),
containerClasses() {
return {
[CENTERED_LIMITED_CONTAINER_CLASSES]: this.limited,
};
},
+ shouldDisplay() {
+ return !this.isDismissed && this.diffFiles.length > 1;
+ },
},
methods: {
@@ -48,7 +53,7 @@ export default {
</script>
<template>
- <div v-if="!isDismissed" data-testid="root" :class="containerClasses" class="col-12">
+ <div v-if="shouldDisplay" data-testid="root" :class="containerClasses" class="col-12">
<gl-alert
:dismissible="true"
:title="__('Some changes are not shown')"
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index e2a1f7236c5..f098d20afd1 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -99,7 +99,7 @@ export default {
v-gl-tooltip.hover
variant="default"
icon="file-tree"
- class="gl-mr-3 js-toggle-tree-list"
+ class="gl-mr-3 js-toggle-tree-list btn-icon"
:title="toggleFileBrowserTitle"
:aria-label="toggleFileBrowserTitle"
:selected="showTreeList"
@@ -109,7 +109,7 @@ export default {
{{ __('Viewing commit') }}
<gl-link :href="commit.commit_url" class="monospace">{{ commit.short_id }}</gl-link>
</div>
- <div v-if="hasNeighborCommits" class="commit-nav-buttons ml-3">
+ <div v-if="hasNeighborCommits" class="commit-nav-buttons">
<gl-button-group>
<gl-button
:href="previousCommitUrl"
@@ -160,6 +160,15 @@ export default {
/>
</template>
</gl-sprintf>
+ <gl-button
+ v-if="commit || startVersion"
+ :href="latestVersionPath"
+ variant="default"
+ class="js-latest-version"
+ :class="{ 'gl-ml-3': commit && !hasNeighborCommits }"
+ >
+ {{ __('Show latest version') }}
+ </gl-button>
<div v-if="hasChanges" class="inline-parallel-buttons d-none d-md-flex ml-auto">
<diff-stats
:diff-files-count-text="diffFilesCountText"
@@ -167,14 +176,6 @@ export default {
:removed-lines="removedLines"
/>
<gl-button
- v-if="commit || startVersion"
- :href="latestVersionPath"
- variant="default"
- class="gl-mr-3 js-latest-version"
- >
- {{ __('Show latest version') }}
- </gl-button>
- <gl-button
v-show="whichCollapsedTypes.any"
variant="default"
class="gl-mr-3"
diff --git a/app/assets/javascripts/diffs/components/diff_content.vue b/app/assets/javascripts/diffs/components/diff_content.vue
index cb74c7dc7cd..858d9e221ae 100644
--- a/app/assets/javascripts/diffs/components/diff_content.vue
+++ b/app/assets/javascripts/diffs/components/diff_content.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
-import { mapInline, mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils';
+import { mapParallel } from 'ee_else_ce/diffs/components/diff_row_utils';
import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { diffViewerModes } from '~/ide/constants';
@@ -9,7 +9,6 @@ import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import NoteForm from '../../notes/components/note_form.vue';
import eventHub from '../../notes/event_hub';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -18,14 +17,10 @@ import { getDiffMode } from '../store/utils';
import DiffDiscussions from './diff_discussions.vue';
import DiffView from './diff_view.vue';
import ImageDiffOverlay from './image_diff_overlay.vue';
-import InlineDiffView from './inline_diff_view.vue';
-import ParallelDiffView from './parallel_diff_view.vue';
export default {
components: {
GlLoadingIcon,
- InlineDiffView,
- ParallelDiffView,
DiffView,
DiffViewer,
NoteForm,
@@ -36,7 +31,7 @@ export default {
userAvatarLink,
DiffFileDrafts,
},
- mixins: [diffLineNoteFormMixin, draftCommentsMixin, glFeatureFlagsMixin()],
+ mixins: [diffLineNoteFormMixin, draftCommentsMixin],
props: {
diffFile: {
type: Object,
@@ -52,7 +47,6 @@ export default {
...mapState('diffs', ['projectPath']),
...mapGetters('diffs', [
'isInlineView',
- 'isParallelView',
'getCommentFormForDiffFile',
'diffLines',
'fileLineCodequality',
@@ -86,15 +80,8 @@ export default {
return this.getUserData;
},
mappedLines() {
- if (this.glFeatures.unifiedDiffComponents) {
- return this.diffLines(this.diffFile, true).map(mapParallel(this)) || [];
- }
-
- // TODO: Everything below this line can be deleted when unifiedDiffComponents FF is removed
- if (this.isInlineView) {
- return this.diffFile.highlighted_diff_lines.map(mapInline(this));
- }
- return this.diffLines(this.diffFile).map(mapParallel(this));
+ // TODO: Do this data generation when we recieve a response to save a computed property being created
+ return this.diffLines(this.diffFile).map(mapParallel(this)) || [];
},
},
updated() {
@@ -126,7 +113,7 @@ export default {
<template>
<div class="diff-content">
<div class="diff-viewer">
- <template v-if="isTextFile && glFeatures.unifiedDiffComponents">
+ <template v-if="isTextFile">
<diff-view
:diff-file="diffFile"
:diff-lines="mappedLines"
@@ -135,21 +122,6 @@ export default {
/>
<gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
</template>
- <template v-else-if="isTextFile">
- <inline-diff-view
- v-if="isInlineView"
- :diff-file="diffFile"
- :diff-lines="mappedLines"
- :help-page-path="helpPagePath"
- />
- <parallel-diff-view
- v-else-if="isParallelView"
- :diff-file="diffFile"
- :diff-lines="mappedLines"
- :help-page-path="helpPagePath"
- />
- <gl-loading-icon v-if="diffFile.renderingLines" size="md" class="mt-3" />
- </template>
<not-diffable-viewer v-else-if="notDiffable" />
<no-preview-viewer v-else-if="noPreview" />
<diff-viewer
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index ed8455f0c1c..dde5ea81e9a 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -2,6 +2,7 @@
import { GlButton, GlLoadingIcon, GlSafeHtmlDirective as SafeHtml, GlSprintf } from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import { IdState } from 'vendor/vue-virtual-scroller';
import createFlash from '~/flash';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
@@ -19,7 +20,7 @@ import {
} from '../constants';
import eventHub from '../event_hub';
import { DIFF_FILE, GENERIC_ERROR } from '../i18n';
-import { collapsedType, isCollapsed, getShortShaFromFile } from '../utils/diff_file';
+import { collapsedType, getShortShaFromFile } from '../utils/diff_file';
import DiffContent from './diff_content.vue';
import DiffFileHeader from './diff_file_header.vue';
@@ -34,7 +35,7 @@ export default {
directives: {
SafeHtml,
},
- mixins: [glFeatureFlagsMixin()],
+ mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.file.file_hash })],
props: {
file: {
type: Object,
@@ -68,12 +69,22 @@ export default {
type: Boolean,
required: true,
},
+ active: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ preRender: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
- data() {
+ idState() {
return {
isLoadingCollapsedDiff: false,
forkMessageVisible: false,
- isCollapsed: isCollapsed(this.file),
+ hasToggled: false,
};
},
i18n: {
@@ -91,7 +102,7 @@ export default {
return getShortShaFromFile(this.file);
},
showLoadingIcon() {
- return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
+ return this.idState.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
},
hasDiff() {
return hasDiff(this.file);
@@ -152,45 +163,39 @@ export default {
codequalityDiffForFile() {
return this.codequalityDiff?.files?.[this.file.file_path] || [];
},
+ isCollapsed() {
+ if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
+ return this.viewDiffsFileByFile ? false : this.file.viewer?.automaticallyCollapsed;
+ }
+
+ return this.file.viewer?.manuallyCollapsed;
+ },
},
watch: {
'file.id': {
handler: function fileIdHandler() {
+ if (this.preRender) return;
+
this.manageViewedEffects();
},
},
'file.file_hash': {
handler: function hashChangeWatch(newHash, oldHash) {
- this.isCollapsed = isCollapsed(this.file);
-
- if (newHash && oldHash && !this.hasDiff) {
+ if (newHash && oldHash && !this.hasDiff && !this.preRender) {
this.requestDiff();
}
},
- immediate: true,
- },
- 'file.viewer.automaticallyCollapsed': {
- handler: function autoChangeWatch(automaticValue) {
- if (collapsedType(this.file) !== DIFF_FILE_MANUAL_COLLAPSE) {
- this.isCollapsed = this.viewDiffsFileByFile ? false : automaticValue;
- }
- },
- immediate: true,
- },
- 'file.viewer.manuallyCollapsed': {
- handler: function manualChangeWatch(manualValue) {
- if (manualValue !== null) {
- this.isCollapsed = manualValue;
- }
- },
- immediate: true,
},
},
created() {
+ if (this.preRender) return;
+
notesEventHub.$on(`loadCollapsedDiff/${this.file.file_hash}`, this.requestDiff);
eventHub.$on(EVT_EXPAND_ALL_FILES, this.expandAllListener);
},
mounted() {
+ if (this.preRender) return;
+
if (this.hasDiff) {
this.postRender();
}
@@ -198,6 +203,8 @@ export default {
this.manageViewedEffects();
},
beforeDestroy() {
+ if (this.preRender) return;
+
eventHub.$off(EVT_EXPAND_ALL_FILES, this.expandAllListener);
},
methods: {
@@ -208,8 +215,14 @@ export default {
'setFileCollapsedByUser',
]),
manageViewedEffects() {
- if (this.reviewed && !this.isCollapsed && this.showLocalFileReviews) {
+ if (
+ !this.idState.hasToggled &&
+ this.reviewed &&
+ !this.isCollapsed &&
+ this.showLocalFileReviews
+ ) {
this.handleToggle();
+ this.idState.hasToggled = true;
}
},
expandAllListener() {
@@ -252,11 +265,11 @@ export default {
}
},
requestDiff() {
- this.isLoadingCollapsedDiff = true;
+ this.idState.isLoadingCollapsedDiff = true;
this.loadCollapsedDiff(this.file)
.then(() => {
- this.isLoadingCollapsedDiff = false;
+ this.idState.isLoadingCollapsedDiff = false;
this.setRenderIt(this.file);
})
.then(() => {
@@ -269,17 +282,17 @@ export default {
);
})
.catch(() => {
- this.isLoadingCollapsedDiff = false;
+ this.idState.isLoadingCollapsedDiff = false;
createFlash({
message: this.$options.i18n.genericError,
});
});
},
showForkMessage() {
- this.forkMessageVisible = true;
+ this.idState.forkMessageVisible = true;
},
hideForkMessage() {
- this.forkMessageVisible = false;
+ this.idState.forkMessageVisible = false;
},
},
};
@@ -287,7 +300,7 @@ export default {
<template>
<div
- :id="file.file_hash"
+ :id="!preRender && active && file.file_hash"
:class="{
'is-active': currentDiffFileId === file.file_hash,
'comments-disabled': Boolean(file.brokenSymlink),
@@ -313,7 +326,10 @@ export default {
@showForkMessage="showForkMessage"
/>
- <div v-if="forkMessageVisible" class="js-file-fork-suggestion-section file-fork-suggestion">
+ <div
+ v-if="idState.forkMessageVisible"
+ class="js-file-fork-suggestion-section file-fork-suggestion"
+ >
<span v-safe-html="forkMessage" class="file-fork-suggestion-note"></span>
<a
:href="file.fork_path"
@@ -330,12 +346,13 @@ export default {
</div>
<template v-else>
<div
- :id="`diff-content-${file.file_hash}`"
+ :id="!preRender && active && `diff-content-${file.file_hash}`"
:class="hasBodyClasses.contentByHash"
data-testid="content-area"
>
<gl-loading-icon
v-if="showLoadingIcon"
+ size="sm"
class="diff-content loading gl-my-0 gl-pt-3"
data-testid="loader-icon"
/>
@@ -357,7 +374,7 @@ export default {
</div>
<template v-else>
<div
- v-show="showWarning"
+ v-if="showWarning"
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
>
<p class="gl-mb-5">
@@ -373,7 +390,7 @@ export default {
</gl-button>
</div>
<diff-content
- v-show="showContent"
+ v-if="showContent"
:class="hasBodyClasses.content"
:diff-file="file"
:help-page-path="helpPagePath"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 45c7fe35f03..667b8745f7b 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -13,6 +13,7 @@ import {
} from '@gitlab/ui';
import { escape } from 'lodash';
import { mapActions, mapGetters, mapState } from 'vuex';
+import { IdState } from 'vendor/vue-virtual-scroller';
import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -41,13 +42,12 @@ export default {
GlDropdownDivider,
GlFormCheckbox,
GlLoadingIcon,
- CodeQualityBadge: () => import('ee_component/diffs/components/code_quality_badge.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
SafeHtml: GlSafeHtmlDirective,
},
- mixins: [glFeatureFlagsMixin()],
+ mixins: [glFeatureFlagsMixin(), IdState({ idProp: (vm) => vm.diffFile.file_hash })],
i18n: {
...DIFF_FILE_HEADER,
compareButtonLabel: s__('Compare submodule commit revisions'),
@@ -102,7 +102,7 @@ export default {
default: () => [],
},
},
- data() {
+ idState() {
return {
moreActionsShown: false,
};
@@ -202,8 +202,18 @@ export default {
externalUrlLabel() {
return sprintf(__('View on %{url}'), { url: this.diffFile.formatted_external_url });
},
- showCodequalityBadge() {
- return this.codequalityDiff?.length > 0 && !this.glFeatures.codequalityMrDiffAnnotations;
+ },
+ watch: {
+ 'idState.moreActionsShown': {
+ handler(val) {
+ const el = this.$el.closest('.vue-recycle-scroller__item-view');
+
+ if (this.glFeatures.diffsVirtualScrolling && el) {
+ // We can't add a style with Vue because of the way the virtual
+ // scroller library renders the diff files
+ el.style.zIndex = val ? '1' : null;
+ }
+ },
},
},
methods: {
@@ -239,7 +249,7 @@ export default {
}
},
setMoreActionsShown(val) {
- this.moreActionsShown = val;
+ this.idState.moreActionsShown = val;
},
toggleReview(newReviewedStatus) {
const autoCollapsed =
@@ -268,7 +278,7 @@ export default {
<template>
<div
ref="header"
- :class="{ 'gl-z-dropdown-menu!': moreActionsShown }"
+ :class="{ 'gl-z-dropdown-menu!': idState.moreActionsShown }"
class="js-file-title file-title file-title-flex-parent"
data-qa-selector="file_title_container"
:data-qa-file-name="filePath"
@@ -292,7 +302,7 @@ export default {
>
<file-icon
:file-name="filePath"
- :size="18"
+ :size="16"
aria-hidden="true"
css-classes="gl-mr-2"
:submodule="diffFile.submodule"
@@ -336,13 +346,6 @@ export default {
data-track-property="diff_copy_file"
/>
- <code-quality-badge
- v-if="showCodequalityBadge"
- :file-name="filePath"
- :codequality-diff="codequalityDiff"
- class="gl-mr-2"
- />
-
<small v-if="isModeChanged" ref="fileMode" class="mr-1">
{{ diffFile.a_mode }} → {{ diffFile.b_mode }}
</small>
@@ -453,7 +456,7 @@ export default {
:disabled="diffFile.isLoadingFullFile"
@click="toggleFullDiff(diffFile.file_path)"
>
- <gl-loading-icon v-if="diffFile.isLoadingFullFile" inline />
+ <gl-loading-icon v-if="diffFile.isLoadingFullFile" size="sm" inline />
{{ expandDiffToFullFileTitle }}
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/diffs/components/diff_line_note_form.vue b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
index c907b5dffaf..c445989f143 100644
--- a/app/assets/javascripts/diffs/components/diff_line_note_form.vue
+++ b/app/assets/javascripts/diffs/components/diff_line_note_form.vue
@@ -106,10 +106,7 @@ export default {
};
const getDiffLines = () => {
if (this.diffViewType === PARALLEL_DIFF_VIEW_TYPE) {
- return this.diffLines(this.diffFile, this.glFeatures.unifiedDiffComponents).reduce(
- combineSides,
- [],
- );
+ return this.diffLines(this.diffFile).reduce(combineSides, []);
}
return this.diffFile[INLINE_DIFF_LINES_KEY];
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 37dd7941b2e..c310bd9f31a 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -1,13 +1,9 @@
<script>
/* eslint-disable vue/no-v-html */
-import { GlTooltipDirective } from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { memoize } from 'lodash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import {
- CONTEXT_LINE_CLASS_NAME,
PARALLEL_DIFF_VIEW_TYPE,
- CONFLICT_MARKER_OUR,
CONFLICT_MARKER_THEIR,
CONFLICT_OUR,
CONFLICT_THEIR,
@@ -22,15 +18,8 @@ import DiffGutterAvatars from './diff_gutter_avatars.vue';
import * as utils from './diff_row_utils';
export default {
- components: {
- DiffGutterAvatars,
- CodeQualityGutterIcon: () =>
- import('ee_component/diffs/components/code_quality_gutter_icon.vue'),
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [glFeatureFlagsMixin()],
+ DiffGutterAvatars,
+ CodeQualityGutterIcon: () => import('ee_component/diffs/components/code_quality_gutter_icon.vue'),
props: {
fileHash: {
type: String,
@@ -58,148 +47,109 @@ export default {
type: Number,
required: true,
},
+ isHighlighted: {
+ type: Boolean,
+ required: true,
+ },
+ fileLineCoverage: {
+ type: Function,
+ required: true,
+ },
},
- data() {
- return {
- dragging: false,
- };
- },
- computed: {
- ...mapGetters('diffs', ['fileLineCoverage']),
- ...mapGetters(['isLoggedIn']),
- ...mapState({
- isHighlighted(state) {
- const line = this.line.left?.line_code ? this.line.left : this.line.right;
- return utils.isHighlighted(state, line, false);
- },
- }),
- classNameMap() {
+ classNameMap: memoize(
+ (props) => {
return {
- [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
- [PARALLEL_DIFF_VIEW_TYPE]: !this.inline,
- commented: this.isCommented,
+ [PARALLEL_DIFF_VIEW_TYPE]: !props.inline,
+ commented: props.isCommented,
};
},
- parallelViewLeftLineType() {
- return utils.parallelViewLeftLineType(this.line, this.isHighlighted || this.isCommented);
+ (props) => [!props.inline, props.isCommented].join(':'),
+ ),
+ parallelViewLeftLineType: memoize(
+ (props) => {
+ return utils.parallelViewLeftLineType(props.line, props.isHighlighted || props.isCommented);
},
- coverageStateLeft() {
- if (!this.inline || !this.line.left) return {};
- return this.fileLineCoverage(this.filePath, this.line.left.new_line);
+ (props) =>
+ [props.line.left?.type, props.line.right?.type, props.isHighlighted, props.isCommented].join(
+ ':',
+ ),
+ ),
+ coverageStateLeft: memoize(
+ (props) => {
+ if (!props.inline || !props.line.left) return {};
+ return props.fileLineCoverage(props.filePath, props.line.left.new_line);
},
- coverageStateRight() {
- if (!this.line.right) return {};
- return this.fileLineCoverage(this.filePath, this.line.right.new_line);
+ (props) => [props.inline, props.filePath, props.line.left?.new_line].join(':'),
+ ),
+ coverageStateRight: memoize(
+ (props) => {
+ if (!props.line.right) return {};
+ return props.fileLineCoverage(props.filePath, props.line.right.new_line);
},
- showCodequalityLeft() {
- return (
- this.glFeatures.codequalityMrDiffAnnotations &&
- this.inline &&
- this.line.left?.codequality?.length > 0
- );
+ (props) => [props.line.right?.new_line, props.filePath].join(':'),
+ ),
+ showCodequalityLeft: memoize(
+ (props) => {
+ return props.inline && props.line.left?.codequality?.length > 0;
},
- showCodequalityRight() {
- return (
- this.glFeatures.codequalityMrDiffAnnotations &&
- !this.inline &&
- this.line.right?.codequality?.length > 0
- );
+ (props) => [props.inline, props.line.left?.codequality?.length].join(':'),
+ ),
+ showCodequalityRight: memoize(
+ (props) => {
+ return !props.inline && props.line.right?.codequality?.length > 0;
},
- classNameMapCellLeft() {
+ (props) => [props.inline, props.line.right?.codequality?.length].join(':'),
+ ),
+ classNameMapCellLeft: memoize(
+ (props) => {
return utils.classNameMapCell({
- line: this.line.left,
- hll: this.isHighlighted || this.isCommented,
- isLoggedIn: this.isLoggedIn,
+ line: props.line.left,
+ hll: props.isHighlighted || props.isCommented,
});
},
- classNameMapCellRight() {
+ (props) => [props.line.left.type, props.isHighlighted, props.isCommented].join(':'),
+ ),
+ classNameMapCellRight: memoize(
+ (props) => {
return utils.classNameMapCell({
- line: this.line.right,
- hll: this.isHighlighted || this.isCommented,
- isLoggedIn: this.isLoggedIn,
+ line: props.line.right,
+ hll: props.isHighlighted || props.isCommented,
});
},
- addCommentTooltipLeft() {
- return utils.addCommentTooltip(this.line.left, this.glFeatures.dragCommentSelection);
- },
- addCommentTooltipRight() {
- return utils.addCommentTooltip(this.line.right, this.glFeatures.dragCommentSelection);
+ (props) => [props.line.right.type, props.isHighlighted, props.isCommented].join(':'),
+ ),
+ shouldRenderCommentButton: memoize(
+ (props) => {
+ return isLoggedIn() && !props.line.isContextLineLeft && !props.line.isMetaLineLeft;
},
- emptyCellRightClassMap() {
- return { conflict_their: this.line.left?.type === CONFLICT_OUR };
- },
- emptyCellLeftClassMap() {
- return { conflict_our: this.line.right?.type === CONFLICT_THEIR };
- },
- shouldRenderCommentButton() {
- return this.isLoggedIn && !this.line.isContextLineLeft && !this.line.isMetaLineLeft;
- },
- isLeftConflictMarker() {
- return [CONFLICT_MARKER_OUR, CONFLICT_MARKER_THEIR].includes(this.line.left?.type);
- },
- interopLeftAttributes() {
- if (this.inline) {
- return getInteropInlineAttributes(this.line.left);
- }
+ (props) => [props.line.isContextLineLeft, props.line.isMetaLineLeft].join(':'),
+ ),
+ interopLeftAttributes(props) {
+ if (props.inline) {
+ return getInteropInlineAttributes(props.line.left);
+ }
- return getInteropOldSideAttributes(this.line.left);
- },
- interopRightAttributes() {
- return getInteropNewSideAttributes(this.line.right);
- },
+ return getInteropOldSideAttributes(props.line.left);
},
- mounted() {
- this.scrollToLineIfNeededParallel(this.line);
+ interopRightAttributes(props) {
+ return getInteropNewSideAttributes(props.line.right);
},
- methods: {
- ...mapActions('diffs', [
- 'scrollToLineIfNeededParallel',
- 'showCommentForm',
- 'setHighlightedRow',
- 'toggleLineDiscussions',
- ]),
- // Prevent text selecting on both sides of parallel diff view
- // Backport of the same code from legacy diff notes.
- handleParallelLineMouseDown(e) {
- const line = e.currentTarget;
- const table = line.closest('.diff-table');
-
- table.classList.remove('left-side-selected', 'right-side-selected');
- const [lineClass] = ['left-side', 'right-side'].filter((name) =>
- line.classList.contains(name),
- );
-
- if (lineClass) {
- table.classList.add(`${lineClass}-selected`);
- }
- },
- handleCommentButton(line) {
- this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
- },
- conflictText(line) {
- return line.type === CONFLICT_MARKER_THEIR
- ? this.$options.THEIR_CHANGES
- : this.$options.OUR_CHANGES;
- },
- onDragEnd() {
- this.dragging = false;
- if (!this.glFeatures.dragCommentSelection) return;
-
- this.$emit('stopdragging');
+ conflictText: memoize(
+ (line) => {
+ return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes';
},
- onDragEnter(line, index) {
- if (!this.glFeatures.dragCommentSelection) return;
+ (line) => line.type,
+ ),
+ lineContent: memoize(
+ (line) => {
+ if (line.isConflictMarker) {
+ return line.type === CONFLICT_MARKER_THEIR ? 'HEAD//our changes' : 'origin//their changes';
+ }
- this.$emit('enterdragging', { ...line, index });
- },
- onDragStart(line) {
- this.$root.$emit(BV_HIDE_TOOLTIP);
- this.dragging = true;
- this.$emit('startdragging', line);
+ return line.rich_text;
},
- },
- OUR_CHANGES: 'HEAD//our changes',
- THEIR_CHANGES: 'origin//their changes',
+ (line) => line.line_code,
+ ),
CONFLICT_MARKER,
CONFLICT_MARKER_THEIR,
CONFLICT_OUR,
@@ -207,250 +157,256 @@ export default {
};
</script>
-<template>
- <div :class="classNameMap" class="diff-grid-row diff-tr line_holder">
+<!-- eslint-disable-next-line vue/no-deprecated-functional-template -->
+<template functional>
+ <div :class="$options.classNameMap(props)" class="diff-grid-row diff-tr line_holder">
<div
+ :id="props.line.left && props.line.left.line_code"
data-testid="left-side"
class="diff-grid-left left-side"
- v-bind="interopLeftAttributes"
+ v-bind="$options.interopLeftAttributes(props)"
@dragover.prevent
- @dragenter="onDragEnter(line.left, index)"
- @dragend="onDragEnd"
+ @dragenter="listeners.enterdragging({ ...props.line.left, index: props.index })"
+ @dragend="listeners.stopdragging"
>
- <template v-if="line.left && line.left.type !== $options.CONFLICT_MARKER">
+ <template v-if="props.line.left && props.line.left.type !== $options.CONFLICT_MARKER">
<div
- :class="classNameMapCellLeft"
+ :class="$options.classNameMapCellLeft(props)"
data-testid="left-line-number"
class="diff-td diff-line-num"
data-qa-selector="new_diff_line_link"
>
- <template v-if="!isLeftConflictMarker">
- <span
- v-if="shouldRenderCommentButton && !line.hasDiscussionsLeft"
- v-gl-tooltip
- class="add-diff-note tooltip-wrapper"
- :title="addCommentTooltipLeft"
- >
- <div
- data-testid="left-comment-button"
- role="button"
- tabindex="0"
- :draggable="!line.left.commentsDisabled && glFeatures.dragCommentSelection"
- type="button"
- class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
- data-qa-selector="diff_comment_button"
- :class="{ 'gl-cursor-grab': dragging }"
- :disabled="line.left.commentsDisabled"
- :aria-disabled="line.left.commentsDisabled"
- @click="!line.left.commentsDisabled && handleCommentButton(line.left)"
- @keydown.enter="!line.left.commentsDisabled && handleCommentButton(line.left)"
- @keydown.space="!line.left.commentsDisabled && handleCommentButton(line.left)"
- @dragstart="!line.left.commentsDisabled && onDragStart({ ...line.left, index })"
- ></div>
- </span>
- </template>
+ <span
+ v-if="
+ !props.line.left.isConflictMarker &&
+ $options.shouldRenderCommentButton(props) &&
+ !props.line.hasDiscussionsLeft
+ "
+ class="add-diff-note tooltip-wrapper has-tooltip"
+ :title="props.line.left.addCommentTooltip"
+ >
+ <div
+ data-testid="left-comment-button"
+ role="button"
+ tabindex="0"
+ :draggable="!props.line.left.commentsDisabled"
+ type="button"
+ class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
+ data-qa-selector="diff_comment_button"
+ :disabled="props.line.left.commentsDisabled"
+ :aria-disabled="props.line.left.commentsDisabled"
+ @click="
+ !props.line.left.commentsDisabled &&
+ listeners.showCommentForm(props.line.left.line_code)
+ "
+ @keydown.enter="
+ !props.line.left.commentsDisabled &&
+ listeners.showCommentForm(props.line.left.line_code)
+ "
+ @keydown.space="
+ !props.line.left.commentsDisabled &&
+ listeners.showCommentForm(props.line.left.line_code)
+ "
+ @dragstart="
+ !props.line.left.commentsDisabled &&
+ listeners.startdragging({
+ event: $event,
+ line: { ...props.line.left, index: props.index },
+ })
+ "
+ ></div>
+ </span>
<a
- v-if="line.left.old_line && line.left.type !== $options.CONFLICT_THEIR"
- :data-linenumber="line.left.old_line"
- :href="line.lineHrefOld"
- @click="setHighlightedRow(line.lineCode)"
+ v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR"
+ :data-linenumber="props.line.left.old_line"
+ :href="props.line.lineHrefOld"
+ @click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
- <diff-gutter-avatars
- v-if="line.hasDiscussionsLeft"
- :discussions="line.left.discussions"
- :discussions-expanded="line.left.discussionsExpanded"
+ <component
+ :is="$options.DiffGutterAvatars"
+ v-if="props.line.hasDiscussionsLeft"
+ :discussions="props.line.left.discussions"
+ :discussions-expanded="props.line.left.discussionsExpanded"
data-testid="left-discussions"
@toggleLineDiscussions="
- toggleLineDiscussions({
- lineCode: line.left.line_code,
- fileHash,
- expanded: !line.left.discussionsExpanded,
+ listeners.toggleLineDiscussions({
+ lineCode: props.line.left.line_code,
+ expanded: !props.line.left.discussionsExpanded,
})
"
/>
</div>
- <div v-if="inline" :class="classNameMapCellLeft" class="diff-td diff-line-num">
+ <div
+ v-if="props.inline"
+ :class="$options.classNameMapCellLeft(props)"
+ class="diff-td diff-line-num"
+ >
<a
- v-if="line.left.new_line && line.left.type !== $options.CONFLICT_OUR"
- :data-linenumber="line.left.new_line"
- :href="line.lineHrefOld"
- @click="setHighlightedRow(line.lineCode)"
+ v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR"
+ :data-linenumber="props.line.left.new_line"
+ :href="props.line.lineHrefOld"
+ @click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
</div>
<div
- v-gl-tooltip.hover
- :title="coverageStateLeft.text"
- :class="[...parallelViewLeftLineType, coverageStateLeft.class]"
- class="diff-td line-coverage left-side"
+ :title="$options.coverageStateLeft(props).text"
+ :class="[
+ $options.parallelViewLeftLineType(props),
+ $options.coverageStateLeft(props).class,
+ ]"
+ class="diff-td line-coverage left-side has-tooltip"
></div>
- <div class="diff-td line-codequality left-side" :class="[...parallelViewLeftLineType]">
- <code-quality-gutter-icon
- v-if="showCodequalityLeft"
- :file-path="filePath"
- :codequality="line.left.codequality"
+ <div
+ class="diff-td line-codequality left-side"
+ :class="$options.parallelViewLeftLineType(props)"
+ >
+ <component
+ :is="$options.CodeQualityGutterIcon"
+ v-if="$options.showCodequalityLeft(props)"
+ :codequality="props.line.left.codequality"
+ :file-path="props.filePath"
/>
</div>
<div
- :id="line.left.line_code"
- :key="line.left.line_code"
- :class="[parallelViewLeftLineType, { parallel: !inline }]"
+ :key="props.line.left.line_code"
+ :class="[
+ $options.parallelViewLeftLineType(props),
+ { parallel: !props.inline, 'gl-font-weight-bold': props.line.left.isConflictMarker },
+ ]"
class="diff-td line_content with-coverage left-side"
data-testid="left-content"
- @mousedown="handleParallelLineMouseDown"
- >
- <strong v-if="isLeftConflictMarker">{{ conflictText(line.left) }}</strong>
- <span v-else v-html="line.left.rich_text"></span>
- </div>
+ v-html="$options.lineContent(props.line.left)"
+ ></div>
</template>
- <template v-else-if="!inline || (line.left && line.left.type === $options.CONFLICT_MARKER)">
- <div
- data-testid="left-empty-cell"
- class="diff-td diff-line-num old_line empty-cell"
- :class="emptyCellLeftClassMap"
- >
+ <template
+ v-else-if="
+ !props.inline || (props.line.left && props.line.left.type === $options.CONFLICT_MARKER)
+ "
+ >
+ <div data-testid="left-empty-cell" class="diff-td diff-line-num old_line empty-cell">
&nbsp;
</div>
- <div
- v-if="inline"
- class="diff-td diff-line-num old_line empty-cell"
- :class="emptyCellLeftClassMap"
- ></div>
- <div
- class="diff-td line-coverage left-side empty-cell"
- :class="emptyCellLeftClassMap"
- ></div>
- <div
- v-if="inline"
- class="diff-td line-codequality left-side empty-cell"
- :class="emptyCellLeftClassMap"
- ></div>
+ <div v-if="props.inline" class="diff-td diff-line-num old_line empty-cell"></div>
+ <div class="diff-td line-coverage left-side empty-cell"></div>
+ <div v-if="props.inline" class="diff-td line-codequality left-side empty-cell"></div>
<div
class="diff-td line_content with-coverage left-side empty-cell"
- :class="[emptyCellLeftClassMap, { parallel: !inline }]"
+ :class="[{ parallel: !props.inline }]"
></div>
</template>
</div>
<div
- v-if="!inline"
+ v-if="!props.inline"
+ :id="props.line.right && props.line.right.line_code"
data-testid="right-side"
class="diff-grid-right right-side"
- v-bind="interopRightAttributes"
+ v-bind="$options.interopRightAttributes(props)"
@dragover.prevent
- @dragenter="onDragEnter(line.right, index)"
- @dragend="onDragEnd"
+ @dragenter="listeners.enterdragging({ ...props.line.right, index: props.index })"
+ @dragend="listeners.stopdragging"
>
- <template v-if="line.right">
- <div :class="classNameMapCellRight" class="diff-td diff-line-num new_line">
- <template v-if="line.right.type !== $options.CONFLICT_MARKER_THEIR">
+ <template v-if="props.line.right">
+ <div :class="$options.classNameMapCellRight(props)" class="diff-td diff-line-num new_line">
+ <template v-if="props.line.right.type !== $options.CONFLICT_MARKER_THEIR">
<span
- v-if="shouldRenderCommentButton && !line.hasDiscussionsRight"
- v-gl-tooltip
- class="add-diff-note tooltip-wrapper"
- :title="addCommentTooltipRight"
+ v-if="$options.shouldRenderCommentButton(props) && !props.line.hasDiscussionsRight"
+ class="add-diff-note tooltip-wrapper has-tooltip"
+ :title="props.line.right.addCommentTooltip"
>
<div
data-testid="right-comment-button"
role="button"
tabindex="0"
- :draggable="!line.right.commentsDisabled && glFeatures.dragCommentSelection"
+ :draggable="!props.line.right.commentsDisabled"
type="button"
class="add-diff-note unified-diff-components-diff-note-button note-button js-add-diff-note-button"
- :class="{ 'gl-cursor-grab': dragging }"
- :disabled="line.right.commentsDisabled"
- :aria-disabled="line.right.commentsDisabled"
- @click="!line.right.commentsDisabled && handleCommentButton(line.right)"
- @keydown.enter="!line.right.commentsDisabled && handleCommentButton(line.right)"
- @keydown.space="!line.right.commentsDisabled && handleCommentButton(line.right)"
- @dragstart="!line.right.commentsDisabled && onDragStart({ ...line.right, index })"
+ :disabled="props.line.right.commentsDisabled"
+ :aria-disabled="props.line.right.commentsDisabled"
+ @click="
+ !props.line.right.commentsDisabled &&
+ listeners.showCommentForm(props.line.right.line_code)
+ "
+ @keydown.enter="
+ !props.line.right.commentsDisabled &&
+ listeners.showCommentForm(props.line.right.line_code)
+ "
+ @keydown.space="
+ !props.line.right.commentsDisabled &&
+ listeners.showCommentForm(props.line.right.line_code)
+ "
+ @dragstart="
+ !props.line.right.commentsDisabled &&
+ listeners.startdragging({
+ event: $event,
+ line: { ...props.line.right, index: props.index },
+ })
+ "
></div>
</span>
</template>
<a
- v-if="line.right.new_line"
- :data-linenumber="line.right.new_line"
- :href="line.lineHrefNew"
- @click="setHighlightedRow(line.lineCode)"
+ v-if="props.line.right.new_line"
+ :data-linenumber="props.line.right.new_line"
+ :href="props.line.lineHrefNew"
+ @click="listeners.setHighlightedRow(props.line.lineCode)"
>
</a>
- <diff-gutter-avatars
- v-if="line.hasDiscussionsRight"
- :discussions="line.right.discussions"
- :discussions-expanded="line.right.discussionsExpanded"
+ <component
+ :is="$options.DiffGutterAvatars"
+ v-if="props.line.hasDiscussionsRight"
+ :discussions="props.line.right.discussions"
+ :discussions-expanded="props.line.right.discussionsExpanded"
data-testid="right-discussions"
@toggleLineDiscussions="
- toggleLineDiscussions({
- lineCode: line.right.line_code,
- fileHash,
- expanded: !line.right.discussionsExpanded,
+ listeners.toggleLineDiscussions({
+ lineCode: props.line.right.line_code,
+ expanded: !props.line.right.discussionsExpanded,
})
"
/>
</div>
<div
- v-gl-tooltip.hover
- :title="coverageStateRight.text"
+ :title="$options.coverageStateRight(props).text"
:class="[
- line.right.type,
- coverageStateRight.class,
- { hll: isHighlighted, hll: isCommented },
+ props.line.right.type,
+ $options.coverageStateRight(props).class,
+ { hll: props.isHighlighted, hll: props.isCommented },
]"
- class="diff-td line-coverage right-side"
+ class="diff-td line-coverage right-side has-tooltip"
></div>
<div
class="diff-td line-codequality right-side"
- :class="[line.right.type, { hll: isHighlighted, hll: isCommented }]"
+ :class="[props.line.right.type, { hll: props.isHighlighted, hll: props.isCommented }]"
>
- <code-quality-gutter-icon
- v-if="showCodequalityRight"
- :file-path="filePath"
- :codequality="line.right.codequality"
+ <component
+ :is="$options.CodeQualityGutterIcon"
+ v-if="$options.showCodequalityRight(props)"
+ :codequality="props.line.right.codequality"
+ :file-path="props.filePath"
+ data-testid="codeQualityIcon"
/>
</div>
<div
- :id="line.right.line_code"
- :key="line.right.rich_text"
+ :key="props.line.right.rich_text"
:class="[
- line.right.type,
+ props.line.right.type,
{
- hll: isHighlighted,
- hll: isCommented,
- parallel: !inline,
+ hll: props.isHighlighted,
+ hll: props.isCommented,
+ 'gl-font-weight-bold': props.line.right.type === $options.CONFLICT_MARKER_THEIR,
},
]"
- class="diff-td line_content with-coverage right-side"
- @mousedown="handleParallelLineMouseDown"
- >
- <strong v-if="line.right.type === $options.CONFLICT_MARKER_THEIR">{{
- conflictText(line.right)
- }}</strong>
- <span v-else v-html="line.right.rich_text"></span>
- </div>
+ class="diff-td line_content with-coverage right-side parallel"
+ v-html="$options.lineContent(props.line.right)"
+ ></div>
</template>
<template v-else>
- <div
- data-testid="right-empty-cell"
- class="diff-td diff-line-num old_line empty-cell"
- :class="emptyCellRightClassMap"
- ></div>
- <div
- v-if="inline"
- class="diff-td diff-line-num old_line empty-cell"
- :class="emptyCellRightClassMap"
- ></div>
- <div
- class="diff-td line-coverage right-side empty-cell"
- :class="emptyCellRightClassMap"
- ></div>
- <div
- class="diff-td line-codequality right-side empty-cell"
- :class="emptyCellRightClassMap"
- ></div>
- <div
- class="diff-td line_content with-coverage right-side empty-cell"
- :class="[emptyCellRightClassMap, { parallel: !inline }]"
- ></div>
+ <div data-testid="right-empty-cell" class="diff-td diff-line-num old_line empty-cell"></div>
+ <div class="diff-td line-coverage right-side empty-cell"></div>
+ <div class="diff-td line-codequality right-side empty-cell"></div>
+ <div class="diff-td line_content with-coverage right-side empty-cell parallel"></div>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index cd45474afcd..99999445c43 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -6,13 +6,17 @@ import {
OLD_NO_NEW_LINE_TYPE,
NEW_NO_NEW_LINE_TYPE,
EMPTY_CELL_TYPE,
+ CONFLICT_MARKER_OUR,
+ CONFLICT_MARKER_THEIR,
+ CONFLICT_THEIR,
+ CONFLICT_OUR,
} from '../constants';
-export const isHighlighted = (state, line, isCommented) => {
+export const isHighlighted = (highlightedRow, line, isCommented) => {
if (isCommented) return true;
const lineCode = line?.line_code;
- return lineCode ? lineCode === state.diffs.highlightedRow : false;
+ return lineCode ? lineCode === highlightedRow : false;
};
export const isContextLine = (type) => type === CONTEXT_LINE_TYPE;
@@ -50,13 +54,11 @@ export const classNameMapCell = ({ line, hll, isLoggedIn, isHover }) => {
];
};
-export const addCommentTooltip = (line, dragCommentSelectionEnabled = false) => {
+export const addCommentTooltip = (line) => {
let tooltip;
if (!line) return tooltip;
- tooltip = dragCommentSelectionEnabled
- ? __('Add a comment to this line or drag for multiple lines')
- : __('Add a comment to this line');
+ tooltip = __('Add a comment to this line or drag for multiple lines');
const brokenSymlinks = line.commentsDisabled;
if (brokenSymlinks) {
@@ -107,6 +109,10 @@ export const mapParallel = (content) => (line) => {
hasDraft: content.hasParallelDraftLeft(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'left'),
hasCommentForm: left.hasForm,
+ isConflictMarker:
+ line.left.type === CONFLICT_MARKER_OUR || line.left.type === CONFLICT_MARKER_THEIR,
+ emptyCellClassMap: { conflict_our: line.right?.type === CONFLICT_THEIR },
+ addCommentTooltip: addCommentTooltip(line.left),
};
}
if (right) {
@@ -116,6 +122,8 @@ export const mapParallel = (content) => (line) => {
hasDraft: content.hasParallelDraftRight(content.diffFile.file_hash, line),
lineDraft: content.draftForLine(content.diffFile.file_hash, line, 'right'),
hasCommentForm: Boolean(right.hasForm && right.type),
+ emptyCellClassMap: { conflict_their: line.left?.type === CONFLICT_OUR },
+ addCommentTooltip: addCommentTooltip(line.right),
};
}
@@ -139,24 +147,3 @@ export const mapParallel = (content) => (line) => {
commentRowClasses: hasDiscussions(left) || hasDiscussions(right) ? '' : 'js-temp-notes-holder',
};
};
-
-// TODO: Delete this function when unifiedDiffComponents FF is removed
-export const mapInline = (content) => (line) => {
- // Discussions/Comments
- const renderCommentRow = line.hasForm || (line.discussions?.length && line.discussionsExpanded);
-
- return {
- ...line,
- renderDiscussion: Boolean(line.discussions?.length),
- isMatchLine: isMatchLine(line.type),
- commentRowClasses: line.discussions?.length ? '' : 'js-temp-notes-holder',
- renderCommentRow,
- hasDraft: content.shouldRenderDraftRow(content.diffFile.file_hash, line),
- hasCommentForm: line.hasForm,
- isMetaLine: isMetaLine(line.type),
- isContextLine: isContextLine(line.type),
- hasDiscussions: hasDiscussions(line),
- lineHref: lineHref(line),
- lineCode: lineCode(line),
- };
-};
diff --git a/app/assets/javascripts/diffs/components/diff_view.vue b/app/assets/javascripts/diffs/components/diff_view.vue
index a2a6ebaeedf..5cf242b4ddd 100644
--- a/app/assets/javascripts/diffs/components/diff_view.vue
+++ b/app/assets/javascripts/diffs/components/diff_view.vue
@@ -1,12 +1,15 @@
<script>
import { mapGetters, mapState, mapActions } from 'vuex';
+import { IdState } from 'vendor/vue-virtual-scroller';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import draftCommentsMixin from '~/diffs/mixins/draft_comments';
import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
+import { hide } from '~/tooltips';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import DiffCommentCell from './diff_comment_cell.vue';
import DiffExpansionCell from './diff_expansion_cell.vue';
import DiffRow from './diff_row.vue';
+import { isHighlighted } from './diff_row_utils';
export default {
components: {
@@ -15,7 +18,11 @@ export default {
DiffCommentCell,
DraftNote,
},
- mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
+ mixins: [
+ draftCommentsMixin,
+ glFeatureFlagsMixin(),
+ IdState({ idProp: (vm) => vm.diffFile.file_hash }),
+ ],
props: {
diffFile: {
type: Object,
@@ -36,15 +43,15 @@ export default {
default: false,
},
},
- data() {
+ idState() {
return {
dragStart: null,
updatedLineRange: null,
};
},
computed: {
- ...mapGetters('diffs', ['commitId']),
- ...mapState('diffs', ['codequalityDiff']),
+ ...mapGetters('diffs', ['commitId', 'fileLineCoverage']),
+ ...mapState('diffs', ['codequalityDiff', 'highlightedRow']),
...mapState({
selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
@@ -59,45 +66,65 @@ export default {
);
},
hasCodequalityChanges() {
- return (
- this.glFeatures.codequalityMrDiffAnnotations &&
- this.codequalityDiff?.files?.[this.diffFile.file_path]?.length > 0
- );
+ return this.codequalityDiff?.files?.[this.diffFile.file_path]?.length > 0;
},
},
methods: {
...mapActions(['setSelectedCommentPosition']),
- ...mapActions('diffs', ['showCommentForm']),
+ ...mapActions('diffs', ['showCommentForm', 'setHighlightedRow', 'toggleLineDiscussions']),
showCommentLeft(line) {
return line.left && !line.right;
},
showCommentRight(line) {
return line.right && !line.left;
},
- onStartDragging(line) {
- this.dragStart = line;
+ onStartDragging({ event = {}, line }) {
+ if (event.target?.parentNode) {
+ hide(event.target.parentNode);
+ }
+ this.idState.dragStart = line;
},
onDragOver(line) {
- if (line.chunk !== this.dragStart.chunk) return;
+ if (line.chunk !== this.idState.dragStart.chunk) return;
- let start = this.dragStart;
+ let start = this.idState.dragStart;
let end = line;
- if (this.dragStart.index >= line.index) {
+ if (this.idState.dragStart.index >= line.index) {
start = line;
- end = this.dragStart;
+ end = this.idState.dragStart;
}
- this.updatedLineRange = { start, end };
+ this.idState.updatedLineRange = { start, end };
- this.setSelectedCommentPosition(this.updatedLineRange);
+ this.setSelectedCommentPosition(this.idState.updatedLineRange);
},
onStopDragging() {
this.showCommentForm({
- lineCode: this.updatedLineRange?.end?.line_code,
+ lineCode: this.idState.updatedLineRange?.end?.line_code,
fileHash: this.diffFile.file_hash,
});
- this.dragStart = null;
+ this.idState.dragStart = null;
+ },
+ isHighlighted(line) {
+ return isHighlighted(
+ this.highlightedRow,
+ line.left?.line_code ? line.left : line.right,
+ false,
+ );
+ },
+ handleParallelLineMouseDown(e) {
+ const line = e.target.closest('.diff-td');
+ const table = line.closest('.diff-table');
+
+ table.classList.remove('left-side-selected', 'right-side-selected');
+ const [lineClass] = ['left-side', 'right-side'].filter((name) =>
+ line.classList.contains(name),
+ );
+
+ if (lineClass) {
+ table.classList.add(`${lineClass}-selected`);
+ }
},
},
userColorScheme: window.gon.user_color_scheme,
@@ -109,6 +136,7 @@ export default {
:class="[$options.userColorScheme, { inline, 'with-codequality': hasCodequalityChanges }]"
:data-commit-id="commitId"
class="diff-grid diff-table code diff-wrap-lines js-syntax-highlight text-file"
+ @mousedown="handleParallelLineMouseDown"
>
<template v-for="(line, index) in diffLines">
<div
@@ -136,6 +164,14 @@ export default {
:is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
:inline="inline"
:index="index"
+ :is-highlighted="isHighlighted(line)"
+ :file-line-coverage="fileLineCoverage"
+ @showCommentForm="(lineCode) => showCommentForm({ lineCode, fileHash: diffFile.file_hash })"
+ @setHighlightedRow="setHighlightedRow"
+ @toggleLineDiscussions="
+ ({ lineCode, expanded }) =>
+ toggleLineDiscussions({ lineCode, fileHash: diffFile.file_hash, expanded })
+ "
@enterdragging="onDragOver"
@startdragging="onStartDragging"
@stopdragging="onStopDragging"
diff --git a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue b/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
deleted file mode 100644
index f903fef72b7..00000000000
--- a/app/assets/javascripts/diffs/components/inline_diff_table_row.vue
+++ /dev/null
@@ -1,204 +0,0 @@
-<script>
-import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { CONTEXT_LINE_CLASS_NAME } from '../constants';
-import { getInteropInlineAttributes } from '../utils/interoperability';
-import DiffGutterAvatars from './diff_gutter_avatars.vue';
-import {
- isHighlighted,
- shouldShowCommentButton,
- shouldRenderCommentButton,
- classNameMapCell,
- addCommentTooltip,
-} from './diff_row_utils';
-
-export default {
- components: {
- DiffGutterAvatars,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
- },
- props: {
- fileHash: {
- type: String,
- required: true,
- },
- filePath: {
- type: String,
- required: true,
- },
- line: {
- type: Object,
- required: true,
- },
- isBottom: {
- type: Boolean,
- required: false,
- default: false,
- },
- isCommented: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isHover: false,
- };
- },
- computed: {
- ...mapGetters(['isLoggedIn']),
- ...mapGetters('diffs', ['fileLineCoverage']),
- ...mapState({
- isHighlighted(state) {
- return isHighlighted(state, this.line, this.isCommented);
- },
- }),
- classNameMap() {
- return [
- this.line.type,
- {
- [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLine,
- },
- ];
- },
- inlineRowId() {
- return this.line.line_code || `${this.fileHash}_${this.line.old_line}_${this.line.new_line}`;
- },
- coverageState() {
- return this.fileLineCoverage(this.filePath, this.line.new_line);
- },
- classNameMapCell() {
- return classNameMapCell({
- line: this.line,
- hll: this.isHighlighted,
- isLoggedIn: this.isLoggedIn,
- isHover: this.isHover,
- });
- },
- addCommentTooltip() {
- return addCommentTooltip(this.line);
- },
- shouldRenderCommentButton() {
- return shouldRenderCommentButton(this.isLoggedIn, true);
- },
- shouldShowCommentButton() {
- return shouldShowCommentButton(
- this.isHover,
- this.line.isContextLine,
- this.line.isMetaLine,
- this.line.hasDiscussions,
- );
- },
- shouldShowAvatarsOnGutter() {
- return this.line.hasDiscussions;
- },
- interopAttrs() {
- return getInteropInlineAttributes(this.line);
- },
- },
- mounted() {
- this.scrollToLineIfNeededInline(this.line);
- },
- methods: {
- ...mapActions('diffs', [
- 'scrollToLineIfNeededInline',
- 'showCommentForm',
- 'setHighlightedRow',
- 'toggleLineDiscussions',
- ]),
- handleMouseMove(e) {
- // To show the comment icon on the gutter we need to know if we hover the line.
- // Current table structure doesn't allow us to do this with CSS in both of the diff view types
- this.isHover = e.type === 'mouseover';
- },
- handleCommentButton() {
- this.showCommentForm({ lineCode: this.line.line_code, fileHash: this.fileHash });
- },
- },
-};
-</script>
-
-<template>
- <tr
- :id="inlineRowId"
- :class="classNameMap"
- class="line_holder"
- v-bind="interopAttrs"
- @mouseover="handleMouseMove"
- @mouseout="handleMouseMove"
- >
- <td ref="oldTd" class="diff-line-num old_line" :class="classNameMapCell">
- <span
- v-if="shouldRenderCommentButton"
- ref="addNoteTooltip"
- v-gl-tooltip
- class="add-diff-note tooltip-wrapper"
- :title="addCommentTooltip"
- >
- <button
- v-show="shouldShowCommentButton"
- ref="addDiffNoteButton"
- type="button"
- class="add-diff-note note-button js-add-diff-note-button"
- :disabled="line.commentsDisabled"
- :aria-label="addCommentTooltip"
- @click="handleCommentButton"
- >
- <gl-icon :size="12" name="comment" />
- </button>
- </span>
- <a
- v-if="line.old_line"
- ref="lineNumberRefOld"
- :data-linenumber="line.old_line"
- :href="line.lineHref"
- @click="setHighlightedRow(line.lineCode)"
- >
- </a>
- <diff-gutter-avatars
- v-if="shouldShowAvatarsOnGutter"
- :discussions="line.discussions"
- :discussions-expanded="line.discussionsExpanded"
- @toggleLineDiscussions="
- toggleLineDiscussions({
- lineCode: line.lineCode,
- fileHash,
- expanded: !line.discussionsExpanded,
- })
- "
- />
- </td>
- <td ref="newTd" class="diff-line-num new_line" :class="classNameMapCell">
- <a
- v-if="line.new_line"
- ref="lineNumberRefNew"
- :data-linenumber="line.new_line"
- :href="line.lineHref"
- @click="setHighlightedRow(line.lineCode)"
- >
- </a>
- </td>
- <td
- v-gl-tooltip.hover
- :title="coverageState.text"
- :class="[line.type, coverageState.class, { hll: isHighlighted }]"
- class="line-coverage"
- ></td>
- <td
- :key="line.line_code"
- v-safe-html="line.rich_text"
- :class="[
- line.type,
- {
- hll: isHighlighted,
- },
- ]"
- class="line_content with-coverage"
- ></td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/diffs/components/inline_diff_view.vue b/app/assets/javascripts/diffs/components/inline_diff_view.vue
deleted file mode 100644
index e407609d9e9..00000000000
--- a/app/assets/javascripts/diffs/components/inline_diff_view.vue
+++ /dev/null
@@ -1,117 +0,0 @@
-<script>
-import { mapGetters, mapState } from 'vuex';
-import DraftNote from '~/batch_comments/components/draft_note.vue';
-import draftCommentsMixin from '~/diffs/mixins/draft_comments';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import DiffCommentCell from './diff_comment_cell.vue';
-import DiffExpansionCell from './diff_expansion_cell.vue';
-import inlineDiffTableRow from './inline_diff_table_row.vue';
-
-export default {
- components: {
- DiffCommentCell,
- inlineDiffTableRow,
- DraftNote,
- DiffExpansionCell,
- },
- mixins: [draftCommentsMixin, glFeatureFlagsMixin()],
- props: {
- diffFile: {
- type: Object,
- required: true,
- },
- diffLines: {
- type: Array,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- ...mapGetters('diffs', ['commitId']),
- ...mapState({
- selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
- selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
- }),
- diffLinesLength() {
- return this.diffLines.length;
- },
- commentedLines() {
- return getCommentedLines(
- this.selectedCommentPosition || this.selectedCommentPositionHover,
- this.diffLines,
- );
- },
- },
- userColorScheme: window.gon.user_color_scheme,
-};
-</script>
-
-<template>
- <table
- :class="$options.userColorScheme"
- :data-commit-id="commitId"
- class="code diff-wrap-lines js-syntax-highlight text-file js-diff-inline-view"
- >
- <colgroup>
- <col style="width: 50px" />
- <col style="width: 50px" />
- <col style="width: 8px" />
- <col />
- </colgroup>
- <tbody>
- <template v-for="(line, index) in diffLines">
- <tr v-if="line.isMatchLine" :key="`expand-${index}`" class="line_expansion match">
- <td colspan="4" class="text-center gl-font-regular">
- <diff-expansion-cell
- :file-hash="diffFile.file_hash"
- :context-lines-path="diffFile.context_lines_path"
- :line="line"
- :is-top="index === 0"
- :is-bottom="index + 1 === diffLinesLength"
- />
- </td>
- </tr>
- <inline-diff-table-row
- v-if="!line.isMatchLine"
- :key="`${line.line_code || index}`"
- :file-hash="diffFile.file_hash"
- :file-path="diffFile.file_path"
- :line="line"
- :is-bottom="index + 1 === diffLinesLength"
- :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
- />
- <tr
- v-if="line.renderCommentRow"
- :key="`icr-${line.line_code || index}`"
- :class="line.commentRowClasses"
- class="notes_holder"
- >
- <td class="notes-content" colspan="4">
- <diff-comment-cell
- :diff-file-hash="diffFile.file_hash"
- :line="line"
- :help-page-path="helpPagePath"
- :has-draft="line.hasDraft"
- />
- </td>
- </tr>
- <tr v-if="line.hasDraft" :key="`draft_${index}`" class="notes_holder js-temp-notes-holder">
- <td class="notes-content" colspan="4">
- <div class="content">
- <draft-note
- :draft="draftForLine(diffFile.file_hash, line)"
- :diff-file="diffFile"
- :line="line"
- />
- </div>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
-</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue b/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
deleted file mode 100644
index 2d33926c8aa..00000000000
--- a/app/assets/javascripts/diffs/components/parallel_diff_table_row.vue
+++ /dev/null
@@ -1,310 +0,0 @@
-<script>
-import { GlTooltipDirective, GlIcon, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
-import $ from 'jquery';
-import { mapActions, mapGetters, mapState } from 'vuex';
-import { CONTEXT_LINE_CLASS_NAME, PARALLEL_DIFF_VIEW_TYPE } from '../constants';
-import {
- getInteropOldSideAttributes,
- getInteropNewSideAttributes,
-} from '../utils/interoperability';
-import DiffGutterAvatars from './diff_gutter_avatars.vue';
-import * as utils from './diff_row_utils';
-
-export default {
- components: {
- GlIcon,
- DiffGutterAvatars,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- SafeHtml,
- },
- props: {
- fileHash: {
- type: String,
- required: true,
- },
- filePath: {
- type: String,
- required: true,
- },
- line: {
- type: Object,
- required: true,
- },
- isBottom: {
- type: Boolean,
- required: false,
- default: false,
- },
- isCommented: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isLeftHover: false,
- isRightHover: false,
- isCommentButtonRendered: false,
- };
- },
- computed: {
- ...mapGetters('diffs', ['fileLineCoverage']),
- ...mapGetters(['isLoggedIn']),
- ...mapState({
- isHighlighted(state) {
- const line = this.line.left?.line_code ? this.line.left : this.line.right;
- return utils.isHighlighted(state, line, this.isCommented);
- },
- }),
- classNameMap() {
- return {
- [CONTEXT_LINE_CLASS_NAME]: this.line.isContextLineLeft,
- [PARALLEL_DIFF_VIEW_TYPE]: true,
- };
- },
- parallelViewLeftLineType() {
- return utils.parallelViewLeftLineType(this.line, this.isHighlighted);
- },
- coverageState() {
- return this.fileLineCoverage(this.filePath, this.line.right.new_line);
- },
- classNameMapCellLeft() {
- return utils.classNameMapCell({
- line: this.line.left,
- hll: this.isHighlighted,
- isLoggedIn: this.isLoggedIn,
- isHover: this.isLeftHover,
- });
- },
- classNameMapCellRight() {
- return utils.classNameMapCell({
- line: this.line.right,
- hll: this.isHighlighted,
- isLoggedIn: this.isLoggedIn,
- isHover: this.isRightHover,
- });
- },
- addCommentTooltipLeft() {
- return utils.addCommentTooltip(this.line.left);
- },
- addCommentTooltipRight() {
- return utils.addCommentTooltip(this.line.right);
- },
- shouldRenderCommentButton() {
- return utils.shouldRenderCommentButton(this.isLoggedIn, this.isCommentButtonRendered);
- },
- shouldShowCommentButtonLeft() {
- return utils.shouldShowCommentButton(
- this.isLeftHover,
- this.line.isContextLineLeft,
- this.line.isMetaLineLeft,
- this.line.hasDiscussionsLeft,
- );
- },
- shouldShowCommentButtonRight() {
- return utils.shouldShowCommentButton(
- this.isRightHover,
- this.line.isContextLineRight,
- this.line.isMetaLineRight,
- this.line.hasDiscussionsRight,
- );
- },
- interopLeftAttributes() {
- return getInteropOldSideAttributes(this.line.left);
- },
- interopRightAttributes() {
- return getInteropNewSideAttributes(this.line.right);
- },
- },
- mounted() {
- this.scrollToLineIfNeededParallel(this.line);
- this.unwatchShouldShowCommentButton = this.$watch(
- (vm) => [vm.shouldShowCommentButtonLeft, vm.shouldShowCommentButtonRight].join(),
- (newVal) => {
- if (newVal) {
- this.isCommentButtonRendered = true;
- this.unwatchShouldShowCommentButton();
- }
- },
- );
- },
- beforeDestroy() {
- this.unwatchShouldShowCommentButton();
- },
- methods: {
- ...mapActions('diffs', [
- 'scrollToLineIfNeededParallel',
- 'showCommentForm',
- 'setHighlightedRow',
- 'toggleLineDiscussions',
- ]),
- handleMouseMove(e) {
- const isHover = e.type === 'mouseover';
- const hoveringCell = e.target.closest('td');
- const allCellsInHoveringRow = Array.from(e.currentTarget.children);
- const hoverIndex = allCellsInHoveringRow.indexOf(hoveringCell);
-
- if (hoverIndex >= 3) {
- this.isRightHover = isHover;
- } else {
- this.isLeftHover = isHover;
- }
- },
- // Prevent text selecting on both sides of parallel diff view
- // Backport of the same code from legacy diff notes.
- handleParallelLineMouseDown(e) {
- const line = $(e.currentTarget);
- const table = line.closest('table');
-
- table.removeClass('left-side-selected right-side-selected');
- const [lineClass] = ['left-side', 'right-side'].filter((name) => line.hasClass(name));
-
- if (lineClass) {
- table.addClass(`${lineClass}-selected`);
- }
- },
- handleCommentButton(line) {
- this.showCommentForm({ lineCode: line.line_code, fileHash: this.fileHash });
- },
- },
-};
-</script>
-
-<template>
- <tr
- :class="classNameMap"
- class="line_holder"
- @mouseover="handleMouseMove"
- @mouseout="handleMouseMove"
- >
- <template v-if="line.left && !line.isMatchLineLeft">
- <td ref="oldTd" :class="classNameMapCellLeft" class="diff-line-num old_line">
- <span
- v-if="shouldRenderCommentButton"
- ref="addNoteTooltipLeft"
- v-gl-tooltip
- class="add-diff-note tooltip-wrapper"
- :title="addCommentTooltipLeft"
- >
- <button
- v-show="shouldShowCommentButtonLeft"
- ref="addDiffNoteButtonLeft"
- type="button"
- class="add-diff-note note-button js-add-diff-note-button"
- :disabled="line.left.commentsDisabled"
- :aria-label="addCommentTooltipLeft"
- @click="handleCommentButton(line.left)"
- >
- <gl-icon :size="12" name="comment" />
- </button>
- </span>
- <a
- v-if="line.left.old_line"
- ref="lineNumberRefOld"
- :data-linenumber="line.left.old_line"
- :href="line.lineHrefOld"
- @click="setHighlightedRow(line.lineCode)"
- >
- </a>
- <diff-gutter-avatars
- v-if="line.hasDiscussionsLeft"
- :discussions="line.left.discussions"
- :discussions-expanded="line.left.discussionsExpanded"
- @toggleLineDiscussions="
- toggleLineDiscussions({
- lineCode: line.left.line_code,
- fileHash,
- expanded: !line.left.discussionsExpanded,
- })
- "
- />
- </td>
- <td :class="parallelViewLeftLineType" class="line-coverage left-side"></td>
- <td
- :id="line.left.line_code"
- :key="line.left.line_code"
- v-safe-html="line.left.rich_text"
- :class="parallelViewLeftLineType"
- v-bind="interopLeftAttributes"
- class="line_content with-coverage parallel left-side"
- @mousedown="handleParallelLineMouseDown"
- ></td>
- </template>
- <template v-else>
- <td class="diff-line-num old_line empty-cell"></td>
- <td class="line-coverage left-side empty-cell"></td>
- <td class="line_content with-coverage parallel left-side empty-cell"></td>
- </template>
- <template v-if="line.right && !line.isMatchLineRight">
- <td ref="newTd" :class="classNameMapCellRight" class="diff-line-num new_line">
- <span
- v-if="shouldRenderCommentButton"
- ref="addNoteTooltipRight"
- v-gl-tooltip
- class="add-diff-note tooltip-wrapper"
- :title="addCommentTooltipRight"
- >
- <button
- v-show="shouldShowCommentButtonRight"
- ref="addDiffNoteButtonRight"
- type="button"
- class="add-diff-note note-button js-add-diff-note-button"
- :disabled="line.right.commentsDisabled"
- :aria-label="addCommentTooltipRight"
- @click="handleCommentButton(line.right)"
- >
- <gl-icon :size="12" name="comment" />
- </button>
- </span>
- <a
- v-if="line.right.new_line"
- ref="lineNumberRefNew"
- :data-linenumber="line.right.new_line"
- :href="line.lineHrefNew"
- @click="setHighlightedRow(line.lineCode)"
- >
- </a>
- <diff-gutter-avatars
- v-if="line.hasDiscussionsRight"
- :discussions="line.right.discussions"
- :discussions-expanded="line.right.discussionsExpanded"
- @toggleLineDiscussions="
- toggleLineDiscussions({
- lineCode: line.right.line_code,
- fileHash,
- expanded: !line.right.discussionsExpanded,
- })
- "
- />
- </td>
- <td
- v-gl-tooltip.hover
- :title="coverageState.text"
- :class="[line.right.type, coverageState.class, { hll: isHighlighted }]"
- class="line-coverage right-side"
- ></td>
- <td
- :id="line.right.line_code"
- :key="line.right.rich_text"
- v-safe-html="line.right.rich_text"
- :class="[
- line.right.type,
- {
- hll: isHighlighted,
- },
- ]"
- v-bind="interopRightAttributes"
- class="line_content with-coverage parallel right-side"
- @mousedown="handleParallelLineMouseDown"
- ></td>
- </template>
- <template v-else>
- <td class="diff-line-num old_line empty-cell"></td>
- <td class="line-coverage right-side empty-cell"></td>
- <td class="line_content with-coverage parallel right-side empty-cell"></td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/diffs/components/parallel_diff_view.vue b/app/assets/javascripts/diffs/components/parallel_diff_view.vue
deleted file mode 100644
index b167081a379..00000000000
--- a/app/assets/javascripts/diffs/components/parallel_diff_view.vue
+++ /dev/null
@@ -1,142 +0,0 @@
-<script>
-import { mapGetters, mapState } from 'vuex';
-import DraftNote from '~/batch_comments/components/draft_note.vue';
-import draftCommentsMixin from '~/diffs/mixins/draft_comments';
-import { getCommentedLines } from '~/notes/components/multiline_comment_utils';
-import DiffCommentCell from './diff_comment_cell.vue';
-import DiffExpansionCell from './diff_expansion_cell.vue';
-import parallelDiffTableRow from './parallel_diff_table_row.vue';
-
-export default {
- components: {
- DiffExpansionCell,
- parallelDiffTableRow,
- DiffCommentCell,
- DraftNote,
- },
- mixins: [draftCommentsMixin],
- props: {
- diffFile: {
- type: Object,
- required: true,
- },
- diffLines: {
- type: Array,
- required: true,
- },
- helpPagePath: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- ...mapGetters('diffs', ['commitId']),
- ...mapState({
- selectedCommentPosition: ({ notes }) => notes.selectedCommentPosition,
- selectedCommentPositionHover: ({ notes }) => notes.selectedCommentPositionHover,
- }),
- diffLinesLength() {
- return this.diffLines.length;
- },
- commentedLines() {
- return getCommentedLines(
- this.selectedCommentPosition || this.selectedCommentPositionHover,
- this.diffLines,
- );
- },
- },
- userColorScheme: window.gon.user_color_scheme,
-};
-</script>
-
-<template>
- <table
- :class="$options.userColorScheme"
- :data-commit-id="commitId"
- class="code diff-wrap-lines js-syntax-highlight text-file"
- >
- <colgroup>
- <col style="width: 50px" />
- <col style="width: 8px" />
- <col />
- <col style="width: 50px" />
- <col style="width: 8px" />
- <col />
- </colgroup>
- <tbody>
- <template v-for="(line, index) in diffLines">
- <tr
- v-if="line.isMatchLineLeft || line.isMatchLineRight"
- :key="`expand-${index}`"
- class="line_expansion match"
- >
- <td colspan="6" class="text-center gl-font-regular">
- <diff-expansion-cell
- :file-hash="diffFile.file_hash"
- :context-lines-path="diffFile.context_lines_path"
- :line="line.left"
- :is-top="index === 0"
- :is-bottom="index + 1 === diffLinesLength"
- />
- </td>
- </tr>
- <parallel-diff-table-row
- :key="line.line_code"
- :file-hash="diffFile.file_hash"
- :file-path="diffFile.file_path"
- :line="line"
- :is-bottom="index + 1 === diffLinesLength"
- :is-commented="index >= commentedLines.startLine && index <= commentedLines.endLine"
- />
- <tr
- v-if="line.renderCommentRow"
- :key="`dcr-${line.line_code || index}`"
- :class="line.commentRowClasses"
- class="notes_holder"
- >
- <td class="notes-content parallel old" colspan="3">
- <diff-comment-cell
- v-if="line.left"
- :line="line.left"
- :diff-file-hash="diffFile.file_hash"
- :help-page-path="helpPagePath"
- :has-draft="line.left.hasDraft"
- line-position="left"
- />
- </td>
- <td class="notes-content parallel new" colspan="3">
- <diff-comment-cell
- v-if="line.right"
- :line="line.right"
- :diff-file-hash="diffFile.file_hash"
- :line-index="index"
- :help-page-path="helpPagePath"
- :has-draft="line.right.hasDraft"
- line-position="right"
- />
- </td>
- </tr>
- <tr
- v-if="shouldRenderParallelDraftRow(diffFile.file_hash, line)"
- :key="`drafts-${index}`"
- :class="line.draftRowClasses"
- class="notes_holder"
- >
- <td class="notes_line old"></td>
- <td class="notes-content parallel old" colspan="2">
- <div v-if="line.left && line.left.lineDraft.isDraft" class="content">
- <draft-note :draft="line.left.lineDraft" :line="line.left" />
- </div>
- </td>
- <td class="notes_line new"></td>
- <td class="notes-content parallel new" colspan="2">
- <div v-if="line.right && line.right.lineDraft.isDraft" class="content">
- <draft-note :draft="line.right.lineDraft" :line="line.right" />
- </div>
- </td>
- </tr>
- </template>
- </tbody>
- </table>
-</template>
diff --git a/app/assets/javascripts/diffs/components/pre_renderer.vue b/app/assets/javascripts/diffs/components/pre_renderer.vue
new file mode 100644
index 00000000000..c357aa2d924
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/pre_renderer.vue
@@ -0,0 +1,84 @@
+<script>
+export default {
+ inject: ['vscrollParent'],
+ props: {
+ maxLength: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ nextIndex: -1,
+ nextItem: null,
+ startedRender: false,
+ width: 0,
+ };
+ },
+ mounted() {
+ this.width = this.$el.parentNode.offsetWidth;
+ window.test = this;
+
+ this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
+ await this.$nextTick();
+
+ const nextItem = this.findNextToRender();
+
+ if (nextItem) {
+ this.startedRender = true;
+ requestIdleCallback(() => {
+ this.nextItem = nextItem;
+
+ if (this.nextIndex === this.maxLength - 1) {
+ this.$nextTick(() => {
+ if (this.vscrollParent.itemsWithSize[this.maxLength - 1].size !== 0) {
+ this.clearRendering();
+ }
+ });
+ }
+ });
+ } else if (this.startedRender) {
+ this.clearRendering();
+ }
+ });
+ },
+ beforeDestroy() {
+ this.$_itemsWithSizeWatcher();
+ },
+ methods: {
+ clearRendering() {
+ this.nextItem = null;
+
+ if (this.maxLength === this.vscrollParent.itemsWithSize.length) {
+ this.$_itemsWithSizeWatcher();
+ }
+ },
+ findNextToRender() {
+ return this.vscrollParent.itemsWithSize.find(({ size }, index) => {
+ const isNext = size === 0;
+
+ if (isNext) {
+ this.nextIndex = index;
+ }
+
+ return isNext;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="nextItem" :style="{ width: `${width}px` }" class="gl-absolute diff-file-offscreen">
+ <slot
+ v-bind="{ item: nextItem.item, index: nextIndex, active: true, itemWithSize: nextItem }"
+ ></slot>
+ </div>
+</template>
+
+<style scoped>
+.diff-file-offscreen {
+ top: -200%;
+ left: -200%;
+}
+</style>
diff --git a/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js b/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js
new file mode 100644
index 00000000000..984c6f8c0c9
--- /dev/null
+++ b/app/assets/javascripts/diffs/components/virtual_scroller_scroll_sync.js
@@ -0,0 +1,51 @@
+import { handleLocationHash } from '~/lib/utils/common_utils';
+
+export default {
+ inject: ['vscrollParent'],
+ props: {
+ index: {
+ type: Number,
+ required: true,
+ },
+ },
+ watch: {
+ index: {
+ handler() {
+ const { index } = this;
+
+ if (index < 0) return;
+
+ if (this.vscrollParent.itemsWithSize[index].size) {
+ this.scrollToIndex(index);
+ } else {
+ this.$_itemsWithSizeWatcher = this.$watch('vscrollParent.itemsWithSize', async () => {
+ await this.$nextTick();
+
+ if (this.vscrollParent.itemsWithSize[index].size) {
+ this.$_itemsWithSizeWatcher();
+ this.scrollToIndex(index);
+
+ await this.$nextTick();
+ }
+ });
+ }
+ },
+ immediate: true,
+ },
+ },
+ beforeDestroy() {
+ if (this.$_itemsWithSizeWatcher) this.$_itemsWithSizeWatcher();
+ },
+ methods: {
+ scrollToIndex(index) {
+ this.vscrollParent.scrollToItem(index);
+
+ setTimeout(() => {
+ handleLocationHash();
+ });
+ },
+ },
+ render(h) {
+ return h(null);
+ },
+};
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index d1e02fbc598..f1cf556fde0 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -59,7 +59,6 @@ export const MIN_RENDERING_MS = 2;
export const START_RENDERING_INDEX = 200;
export const INLINE_DIFF_LINES_KEY = 'highlighted_diff_lines';
export const PARALLEL_DIFF_LINES_KEY = 'parallel_diff_lines';
-export const DIFFS_PER_PAGE = 20;
export const DIFF_COMPARE_BASE_VERSION_INDEX = -1;
export const DIFF_COMPARE_HEAD_VERSION_INDEX = -2;
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 0ab72749760..ea83523008c 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -50,9 +50,6 @@ export default function initDiffsApp(store) {
click: this.openFile,
},
class: ['diff-file-finder'],
- style: {
- display: this.fileFinderVisible ? '' : 'none',
- },
});
},
});
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 2e94f147086..66510edf3db 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -25,7 +25,6 @@ import {
MIN_RENDERING_MS,
START_RENDERING_INDEX,
INLINE_DIFF_LINES_KEY,
- DIFFS_PER_PAGE,
DIFF_FILE_MANUAL_COLLAPSE,
DIFF_FILE_AUTOMATIC_COLLAPSE,
EVT_PERF_MARK_FILE_TREE_START,
@@ -92,22 +91,18 @@ export const setBaseConfig = ({ commit }, options) => {
};
export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
- const diffsGradualLoad = window.gon?.features?.diffsGradualLoad;
- let perPage = DIFFS_PER_PAGE;
+ let perPage = state.viewDiffsFileByFile ? 1 : 5;
let increaseAmount = 1.4;
-
- if (diffsGradualLoad) {
- perPage = state.viewDiffsFileByFile ? 1 : 5;
- }
-
- const startPage = diffsGradualLoad ? 0 : 1;
+ const startPage = 0;
const id = window?.location?.hash;
const isNoteLink = id.indexOf('#note') === 0;
const urlParams = {
w: state.showWhitespace ? '0' : '1',
view: 'inline',
};
+ const hash = window.location.hash.replace('#', '').split('diff-content-').pop();
let totalLoaded = 0;
+ let scrolledVirtualScroller = false;
commit(types.SET_BATCH_LOADING, true);
commit(types.SET_RETRIEVING_BATCHES, true);
@@ -122,6 +117,18 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files });
commit(types.SET_BATCH_LOADING, false);
+ if (window.gon?.features?.diffsVirtualScrolling && !scrolledVirtualScroller) {
+ const index = state.diffFiles.findIndex(
+ (f) =>
+ f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash),
+ );
+
+ if (index >= 0) {
+ eventHub.$emit('scrollToIndex', index);
+ scrolledVirtualScroller = true;
+ }
+ }
+
if (!isNoteLink && !state.currentDiffFileId) {
commit(types.VIEW_DIFF_FILE, diff_files[0].file_hash);
}
@@ -130,11 +137,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
dispatch('setCurrentDiffFileIdFromNote', id.split('_').pop());
}
- if (
- (diffsGradualLoad &&
- (totalLoaded === pagination.total_pages || pagination.total_pages === null)) ||
- (!diffsGradualLoad && !pagination.next_page)
- ) {
+ if (totalLoaded === pagination.total_pages || pagination.total_pages === null) {
commit(types.SET_RETRIEVING_BATCHES, false);
// We need to check that the currentDiffFileId points to a file that exists
@@ -164,15 +167,11 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
return null;
}
- if (diffsGradualLoad) {
- const nextPage = page + perPage;
- perPage = Math.min(Math.ceil(perPage * increaseAmount), 30);
- increaseAmount = Math.min(increaseAmount + 0.2, 2);
-
- return nextPage;
- }
+ const nextPage = page + perPage;
+ perPage = Math.min(Math.ceil(perPage * increaseAmount), 30);
+ increaseAmount = Math.min(increaseAmount + 0.2, 2);
- return pagination.next_page;
+ return nextPage;
})
.then((nextPage) => {
dispatch('startRenderDiffsQueue');
@@ -186,7 +185,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
.catch(() => commit(types.SET_RETRIEVING_BATCHES, false));
return getBatch()
- .then(handleLocationHash)
+ .then(() => !window.gon?.features?.diffsVirtualScrolling && handleLocationHash())
.catch(() => null);
};
@@ -250,6 +249,8 @@ export const setHighlightedRow = ({ commit }, lineCode) => {
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.VIEW_DIFF_FILE, fileHash);
+
+ handleLocationHash();
};
// This is adding line discussions to the actual lines in the diff tree
@@ -523,9 +524,18 @@ export const scrollToFile = ({ state, commit }, path) => {
if (!state.treeEntries[path]) return;
const { fileHash } = state.treeEntries[path];
- document.location.hash = fileHash;
commit(types.VIEW_DIFF_FILE, fileHash);
+
+ if (window.gon?.features?.diffsVirtualScrolling) {
+ eventHub.$emit('scrollToFileHash', fileHash);
+
+ setTimeout(() => {
+ window.history.replaceState(null, null, `#${fileHash}`);
+ });
+ } else {
+ document.location.hash = fileHash;
+ }
};
export const setShowTreeList = ({ commit }, { showTreeList, saving = true }) => {
@@ -570,7 +580,7 @@ export const setShowWhitespace = async (
{ state, commit },
{ url, showWhitespace, updateDatabase = true },
) => {
- if (updateDatabase) {
+ if (updateDatabase && Boolean(window.gon?.current_user_id)) {
await axios.put(url || state.endpointUpdateUser, { show_whitespace_in_diffs: showWhitespace });
}
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index a536db5c417..1b6a673925f 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -151,11 +151,7 @@ export const currentDiffIndex = (state) =>
state.diffFiles.findIndex((diff) => diff.file_hash === state.currentDiffFileId),
);
-export const diffLines = (state) => (file, unifiedDiffComponents) => {
- if (!unifiedDiffComponents && state.diffViewType === INLINE_DIFF_VIEW_TYPE) {
- return null;
- }
-
+export const diffLines = (state) => (file) => {
return parallelizeDiffLines(
file.highlighted_diff_lines || [],
state.diffViewType === INLINE_DIFF_VIEW_TYPE,
diff --git a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
index 673ec821b58..65ffd42fa27 100644
--- a/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
+++ b/app/assets/javascripts/diffs/store/getters_versions_dropdowns.js
@@ -1,4 +1,5 @@
-import { getParameterByName, parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import { DIFF_COMPARE_BASE_VERSION_INDEX, DIFF_COMPARE_HEAD_VERSION_INDEX } from '../constants';
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index 75d2cf43b94..3f1af68e37a 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -381,9 +381,15 @@ function prepareDiffFileLines(file) {
}
function finalizeDiffFile(file, index) {
+ let renderIt = Boolean(window.gon?.features?.diffsVirtualScrolling);
+
+ if (!window.gon?.features?.diffsVirtualScrolling) {
+ renderIt =
+ index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false;
+ }
+
Object.assign(file, {
- renderIt:
- index < 3 ? file[INLINE_DIFF_LINES_KEY].length < LINES_TO_BE_RENDERED_DIRECTLY : false,
+ renderIt,
isShowingFullFile: false,
isLoadingFullFile: false,
discussions: [],
diff --git a/app/assets/javascripts/editor/constants.js b/app/assets/javascripts/editor/constants.js
index c991316dda2..849ff91841a 100644
--- a/app/assets/javascripts/editor/constants.js
+++ b/app/assets/javascripts/editor/constants.js
@@ -1,14 +1,15 @@
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
-export const EDITOR_LITE_INSTANCE_ERROR_NO_EL = __(
+export const SOURCE_EDITOR_INSTANCE_ERROR_NO_EL = __(
'"el" parameter is required for createInstance()',
);
export const URI_PREFIX = 'gitlab';
-export const CONTENT_UPDATE_DEBOUNCE = 250;
+export const CONTENT_UPDATE_DEBOUNCE = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const ERROR_INSTANCE_REQUIRED_FOR_EXTENSION = __(
- 'Editor Lite instance is required to set up an extension.',
+ 'Source Editor instance is required to set up an extension.',
);
export const EDITOR_READY_EVENT = 'editor-ready';
diff --git a/app/assets/javascripts/editor/extensions/editor_file_template_ext.js b/app/assets/javascripts/editor/extensions/editor_file_template_ext.js
deleted file mode 100644
index f5474318447..00000000000
--- a/app/assets/javascripts/editor/extensions/editor_file_template_ext.js
+++ /dev/null
@@ -1,8 +0,0 @@
-import { Position } from 'monaco-editor';
-import { EditorLiteExtension } from './editor_lite_extension_base';
-
-export class FileTemplateExtension extends EditorLiteExtension {
- navigateFileStart() {
- this.setPosition(new Position(1, 1));
- }
-}
diff --git a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
index c5ee61ec86e..410aaed86a7 100644
--- a/app/assets/javascripts/editor/extensions/editor_ci_schema_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_ci_schema_ext.js
@@ -1,9 +1,9 @@
import Api from '~/api';
import { registerSchema } from '~/ide/utils';
import { EXTENSION_CI_SCHEMA_FILE_NAME_MATCH } from '../constants';
-import { EditorLiteExtension } from './editor_lite_extension_base';
+import { SourceEditorExtension } from './source_editor_extension_base';
-export class CiSchemaExtension extends EditorLiteExtension {
+export class CiSchemaExtension extends SourceEditorExtension {
/**
* Registers a syntax schema to the editor based on project
* identifier and commit.
diff --git a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
index 05a020bd958..5fa01f03f7e 100644
--- a/app/assets/javascripts/editor/extensions/editor_lite_extension_base.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_extension_base.js
@@ -16,15 +16,15 @@ const createAnchor = (href) => {
return fragment;
};
-export class EditorLiteExtension {
+export class SourceEditorExtension {
constructor({ instance, ...options } = {}) {
if (instance) {
Object.assign(instance, options);
- EditorLiteExtension.highlightLines(instance);
+ SourceEditorExtension.highlightLines(instance);
if (instance.getEditorType && instance.getEditorType() === EDITOR_TYPE_CODE) {
- EditorLiteExtension.setupLineLinking(instance);
+ SourceEditorExtension.setupLineLinking(instance);
}
- EditorLiteExtension.deferRerender(instance);
+ SourceEditorExtension.deferRerender(instance);
} else if (Object.entries(options).length) {
throw new Error(ERROR_INSTANCE_REQUIRED_FOR_EXTENSION);
}
@@ -79,7 +79,7 @@ export class EditorLiteExtension {
}
static setupLineLinking(instance) {
- instance.onMouseMove(EditorLiteExtension.onMouseMoveHandler);
+ instance.onMouseMove(SourceEditorExtension.onMouseMoveHandler);
instance.onMouseDown((e) => {
const isCorrectAnchor = e.target.element.classList.contains('link-anchor');
if (!isCorrectAnchor) {
diff --git a/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
new file mode 100644
index 00000000000..397e090ed30
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_file_template_ext.js
@@ -0,0 +1,8 @@
+import { Position } from 'monaco-editor';
+import { SourceEditorExtension } from './source_editor_extension_base';
+
+export class FileTemplateExtension extends SourceEditorExtension {
+ navigateFileStart() {
+ this.setPosition(new Position(1, 1));
+ }
+}
diff --git a/app/assets/javascripts/editor/extensions/editor_markdown_ext.js b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
index 2ce003753f7..997503a12f5 100644
--- a/app/assets/javascripts/editor/extensions/editor_markdown_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_markdown_ext.js
@@ -1,6 +1,6 @@
-import { EditorLiteExtension } from './editor_lite_extension_base';
+import { SourceEditorExtension } from './source_editor_extension_base';
-export class EditorMarkdownExtension extends EditorLiteExtension {
+export class EditorMarkdownExtension extends SourceEditorExtension {
getSelectedText(selection = this.getSelection()) {
const { startLineNumber, endLineNumber, startColumn, endColumn } = selection;
const valArray = this.getValue().split('\n');
diff --git a/app/assets/javascripts/editor/extensions/editor_lite_webide_ext.js b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
index 83b0386d470..98e05489c1c 100644
--- a/app/assets/javascripts/editor/extensions/editor_lite_webide_ext.js
+++ b/app/assets/javascripts/editor/extensions/source_editor_webide_ext.js
@@ -1,7 +1,7 @@
import { debounce } from 'lodash';
import { KeyCode, KeyMod, Range } from 'monaco-editor';
import { EDITOR_TYPE_DIFF } from '~/editor/constants';
-import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
+import { SourceEditorExtension } from '~/editor/extensions/source_editor_extension_base';
import Disposable from '~/ide/lib/common/disposable';
import { editorOptions } from '~/ide/lib/editor_options';
import keymap from '~/ide/lib/keymap.json';
@@ -12,7 +12,7 @@ const isDiffEditorType = (instance) => {
export const UPDATE_DIMENSIONS_DELAY = 200;
-export class EditorWebIdeExtension extends EditorLiteExtension {
+export class EditorWebIdeExtension extends SourceEditorExtension {
constructor({ instance, modelManager, ...options } = {}) {
super({
instance,
diff --git a/app/assets/javascripts/editor/editor_lite.js b/app/assets/javascripts/editor/source_editor.js
index 249888ede9b..ee97714824e 100644
--- a/app/assets/javascripts/editor/editor_lite.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -6,23 +6,23 @@ import { registerLanguages } from '~/ide/utils';
import { joinPaths } from '~/lib/utils/url_utility';
import { uuids } from '~/lib/utils/uuids';
import {
- EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ SOURCE_EDITOR_INSTANCE_ERROR_NO_EL,
URI_PREFIX,
EDITOR_READY_EVENT,
EDITOR_TYPE_DIFF,
} from './constants';
import { clearDomElement } from './utils';
-export default class EditorLite {
+export default class SourceEditor {
constructor(options = {}) {
this.instances = [];
this.options = {
- extraEditorClassName: 'gl-editor-lite',
+ extraEditorClassName: 'gl-source-editor',
...defaultEditorOptions,
...options,
};
- EditorLite.setupMonacoTheme();
+ SourceEditor.setupMonacoTheme();
registerLanguages(...languages);
}
@@ -56,7 +56,7 @@ export default class EditorLite {
extensionsArray.forEach((ext) => {
const prefix = ext.includes('/') ? '' : 'editor/';
const trimmedExt = ext.replace(/^\//, '').trim();
- EditorLite.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
+ SourceEditor.pushToImportsArray(promises, `~/${prefix}${trimmedExt}`);
});
return Promise.all(promises);
@@ -77,7 +77,7 @@ export default class EditorLite {
static prepareInstance(el) {
if (!el) {
- throw new Error(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
+ throw new Error(SOURCE_EDITOR_INSTANCE_ERROR_NO_EL);
}
clearDomElement(el);
@@ -88,7 +88,7 @@ export default class EditorLite {
}
static manageDefaultExtensions(instance, el, extensions) {
- EditorLite.loadExtensions(extensions, instance)
+ SourceEditor.loadExtensions(extensions, instance)
.then((modules) => {
if (modules) {
modules.forEach((module) => {
@@ -126,7 +126,7 @@ export default class EditorLite {
const diffModel = {
original: monacoEditor.createModel(
blobOriginalContent,
- EditorLite.getModelLanguage(model.uri.path),
+ SourceEditor.getModelLanguage(model.uri.path),
),
modified: model,
};
@@ -135,18 +135,18 @@ export default class EditorLite {
}
static convertMonacoToELInstance = (inst) => {
- const editorLiteInstanceAPI = {
+ const sourceEditorInstanceAPI = {
updateModelLanguage: (path) => {
- return EditorLite.instanceUpdateLanguage(inst, path);
+ return SourceEditor.instanceUpdateLanguage(inst, path);
},
use: (exts = []) => {
- return EditorLite.instanceApplyExtension(inst, exts);
+ return SourceEditor.instanceApplyExtension(inst, exts);
},
};
const handler = {
get(target, prop, receiver) {
- if (Reflect.has(editorLiteInstanceAPI, prop)) {
- return editorLiteInstanceAPI[prop];
+ if (Reflect.has(sourceEditorInstanceAPI, prop)) {
+ return sourceEditorInstanceAPI[prop];
}
return Reflect.get(target, prop, receiver);
},
@@ -155,7 +155,7 @@ export default class EditorLite {
};
static instanceUpdateLanguage(inst, path) {
- const lang = EditorLite.getModelLanguage(path);
+ const lang = SourceEditor.getModelLanguage(path);
const model = inst.getModel();
return monacoEditor.setModelLanguage(model, lang);
}
@@ -163,7 +163,7 @@ export default class EditorLite {
static instanceApplyExtension(inst, exts = []) {
const extensions = [].concat(exts);
extensions.forEach((extension) => {
- EditorLite.mixIntoInstance(extension, inst);
+ SourceEditor.mixIntoInstance(extension, inst);
});
return inst;
}
@@ -210,10 +210,10 @@ export default class EditorLite {
isDiff = false,
...instanceOptions
} = {}) {
- EditorLite.prepareInstance(el);
+ SourceEditor.prepareInstance(el);
const createEditorFn = isDiff ? 'createDiffEditor' : 'create';
- const instance = EditorLite.convertMonacoToELInstance(
+ const instance = SourceEditor.convertMonacoToELInstance(
monacoEditor[createEditorFn].call(this, el, {
...this.options,
...instanceOptions,
@@ -222,7 +222,7 @@ export default class EditorLite {
let model;
if (instanceOptions.model !== null) {
- model = EditorLite.createEditorModel({
+ model = SourceEditor.createEditorModel({
blobGlobalId,
blobOriginalContent,
blobPath,
@@ -233,11 +233,11 @@ export default class EditorLite {
}
instance.onDidDispose(() => {
- EditorLite.instanceRemoveFromRegistry(this, instance);
- EditorLite.instanceDisposeModels(this, instance, model);
+ SourceEditor.instanceRemoveFromRegistry(this, instance);
+ SourceEditor.instanceDisposeModels(this, instance, model);
});
- EditorLite.manageDefaultExtensions(instance, el, extensions);
+ SourceEditor.manageDefaultExtensions(instance, el, extensions);
this.instances.push(instance);
return instance;
diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue
index 539cd6963b1..4f4c32af113 100644
--- a/app/assets/javascripts/emoji/components/emoji_group.vue
+++ b/app/assets/javascripts/emoji/components/emoji_group.vue
@@ -17,6 +17,7 @@ export default {
};
</script>
+<!-- eslint-disable-next-line vue/no-deprecated-functional-template -->
<template functional>
<div class="gl-display-flex gl-flex-wrap gl-mb-2">
<template v-if="props.renderGroup">
diff --git a/app/assets/javascripts/environments/components/deploy_board.vue b/app/assets/javascripts/environments/components/deploy_board.vue
index 217cea051b7..c642a07fd1e 100644
--- a/app/assets/javascripts/environments/components/deploy_board.vue
+++ b/app/assets/javascripts/environments/components/deploy_board.vue
@@ -111,7 +111,7 @@ export default {
</script>
<template>
<div class="js-deploy-board deploy-board">
- <gl-loading-icon v-if="isLoading" class="loading-icon" />
+ <gl-loading-icon v-if="isLoading" size="sm" class="loading-icon" />
<template v-else>
<div v-if="canRenderDeployBoard" class="deploy-board-information gl-p-5">
<div class="deploy-board-information gl-w-full">
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 542b8c9219d..2d98f00433a 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -80,7 +80,7 @@ export default {
<template #button-content>
<gl-icon name="play" />
<gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
</template>
<gl-dropdown-item
v-for="(action, i) in actions"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 4db0dff16aa..5ae8b000fc0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -552,6 +552,9 @@ export default {
{ 'gl-display-none gl-md-display-block': !this.upcomingDeployment },
];
},
+ tableNameSpacingClass() {
+ return this.isFolder ? 'section-100' : this.tableData.name.spacing;
+ },
},
methods: {
@@ -588,8 +591,9 @@ export default {
>
<div
class="table-section section-wrap text-truncate"
- :class="tableData.name.spacing"
+ :class="tableNameSpacingClass"
role="gridcell"
+ data-testid="environment-name-cell"
>
<div v-if="!isFolder" class="table-mobile-header" role="rowheader">
{{ getMobileViewTitleForField('name') }}
@@ -632,9 +636,11 @@ export default {
</div>
<div
+ v-if="!isFolder"
class="table-section deployment-column d-none d-md-block"
:class="tableData.deploy.spacing"
role="gridcell"
+ data-testid="enviornment-deployment-id-cell"
>
<span v-if="shouldRenderDeploymentID" class="text-break-word">
{{ deploymentInternalId }}
@@ -656,7 +662,13 @@ export default {
</div>
</div>
- <div class="table-section d-none d-md-block" :class="tableData.build.spacing" role="gridcell">
+ <div
+ v-if="!isFolder"
+ class="table-section d-none d-md-block"
+ :class="tableData.build.spacing"
+ role="gridcell"
+ data-testid="environment-build-cell"
+ >
<a v-if="shouldRenderBuildName" :href="buildPath" class="build-link cgray">
<tooltip-on-truncate
:title="buildName"
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 8bd71db957c..e4cf5760987 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -1,6 +1,6 @@
<script>
import { GlBadge, GlButton, GlModalDirective, GlTab, GlTabs } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
import environmentsMixin from '../mixins/environments_mixin';
@@ -89,7 +89,9 @@ export default {
.then((response) => this.store.setfolderContent(folder, response.data.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
.catch(() => {
- Flash(s__('Environments|An error occurred while fetching the environments.'));
+ createFlash({
+ message: s__('Environments|An error occurred while fetching the environments.'),
+ });
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false);
});
},
@@ -133,7 +135,7 @@ export default {
>{{ $options.i18n.newEnvironmentButtonLabel }}</gl-button
>
</div>
- <gl-tabs content-class="gl-display-none">
+ <gl-tabs :value="activeTab" content-class="gl-display-none">
<gl-tab
v-for="(tab, idx) in tabs"
:key="idx"
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index f82d3065ca5..61438872afc 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -152,8 +152,7 @@ export default {
</div>
</div>
<template v-for="(model, i) in sortedEnvironments" :model="model">
- <div
- is="environment-item"
+ <environment-item
:key="`environment-item-${i}`"
:model="model"
:can-read-environment="canReadEnvironment"
@@ -189,8 +188,7 @@ export default {
<template v-else>
<template v-for="(child, index) in model.children">
- <div
- is="environment-item"
+ <environment-item
:key="`environment-row-${i}-${index}`"
:model="child"
:can-read-environment="canReadEnvironment"
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
index d5caff1660a..6f701f87261 100644
--- a/app/assets/javascripts/environments/mixins/environments_mixin.js
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -3,9 +3,9 @@
*/
import { isEqual, isFunction, omitBy } from 'lodash';
import Visibility from 'visibilityjs';
-import { deprecatedCreateFlash as Flash } from '../../flash';
-import { getParameterByName } from '../../lib/utils/common_utils';
+import createFlash from '~/flash';
import Poll from '../../lib/utils/poll';
+import { getParameterByName } from '../../lib/utils/url_utility';
import { s__ } from '../../locale';
import tabs from '../../vue_shared/components/navigation_tabs.vue';
import tablePagination from '../../vue_shared/components/pagination/table_pagination.vue';
@@ -94,7 +94,9 @@ export default {
errorCallback() {
this.isLoading = false;
- Flash(s__('Environments|An error occurred while fetching the environments.'));
+ createFlash({
+ message: s__('Environments|An error occurred while fetching the environments.'),
+ });
},
postAction({
@@ -109,7 +111,9 @@ export default {
.then(() => this.fetchEnvironments())
.catch((err) => {
this.isLoading = false;
- Flash(isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage);
+ createFlash({
+ message: isFunction(errorMessage) ? errorMessage(err.response.data) : errorMessage,
+ });
});
}
},
@@ -163,7 +167,9 @@ export default {
window.location.href = url.join('/');
})
.catch(() => {
- Flash(errorMessage);
+ createFlash({
+ message: errorMessage,
+ });
});
},
@@ -202,6 +208,9 @@ export default {
},
];
},
+ activeTab() {
+ return this.tabs.findIndex(({ isActive }) => isActive) ?? 0;
+ },
},
/**
diff --git a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
index dd320676e98..68b4438831e 100644
--- a/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
+++ b/app/assets/javascripts/error_tracking/components/stacktrace_entry.vue
@@ -82,7 +82,7 @@ export default {
<div v-if="hasCode" class="d-inline-block cursor-pointer" @click="toggle()">
<gl-icon :name="collapseIcon" :size="16" class="gl-mr-2" />
</div>
- <file-icon :file-name="filePath" :size="18" aria-hidden="true" css-classes="gl-mr-2" />
+ <file-icon :file-name="filePath" :size="16" aria-hidden="true" css-classes="gl-mr-2" />
<strong
v-gl-tooltip
:title="filePath"
diff --git a/app/assets/javascripts/error_tracking_settings/store/actions.js b/app/assets/javascripts/error_tracking_settings/store/actions.js
index c945a9e2316..d402d0336d9 100644
--- a/app/assets/javascripts/error_tracking_settings/store/actions.js
+++ b/app/assets/javascripts/error_tracking_settings/store/actions.js
@@ -48,7 +48,6 @@ export const receiveSettingsError = ({ commit }, { response = {} }) => {
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
- type: 'alert',
});
commit(types.UPDATE_SETTINGS_LOADING, false);
};
diff --git a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
index 77e40039b43..d86e13ce722 100644
--- a/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
+++ b/app/assets/javascripts/feature_flags/components/configure_feature_flags_modal.vue
@@ -196,6 +196,7 @@ export default {
/>
<gl-loading-icon
v-if="isRotating"
+ size="sm"
class="gl-absolute gl-align-self-center gl-right-5 gl-mr-7"
/>
diff --git a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
index e7f4b51c964..dde021b67be 100644
--- a/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/edit_feature_flag.vue
@@ -1,10 +1,8 @@
<script>
import { GlAlert, GlLoadingIcon, GlToggle } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import axios from '~/lib/utils/axios_utils';
import { sprintf, s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { LEGACY_FLAG } from '../constants';
import FeatureFlagForm from './form.vue';
export default {
@@ -15,59 +13,29 @@ export default {
FeatureFlagForm,
},
mixins: [glFeatureFlagMixin()],
- inject: {
- showUserCallout: {},
- userCalloutId: {
- default: '',
- },
- userCalloutsPath: {
- default: '',
- },
- },
- data() {
- return {
- userShouldSeeNewFlagAlert: this.showUserCallout,
- };
- },
- translations: {
- legacyReadOnlyFlagAlert: s__(
- 'FeatureFlags|GitLab is moving to a new way of managing feature flags. This feature flag is read-only, and it will be removed in 14.0. Please create a new feature flag.',
- ),
- },
computed: {
...mapState([
'path',
'error',
'name',
'description',
- 'scopes',
'strategies',
'isLoading',
'hasError',
'iid',
'active',
- 'version',
]),
title() {
return this.iid
? `^${this.iid} ${this.name}`
: sprintf(s__('Edit %{name}'), { name: this.name });
},
- deprecated() {
- return this.version === LEGACY_FLAG;
- },
},
created() {
return this.fetchFeatureFlag();
},
methods: {
...mapActions(['updateFeatureFlag', 'fetchFeatureFlag', 'toggleActive']),
- dismissNewVersionFlagAlert() {
- this.userShouldSeeNewFlagAlert = false;
- axios.post(this.userCalloutsPath, {
- feature_name: this.userCalloutId,
- });
- },
},
};
</script>
@@ -76,9 +44,6 @@ export default {
<gl-loading-icon v-if="isLoading" size="xl" class="gl-mt-7" />
<template v-else-if="!isLoading && !hasError">
- <gl-alert v-if="deprecated" variant="warning" :dismissible="false" class="gl-my-5">{{
- $options.translations.legacyReadOnlyFlagAlert
- }}</gl-alert>
<div class="gl-display-flex gl-align-items-center gl-mb-4 gl-mt-4">
<gl-toggle
:value="active"
@@ -100,12 +65,10 @@ export default {
<feature-flag-form
:name="name"
:description="description"
- :scopes="scopes"
:strategies="strategies"
:cancel-path="path"
:submit-text="__('Save changes')"
:active="active"
- :version="version"
@handleSubmit="(data) => updateFeatureFlag(data)"
/>
</template>
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index d08e8d2b3a1..53909dcf42e 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -3,11 +3,8 @@ import { GlAlert, GlBadge, GlButton, GlModalDirective, GlSprintf } from '@gitlab
import { isEmpty } from 'lodash';
import { mapState, mapActions } from 'vuex';
-import {
- buildUrlWithCurrentLocation,
- getParameterByName,
- historyPushState,
-} from '~/lib/utils/common_utils';
+import { buildUrlWithCurrentLocation, historyPushState } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import ConfigureFeatureFlagsModal from './configure_feature_flags_modal.vue';
import EmptyState from './empty_state.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 9220077af71..cfd838bf5a1 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -1,8 +1,7 @@
<script>
-import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle, GlIcon } from '@gitlab/ui';
+import { GlBadge, GlButton, GlTooltipDirective, GlModal, GlToggle } from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { ROLLOUT_STRATEGY_PERCENT_ROLLOUT, NEW_VERSION_FLAG, LEGACY_FLAG } from '../constants';
import { labelForStrategy } from '../utils';
export default {
@@ -14,7 +13,6 @@ export default {
components: {
GlBadge,
GlButton,
- GlIcon,
GlModal,
GlToggle,
},
@@ -35,13 +33,7 @@ export default {
deleteFeatureFlagName: null,
};
},
- translations: {
- legacyFlagReadOnlyAlert: s__('FeatureFlags|Flag is read-only'),
- },
computed: {
- permissions() {
- return this.glFeatures.featureFlagPermissions;
- },
modalTitle() {
return sprintf(s__('FeatureFlags|Delete %{name}?'), {
name: this.deleteFeatureFlagName,
@@ -57,12 +49,6 @@ export default {
},
},
methods: {
- isLegacyFlag(flag) {
- return flag.version !== NEW_VERSION_FLAG;
- },
- statusToggleDisabled(flag) {
- return flag.version === LEGACY_FLAG;
- },
scopeTooltipText(scope) {
return !scope.active
? sprintf(s__('FeatureFlags|Inactive flag for %{scope}'), {
@@ -70,22 +56,6 @@ export default {
})
: '';
},
- badgeText(scope) {
- const displayName =
- scope.environmentScope === '*'
- ? s__('FeatureFlags|* (All environments)')
- : scope.environmentScope;
-
- const displayPercentage =
- scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT
- ? `: ${scope.rolloutPercentage}%`
- : '';
-
- return `${displayName}${displayPercentage}`;
- },
- badgeVariant(scope) {
- return scope.active ? 'info' : 'muted';
- },
strategyBadgeText(strategy) {
return labelForStrategy(strategy);
},
@@ -142,7 +112,6 @@ export default {
<gl-toggle
v-if="featureFlag.update_path"
:value="featureFlag.active"
- :disabled="statusToggleDisabled(featureFlag)"
:label="$options.i18n.toggleLabel"
label-position="hidden"
data-testid="feature-flag-status-toggle"
@@ -169,12 +138,6 @@ export default {
<div class="feature-flag-name text-monospace text-truncate">
{{ featureFlag.name }}
</div>
- <gl-icon
- v-if="isLegacyFlag(featureFlag)"
- v-gl-tooltip.hover="$options.translations.legacyFlagReadOnlyAlert"
- class="gl-ml-3"
- name="information-o"
- />
</div>
<div class="feature-flag-description text-secondary text-truncate">
{{ featureFlag.description }}
@@ -189,27 +152,14 @@ export default {
<div
class="table-mobile-content d-flex flex-wrap justify-content-end justify-content-md-start js-feature-flag-environments"
>
- <template v-if="isLegacyFlag(featureFlag)">
- <gl-badge
- v-for="scope in featureFlag.scopes"
- :key="scope.id"
- v-gl-tooltip.hover="scopeTooltipText(scope)"
- :variant="badgeVariant(scope)"
- :data-qa-selector="`feature-flag-scope-${badgeVariant(scope)}-badge`"
- class="gl-mr-3 gl-mt-2"
- >{{ badgeText(scope) }}</gl-badge
- >
- </template>
- <template v-else>
- <gl-badge
- 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
- >
- </template>
+ <gl-badge
+ 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
+ >
</div>
</div>
diff --git a/app/assets/javascripts/feature_flags/components/form.vue b/app/assets/javascripts/feature_flags/components/form.vue
index 67ddceaf080..f7ad2c1f106 100644
--- a/app/assets/javascripts/feature_flags/components/form.vue
+++ b/app/assets/javascripts/feature_flags/components/form.vue
@@ -1,16 +1,6 @@
<script>
-import {
- GlButton,
- GlBadge,
- GlTooltip,
- GlTooltipDirective,
- GlFormTextarea,
- GlFormCheckbox,
- GlSprintf,
- GlIcon,
- GlToggle,
-} from '@gitlab/ui';
-import { memoize, isString, cloneDeep, isNumber, uniqueId } from 'lodash';
+import { GlButton } from '@gitlab/ui';
+import { memoize, cloneDeep, isNumber, uniqueId } from 'lodash';
import Vue from 'vue';
import { s__ } from '~/locale';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
@@ -20,12 +10,8 @@ import {
ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
ROLLOUT_STRATEGY_USER_ID,
ALL_ENVIRONMENTS_NAME,
- INTERNAL_ID_PREFIX,
NEW_VERSION_FLAG,
- LEGACY_FLAG,
} from '../constants';
-import { createNewEnvironmentScope } from '../store/helpers';
-import EnvironmentsDropdown from './environments_dropdown.vue';
import Strategy from './strategy.vue';
export default {
@@ -35,20 +21,9 @@ export default {
},
components: {
GlButton,
- GlBadge,
- GlFormTextarea,
- GlFormCheckbox,
- GlTooltip,
- GlSprintf,
- GlIcon,
- GlToggle,
- EnvironmentsDropdown,
Strategy,
RelatedIssuesRoot,
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
mixins: [featureFlagsMixin()],
inject: {
featureFlagIssuesEndpoint: {
@@ -71,11 +46,6 @@ export default {
required: false,
default: '',
},
- scopes: {
- type: Array,
- required: false,
- default: () => [],
- },
cancelPath: {
type: String,
required: true,
@@ -89,11 +59,6 @@ export default {
required: false,
default: () => [],
},
- version: {
- type: String,
- required: false,
- default: LEGACY_FLAG,
- },
},
translations: {
allEnvironmentsText: s__('FeatureFlags|* (All Environments)'),
@@ -120,35 +85,18 @@ export default {
formName: this.name,
formDescription: this.description,
- // operate on a clone to avoid mutating props
- formScopes: this.scopes.map((s) => ({ ...s })),
formStrategies: cloneDeep(this.strategies),
newScope: '',
};
},
computed: {
- filteredScopes() {
- return this.formScopes.filter((scope) => !scope.shouldBeDestroyed);
- },
filteredStrategies() {
return this.formStrategies.filter((s) => !s.shouldBeDestroyed);
},
- canUpdateFlag() {
- return !this.permissionsFlag || (this.formScopes || []).every((scope) => scope.canUpdate);
- },
- permissionsFlag() {
- return this.glFeatures.featureFlagPermissions;
- },
- supportsStrategies() {
- return this.version === NEW_VERSION_FLAG;
- },
showRelatedIssues() {
return this.featureFlagIssuesEndpoint.length > 0;
},
- readOnly() {
- return this.version === LEGACY_FLAG;
- },
},
methods: {
keyFor(strategy) {
@@ -174,37 +122,6 @@ export default {
isAllEnvironment(name) {
return name === ALL_ENVIRONMENTS_NAME;
},
-
- /**
- * When the user clicks the remove button we delete the scope
- *
- * If the scope has an ID, we need to add the `shouldBeDestroyed` flag.
- * If the scope does *not* have an ID, we can just remove it.
- *
- * This flag will be used when submitting the data to the backend
- * to determine which records to delete (via a "_destroy" property).
- *
- * @param {Object} scope
- */
- removeScope(scope) {
- if (isString(scope.id) && scope.id.startsWith(INTERNAL_ID_PREFIX)) {
- this.formScopes = this.formScopes.filter((s) => s !== scope);
- } else {
- Vue.set(scope, 'shouldBeDestroyed', true);
- }
- },
-
- /**
- * Creates a new scope and adds it to the list of scopes
- *
- * @param overrides An object whose properties will
- * be used override the default scope options
- */
- createNewScope(overrides) {
- this.formScopes.push(createNewEnvironmentScope(overrides, this.permissionsFlag));
- this.newScope = '';
- },
-
/**
* When the user clicks the submit button
* it triggers an event with the form data
@@ -214,61 +131,16 @@ export default {
name: this.formName,
description: this.formDescription,
active: this.active,
- version: this.version,
+ version: NEW_VERSION_FLAG,
+ strategies: this.formStrategies,
};
- if (this.version === LEGACY_FLAG) {
- flag.scopes = this.formScopes;
- } else {
- flag.strategies = this.formStrategies;
- }
-
this.$emit('handleSubmit', flag);
},
- canUpdateScope(scope) {
- return !this.permissionsFlag || scope.canUpdate;
- },
-
isRolloutPercentageInvalid: memoize(function isRolloutPercentageInvalid(percentage) {
return !this.$options.rolloutPercentageRegex.test(percentage);
}),
-
- /**
- * Generates a unique ID for the strategy based on the v-for index
- *
- * @param index The index of the strategy
- */
- rolloutStrategyId(index) {
- return `rollout-strategy-${index}`;
- },
-
- /**
- * Generates a unique ID for the percentage based on the v-for index
- *
- * @param index The index of the percentage
- */
- rolloutPercentageId(index) {
- return `rollout-percentage-${index}`;
- },
- rolloutUserId(index) {
- return `rollout-user-id-${index}`;
- },
-
- shouldDisplayIncludeUserIds(scope) {
- return ![ROLLOUT_STRATEGY_ALL_USERS, ROLLOUT_STRATEGY_USER_ID].includes(
- scope.rolloutStrategy,
- );
- },
- shouldDisplayUserIds(scope) {
- return scope.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID || scope.shouldIncludeUserIds;
- },
- onStrategyChange(index) {
- const scope = this.filteredScopes[index];
- scope.shouldIncludeUserIds =
- scope.rolloutUserIds.length > 0 &&
- scope.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT;
- },
onFormStrategyChange(strategy, index) {
Object.assign(this.filteredStrategies[index], strategy);
},
@@ -281,12 +153,7 @@ export default {
<div class="row">
<div class="form-group col-md-4">
<label for="feature-flag-name" class="label-bold">{{ s__('FeatureFlags|Name') }} *</label>
- <input
- id="feature-flag-name"
- v-model="formName"
- :disabled="!canUpdateFlag"
- class="form-control"
- />
+ <input id="feature-flag-name" v-model="formName" class="form-control" />
</div>
</div>
@@ -298,7 +165,6 @@ export default {
<textarea
id="feature-flag-description"
v-model="formDescription"
- :disabled="!canUpdateFlag"
class="form-control"
rows="4"
></textarea>
@@ -312,277 +178,35 @@ export default {
:show-categorized-issues="false"
/>
- <template v-if="supportsStrategies">
- <div class="row">
- <div class="col-md-12">
- <h4>{{ s__('FeatureFlags|Strategies') }}</h4>
- <div class="flex align-items-baseline justify-content-between">
- <p class="mr-3">{{ $options.translations.newHelpText }}</p>
- <gl-button variant="confirm" category="secondary" @click="addStrategy">
- {{ s__('FeatureFlags|Add strategy') }}
- </gl-button>
- </div>
- </div>
- </div>
- <div v-if="filteredStrategies.length > 0" data-testid="feature-flag-strategies">
- <strategy
- v-for="(strategy, index) in filteredStrategies"
- :key="keyFor(strategy)"
- :strategy="strategy"
- :index="index"
- @change="onFormStrategyChange($event, index)"
- @delete="deleteStrategy(strategy)"
- />
- </div>
- <div v-else class="flex justify-content-center border-top py-4 w-100">
- <span>{{ $options.translations.noStrategiesText }}</span>
- </div>
- </template>
-
- <div v-else class="row">
- <div class="form-group col-md-12">
- <h4>{{ s__('FeatureFlags|Target environments') }}</h4>
- <gl-sprintf :message="$options.translations.helpText">
- <template #code="{ content }">
- <code>{{ content }}</code>
- </template>
- <template #bold="{ content }">
- <b>{{ content }}</b>
- </template>
- </gl-sprintf>
-
- <div class="js-scopes-table gl-mt-3">
- <div class="gl-responsive-table-row table-row-header" role="row">
- <div class="table-section section-30" role="columnheader">
- {{ s__('FeatureFlags|Environment Spec') }}
- </div>
- <div class="table-section section-20 text-center" role="columnheader">
- {{ s__('FeatureFlags|Status') }}
- </div>
- <div class="table-section section-40" role="columnheader">
- {{ s__('FeatureFlags|Rollout Strategy') }}
- </div>
- </div>
-
- <div
- v-for="(scope, index) in filteredScopes"
- :key="scope.id"
- ref="scopeRow"
- class="gl-responsive-table-row"
- role="row"
- >
- <div class="table-section section-30" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Environment Spec') }}
- </div>
- <div
- class="table-mobile-content gl-display-flex gl-align-items-center gl-justify-content-start"
- >
- <p v-if="isAllEnvironment(scope.environmentScope)" class="js-scope-all pl-3">
- {{ $options.translations.allEnvironmentsText }}
- </p>
-
- <environments-dropdown
- v-else
- class="col-12"
- :value="scope.environmentScope"
- :disabled="!canUpdateScope(scope) || scope.environmentScope !== ''"
- @selectEnvironment="(env) => (scope.environmentScope = env)"
- @createClicked="(env) => (scope.environmentScope = env)"
- @clearInput="(env) => (scope.environmentScope = '')"
- />
-
- <gl-badge v-if="permissionsFlag && scope.protected" variant="success">
- {{ s__('FeatureFlags|Protected') }}
- </gl-badge>
- </div>
- </div>
-
- <div class="table-section section-20 text-center" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ $options.i18n.statusLabel }}
- </div>
- <div class="table-mobile-content gl-display-flex gl-justify-content-center">
- <gl-toggle
- :value="scope.active"
- :disabled="!active || !canUpdateScope(scope)"
- :label="$options.i18n.statusLabel"
- label-position="hidden"
- @change="(status) => (scope.active = status)"
- />
- </div>
- </div>
-
- <div class="table-section section-40" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Rollout Strategy') }}
- </div>
- <div class="table-mobile-content js-rollout-strategy form-inline">
- <label class="sr-only" :for="rolloutStrategyId(index)">
- {{ s__('FeatureFlags|Rollout Strategy') }}
- </label>
- <div class="select-wrapper col-12 col-md-8 p-0">
- <select
- :id="rolloutStrategyId(index)"
- v-model="scope.rolloutStrategy"
- :disabled="!scope.active"
- class="form-control select-control w-100 js-rollout-strategy"
- @change="onStrategyChange(index)"
- >
- <option :value="$options.ROLLOUT_STRATEGY_ALL_USERS">
- {{ s__('FeatureFlags|All users') }}
- </option>
- <option :value="$options.ROLLOUT_STRATEGY_PERCENT_ROLLOUT">
- {{ s__('FeatureFlags|Percent rollout (logged in users)') }}
- </option>
- <option :value="$options.ROLLOUT_STRATEGY_USER_ID">
- {{ s__('FeatureFlags|User IDs') }}
- </option>
- </select>
- <gl-icon
- name="chevron-down"
- class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
- :size="16"
- />
- </div>
-
- <div
- v-if="scope.rolloutStrategy === $options.ROLLOUT_STRATEGY_PERCENT_ROLLOUT"
- class="d-flex-center mt-2 mt-md-0 ml-md-2"
- >
- <label class="sr-only" :for="rolloutPercentageId(index)">
- {{ s__('FeatureFlags|Rollout Percentage') }}
- </label>
- <div class="gl-w-9">
- <input
- :id="rolloutPercentageId(index)"
- v-model="scope.rolloutPercentage"
- :disabled="!scope.active"
- :class="{
- 'is-invalid': isRolloutPercentageInvalid(scope.rolloutPercentage),
- }"
- type="number"
- min="0"
- max="100"
- :pattern="$options.rolloutPercentageRegex.source"
- class="rollout-percentage js-rollout-percentage form-control text-right w-100"
- />
- </div>
- <gl-tooltip
- v-if="isRolloutPercentageInvalid(scope.rolloutPercentage)"
- :target="rolloutPercentageId(index)"
- >
- {{
- s__(
- 'FeatureFlags|Percent rollout must be an integer number between 0 and 100',
- )
- }}
- </gl-tooltip>
- <span class="ml-1">%</span>
- </div>
- <div class="d-flex flex-column align-items-start mt-2 w-100">
- <gl-form-checkbox
- v-if="shouldDisplayIncludeUserIds(scope)"
- v-model="scope.shouldIncludeUserIds"
- >{{ s__('FeatureFlags|Include additional user IDs') }}</gl-form-checkbox
- >
- <template v-if="shouldDisplayUserIds(scope)">
- <label :for="rolloutUserId(index)" class="mb-2">
- {{ s__('FeatureFlags|User IDs') }}
- </label>
- <gl-form-textarea
- :id="rolloutUserId(index)"
- v-model="scope.rolloutUserIds"
- class="w-100"
- />
- </template>
- </div>
- </div>
- </div>
-
- <div class="table-section section-10 text-right" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Remove') }}
- </div>
- <div class="table-mobile-content">
- <gl-button
- v-if="!isAllEnvironment(scope.environmentScope) && canUpdateScope(scope)"
- v-gl-tooltip
- :title="$options.i18n.removeLabel"
- :aria-label="$options.i18n.removeLabel"
- class="js-delete-scope btn-transparent pr-3 pl-3"
- icon="clear"
- data-testid="feature-flag-delete"
- @click="removeScope(scope)"
- />
- </div>
- </div>
- </div>
-
- <div class="gl-responsive-table-row" role="row" data-testid="add-new-scope">
- <div class="table-section section-30" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Environment Spec') }}
- </div>
- <div class="table-mobile-content">
- <environments-dropdown
- class="js-new-scope-name col-12"
- :value="newScope"
- @selectEnvironment="(env) => createNewScope({ environmentScope: env })"
- @createClicked="(env) => createNewScope({ environmentScope: env })"
- />
- </div>
- </div>
-
- <div class="table-section section-20 text-center" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ $options.i18n.statusLabel }}
- </div>
- <div class="table-mobile-content gl-display-flex gl-justify-content-center">
- <gl-toggle
- :disabled="!active"
- :label="$options.i18n.statusLabel"
- label-position="hidden"
- :value="false"
- @change="createNewScope({ active: true })"
- />
- </div>
- </div>
-
- <div class="table-section section-40" role="gridcell">
- <div class="table-mobile-header" role="rowheader">
- {{ s__('FeatureFlags|Rollout Strategy') }}
- </div>
- <div class="table-mobile-content js-rollout-strategy form-inline">
- <label class="sr-only" for="new-rollout-strategy-placeholder">{{
- s__('FeatureFlags|Rollout Strategy')
- }}</label>
- <div class="select-wrapper col-12 col-md-8 p-0">
- <select
- id="new-rollout-strategy-placeholder"
- disabled
- class="form-control select-control w-100"
- >
- <option>{{ s__('FeatureFlags|All users') }}</option>
- </select>
- <gl-icon
- name="chevron-down"
- class="gl-absolute gl-top-3 gl-right-3 gl-text-gray-500"
- :size="16"
- />
- </div>
- </div>
- </div>
- </div>
+ <div class="row">
+ <div class="col-md-12">
+ <h4>{{ s__('FeatureFlags|Strategies') }}</h4>
+ <div class="flex align-items-baseline justify-content-between">
+ <p class="mr-3">{{ $options.translations.newHelpText }}</p>
+ <gl-button variant="confirm" category="secondary" @click="addStrategy">
+ {{ s__('FeatureFlags|Add strategy') }}
+ </gl-button>
</div>
</div>
</div>
+ <div v-if="filteredStrategies.length > 0" data-testid="feature-flag-strategies">
+ <strategy
+ v-for="(strategy, index) in filteredStrategies"
+ :key="keyFor(strategy)"
+ :strategy="strategy"
+ :index="index"
+ @change="onFormStrategyChange($event, index)"
+ @delete="deleteStrategy(strategy)"
+ />
+ </div>
+ <div v-else class="flex justify-content-center border-top py-4 w-100">
+ <span>{{ $options.translations.noStrategiesText }}</span>
+ </div>
</fieldset>
<div class="form-actions">
<gl-button
ref="submitButton"
- :disabled="readOnly"
type="button"
variant="confirm"
class="js-ff-submit col-xs-12"
diff --git a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
index c59e3178b09..5575c6567b5 100644
--- a/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
+++ b/app/assets/javascripts/feature_flags/components/new_environments_dropdown.vue
@@ -80,7 +80,7 @@ export default {
@focus="fetchEnvironments"
@keyup="fetchEnvironments"
/>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<gl-dropdown-item
v-for="environment in results"
v-else-if="results.length"
diff --git a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
index 19be57f9d27..865c1e677cd 100644
--- a/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
+++ b/app/assets/javascripts/feature_flags/components/new_feature_flag.vue
@@ -1,10 +1,8 @@
<script>
import { GlAlert } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import axios from '~/lib/utils/axios_utils';
import featureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { NEW_VERSION_FLAG, ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
-import { createNewEnvironmentScope } from '../store/helpers';
+import { ROLLOUT_STRATEGY_ALL_USERS } from '../constants';
import FeatureFlagForm from './form.vue';
export default {
@@ -13,48 +11,14 @@ export default {
GlAlert,
},
mixins: [featureFlagsMixin()],
- inject: {
- showUserCallout: {},
- userCalloutId: {
- default: '',
- },
- userCalloutsPath: {
- default: '',
- },
- },
- data() {
- return {
- userShouldSeeNewFlagAlert: this.showUserCallout,
- };
- },
computed: {
...mapState(['error', 'path']),
- scopes() {
- return [
- createNewEnvironmentScope(
- {
- environmentScope: '*',
- active: true,
- },
- this.glFeatures.featureFlagsPermissions,
- ),
- ];
- },
- version() {
- return NEW_VERSION_FLAG;
- },
strategies() {
return [{ name: ROLLOUT_STRATEGY_ALL_USERS, parameters: {}, scopes: [] }];
},
},
methods: {
...mapActions(['createFeatureFlag']),
- dismissNewVersionFlagAlert() {
- this.userShouldSeeNewFlagAlert = false;
- axios.post(this.userCalloutsPath, {
- feature_name: this.userCalloutId,
- });
- },
},
};
</script>
@@ -69,9 +33,7 @@ export default {
<feature-flag-form
:cancel-path="path"
:submit-text="s__('FeatureFlags|Create feature flag')"
- :scopes="scopes"
:strategies="strategies"
- :version="version"
@handleSubmit="(data) => createFeatureFlag(data)"
/>
</div>
diff --git a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
index 45fc37da747..9dbffe75f6b 100644
--- a/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
+++ b/app/assets/javascripts/feature_flags/components/strategies/gitlab_user_list.vue
@@ -76,7 +76,7 @@ export default {
@focus="fetchUserLists"
@keyup="fetchUserLists"
/>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<gl-dropdown-item
v-for="list in userLists"
:key="list.id"
diff --git a/app/assets/javascripts/feature_flags/edit.js b/app/assets/javascripts/feature_flags/edit.js
index 010674592f8..98dee7c7e97 100644
--- a/app/assets/javascripts/feature_flags/edit.js
+++ b/app/assets/javascripts/feature_flags/edit.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import Vuex from 'vuex';
-import { parseBoolean } from '~/lib/utils/common_utils';
import EditFeatureFlag from './components/edit_feature_flag.vue';
import createStore from './store/edit';
@@ -16,9 +15,6 @@ export default () => {
environmentsEndpoint,
projectId,
featureFlagIssuesEndpoint,
- userCalloutsPath,
- userCalloutId,
- showUserCallout,
} = el.dataset;
return new Vue({
@@ -30,9 +26,6 @@ export default () => {
environmentsEndpoint,
projectId,
featureFlagIssuesEndpoint,
- userCalloutsPath,
- userCalloutId,
- showUserCallout: parseBoolean(showUserCallout),
},
render(createElement) {
return createElement(EditFeatureFlag);
diff --git a/app/assets/javascripts/feature_flags/store/edit/actions.js b/app/assets/javascripts/feature_flags/store/edit/actions.js
index 54c7e8c4453..8656479190a 100644
--- a/app/assets/javascripts/feature_flags/store/edit/actions.js
+++ b/app/assets/javascripts/feature_flags/store/edit/actions.js
@@ -2,8 +2,7 @@ import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { NEW_VERSION_FLAG } from '../../constants';
-import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import { mapStrategiesToRails } from '../helpers';
import * as types from './mutation_types';
/**
@@ -19,12 +18,7 @@ export const updateFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestUpdateFeatureFlag');
axios
- .put(
- state.endpoint,
- params.version === NEW_VERSION_FLAG
- ? mapStrategiesToRails(params)
- : mapFromScopesViewModel(params),
- )
+ .put(state.endpoint, mapStrategiesToRails(params))
.then(() => {
dispatch('receiveUpdateFeatureFlagSuccess');
visitUrl(state.path);
diff --git a/app/assets/javascripts/feature_flags/store/edit/mutations.js b/app/assets/javascripts/feature_flags/store/edit/mutations.js
index 0a610f4b395..3882cb2dfff 100644
--- a/app/assets/javascripts/feature_flags/store/edit/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/edit/mutations.js
@@ -1,5 +1,5 @@
import { LEGACY_FLAG } from '../../constants';
-import { mapToScopesViewModel, mapStrategiesToViewModel } from '../helpers';
+import { mapStrategiesToViewModel } from '../helpers';
import * as types from './mutation_types';
export default {
@@ -14,7 +14,6 @@ export default {
state.description = response.description;
state.iid = response.iid;
state.active = response.active;
- state.scopes = mapToScopesViewModel(response.scopes);
state.strategies = mapStrategiesToViewModel(response.strategies);
state.version = response.version || LEGACY_FLAG;
},
diff --git a/app/assets/javascripts/feature_flags/store/helpers.js b/app/assets/javascripts/feature_flags/store/helpers.js
index 2fa20e25f4e..300709f2771 100644
--- a/app/assets/javascripts/feature_flags/store/helpers.js
+++ b/app/assets/javascripts/feature_flags/store/helpers.js
@@ -1,149 +1,4 @@
-import { isEmpty, uniqueId, isString } from 'lodash';
-import {
- ROLLOUT_STRATEGY_ALL_USERS,
- ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- ROLLOUT_STRATEGY_USER_ID,
- ROLLOUT_STRATEGY_GITLAB_USER_LIST,
- INTERNAL_ID_PREFIX,
- DEFAULT_PERCENT_ROLLOUT,
- PERCENT_ROLLOUT_GROUP_ID,
- fetchPercentageParams,
- fetchUserIdParams,
- LEGACY_FLAG,
-} from '../constants';
-
-/**
- * Converts raw scope objects fetched from the API into an array of scope
- * objects that is easier/nicer to bind to in Vue.
- * @param {Array} scopesFromRails An array of scope objects fetched from the API
- */
-export const mapToScopesViewModel = (scopesFromRails) =>
- (scopesFromRails || []).map((s) => {
- const percentStrategy = (s.strategies || []).find(
- (strat) => strat.name === ROLLOUT_STRATEGY_PERCENT_ROLLOUT,
- );
-
- const rolloutPercentage = fetchPercentageParams(percentStrategy) || DEFAULT_PERCENT_ROLLOUT;
-
- const userStrategy = (s.strategies || []).find(
- (strat) => strat.name === ROLLOUT_STRATEGY_USER_ID,
- );
-
- const rolloutStrategy =
- (percentStrategy && percentStrategy.name) ||
- (userStrategy && userStrategy.name) ||
- ROLLOUT_STRATEGY_ALL_USERS;
-
- const rolloutUserIds = (fetchUserIdParams(userStrategy) || '')
- .split(',')
- .filter((id) => id)
- .join(', ');
-
- return {
- id: s.id,
- environmentScope: s.environment_scope,
- active: Boolean(s.active),
- canUpdate: Boolean(s.can_update),
- protected: Boolean(s.protected),
- rolloutStrategy,
- rolloutPercentage,
- rolloutUserIds,
-
- // eslint-disable-next-line no-underscore-dangle
- shouldBeDestroyed: Boolean(s._destroy),
- shouldIncludeUserIds: rolloutUserIds.length > 0 && percentStrategy !== null,
- };
- });
-/**
- * Converts the parameters emitted by the Vue component into
- * the shape that the Rails API expects.
- * @param {Array} scopesFromVue An array of scope objects from the Vue component
- */
-export const mapFromScopesViewModel = (params) => {
- const scopes = (params.scopes || []).map((s) => {
- const parameters = {};
- if (s.rolloutStrategy === ROLLOUT_STRATEGY_PERCENT_ROLLOUT) {
- parameters.groupId = PERCENT_ROLLOUT_GROUP_ID;
- parameters.percentage = s.rolloutPercentage;
- } else if (s.rolloutStrategy === ROLLOUT_STRATEGY_USER_ID) {
- parameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ',');
- }
-
- const userIdParameters = {};
-
- if (s.shouldIncludeUserIds && s.rolloutStrategy !== ROLLOUT_STRATEGY_USER_ID) {
- userIdParameters.userIds = (s.rolloutUserIds || '').replace(/, /g, ',');
- }
-
- // Strip out any internal IDs
- const id = isString(s.id) && s.id.startsWith(INTERNAL_ID_PREFIX) ? undefined : s.id;
-
- const strategies = [
- {
- name: s.rolloutStrategy,
- parameters,
- },
- ];
-
- if (!isEmpty(userIdParameters)) {
- strategies.push({ name: ROLLOUT_STRATEGY_USER_ID, parameters: userIdParameters });
- }
-
- return {
- id,
- environment_scope: s.environmentScope,
- active: s.active,
- can_update: s.canUpdate,
- protected: s.protected,
- _destroy: s.shouldBeDestroyed,
- strategies,
- };
- });
-
- const model = {
- operations_feature_flag: {
- name: params.name,
- description: params.description,
- active: params.active,
- scopes_attributes: scopes,
- version: LEGACY_FLAG,
- },
- };
-
- return model;
-};
-
-/**
- * Creates a new feature flag environment scope object for use
- * in a Vue component. An optional parameter can be passed to
- * override the property values that are created by default.
- *
- * @param {Object} overrides An optional object whose
- * property values will be used to override the default values.
- *
- */
-export const createNewEnvironmentScope = (overrides = {}, featureFlagPermissions = false) => {
- const defaultScope = {
- environmentScope: '',
- active: false,
- id: uniqueId(INTERNAL_ID_PREFIX),
- rolloutStrategy: ROLLOUT_STRATEGY_ALL_USERS,
- rolloutPercentage: DEFAULT_PERCENT_ROLLOUT,
- rolloutUserIds: '',
- };
-
- const newScope = {
- ...defaultScope,
- ...overrides,
- };
-
- if (featureFlagPermissions) {
- newScope.canUpdate = true;
- newScope.protected = false;
- }
-
- return newScope;
-};
+import { ROLLOUT_STRATEGY_GITLAB_USER_LIST, NEW_VERSION_FLAG } from '../constants';
const mapStrategyScopesToRails = (scopes) =>
scopes.length === 0
@@ -206,8 +61,8 @@ export const mapStrategiesToRails = (params) => ({
operations_feature_flag: {
name: params.name,
description: params.description,
- version: params.version,
active: params.active,
strategies_attributes: (params.strategies || []).map(mapStrategyToRails),
+ version: NEW_VERSION_FLAG,
},
});
diff --git a/app/assets/javascripts/feature_flags/store/index/mutations.js b/app/assets/javascripts/feature_flags/store/index/mutations.js
index 54e48a4b80c..7e08440c299 100644
--- a/app/assets/javascripts/feature_flags/store/index/mutations.js
+++ b/app/assets/javascripts/feature_flags/store/index/mutations.js
@@ -1,10 +1,7 @@
import Vue from 'vue';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { mapToScopesViewModel } from '../helpers';
import * as types from './mutation_types';
-const mapFlag = (flag) => ({ ...flag, scopes: mapToScopesViewModel(flag.scopes || []) });
-
const updateFlag = (state, flag) => {
const index = state.featureFlags.findIndex(({ id }) => id === flag.id);
Vue.set(state.featureFlags, index, flag);
@@ -31,7 +28,7 @@ export default {
[types.RECEIVE_FEATURE_FLAGS_SUCCESS](state, response) {
state.isLoading = false;
state.hasError = false;
- state.featureFlags = (response.data.feature_flags || []).map(mapFlag);
+ state.featureFlags = response.data.feature_flags || [];
const paginationInfo = createPaginationInfo(response.headers);
state.count = paginationInfo?.total ?? state.featureFlags.length;
@@ -58,7 +55,7 @@ export default {
updateFlag(state, flag);
},
[types.RECEIVE_UPDATE_FEATURE_FLAG_SUCCESS](state, data) {
- updateFlag(state, mapFlag(data));
+ updateFlag(state, data);
},
[types.RECEIVE_UPDATE_FEATURE_FLAG_ERROR](state, i) {
const flag = state.featureFlags.find(({ id }) => i === id);
diff --git a/app/assets/javascripts/feature_flags/store/new/actions.js b/app/assets/javascripts/feature_flags/store/new/actions.js
index d0a1c77a69e..dc3f7a21cdb 100644
--- a/app/assets/javascripts/feature_flags/store/new/actions.js
+++ b/app/assets/javascripts/feature_flags/store/new/actions.js
@@ -1,7 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
-import { NEW_VERSION_FLAG } from '../../constants';
-import { mapFromScopesViewModel, mapStrategiesToRails } from '../helpers';
+import { mapStrategiesToRails } from '../helpers';
import * as types from './mutation_types';
/**
@@ -17,12 +16,7 @@ export const createFeatureFlag = ({ state, dispatch }, params) => {
dispatch('requestCreateFeatureFlag');
return axios
- .post(
- state.endpoint,
- params.version === NEW_VERSION_FLAG
- ? mapStrategiesToRails(params)
- : mapFromScopesViewModel(params),
- )
+ .post(state.endpoint, mapStrategiesToRails(params))
.then(() => {
dispatch('receiveCreateFeatureFlagSuccess');
visitUrl(state.path);
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
index 7b4bed69fb8..747f368b671 100644
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '../lib/utils/axios_utils';
import { __ } from '../locale';
@@ -10,10 +10,10 @@ export function dismiss(endpoint, highlightId) {
feature_name: highlightId,
})
.catch(() =>
- Flash(
- __(
+ createFlash({
+ message: __(
'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
),
- ),
+ }),
);
}
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 38a5bdd4a71..d00e6e59cf5 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
@@ -75,6 +75,13 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
icon: 'approval',
tag: '@approved-by',
},
+ tokenAlternative: {
+ formattedKey: __('Approved-By'),
+ key: 'approved-by',
+ type: 'string',
+ param: 'usernames',
+ symbol: '@',
+ },
condition: [
{
url: 'approved_by_usernames[]=None',
@@ -105,7 +112,11 @@ export default (IssuableTokenKeys, disableTargetBranchFilter = false) => {
const tokenPosition = 3;
IssuableTokenKeys.tokenKeys.splice(tokenPosition, 0, ...[approvedBy.token]);
- IssuableTokenKeys.tokenKeysWithAlternative.splice(tokenPosition, 0, ...[approvedBy.token]);
+ IssuableTokenKeys.tokenKeysWithAlternative.splice(
+ tokenPosition,
+ 0,
+ ...[approvedBy.token, approvedBy.tokenAlternative],
+ );
IssuableTokenKeys.conditions.push(...approvedBy.condition);
const environmentToken = {
diff --git a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
index 626a5669067..e0281b8f443 100644
--- a/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
+++ b/app/assets/javascripts/filtered_search/available_dropdown_mappings.js
@@ -1,3 +1,4 @@
+import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
import { mergeUrlParams } from '../lib/utils/url_utility';
import DropdownAjaxFilter from './dropdown_ajax_filter';
import DropdownEmoji from './dropdown_emoji';
@@ -87,6 +88,7 @@ export default class AvailableDropdownMappings {
extraArguments: {
endpoint: this.getMilestoneEndpoint(),
symbol: '%',
+ preprocessing: (milestones) => milestones.sort(sortMilestonesByDueDate),
},
element: this.container.querySelector('#js-dropdown-milestone'),
},
diff --git a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
index 35c79891458..545719ee681 100644
--- a/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
+++ b/app/assets/javascripts/filtered_search/dropdown_ajax_filter.js
@@ -1,6 +1,6 @@
+import createFlash from '~/flash';
import { __ } from '~/locale';
import AjaxFilter from '../droplab/plugins/ajax_filter';
-import createFlash from '../flash';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
import FilteredSearchTokenizer from './filtered_search_tokenizer';
diff --git a/app/assets/javascripts/filtered_search/dropdown_emoji.js b/app/assets/javascripts/filtered_search/dropdown_emoji.js
index 91af3a6b812..a7648a3c463 100644
--- a/app/assets/javascripts/filtered_search/dropdown_emoji.js
+++ b/app/assets/javascripts/filtered_search/dropdown_emoji.js
@@ -1,7 +1,7 @@
+import createFlash from '~/flash';
import { __ } from '~/locale';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
-import createFlash from '../flash';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/dropdown_non_user.js b/app/assets/javascripts/filtered_search/dropdown_non_user.js
index 93051b00756..f78644a3893 100644
--- a/app/assets/javascripts/filtered_search/dropdown_non_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_non_user.js
@@ -1,7 +1,7 @@
+import createFlash from '~/flash';
import { __ } from '~/locale';
import Ajax from '../droplab/plugins/ajax';
import Filter from '../droplab/plugins/filter';
-import createFlash from '../flash';
import DropdownUtils from './dropdown_utils';
import FilteredSearchDropdown from './filtered_search_dropdown';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 707205a6502..5ba69f052c9 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,7 +1,7 @@
import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
-import { getParameterByName, getUrlParamsArray } from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
import {
ENTER_KEY_CODE,
BACKSPACE_KEY_CODE,
@@ -10,9 +10,8 @@ import {
DOWN_KEY_CODE,
} from '~/lib/utils/keycodes';
import { __ } from '~/locale';
-import createFlash from '../flash';
import { addClassIfElementExists } from '../lib/utils/dom_utils';
-import { visitUrl } from '../lib/utils/url_utility';
+import { visitUrl, getUrlParamsArray, getParameterByName } from '../lib/utils/url_utility';
import FilteredSearchContainer from './container';
import DropdownUtils from './dropdown_utils';
import eventHub from './event_hub';
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 eec4db41b0a..7143cb50ea6 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,4 +1,5 @@
-import { objectToQueryString, spriteIcon } from '~/lib/utils/common_utils';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { objectToQuery } from '~/lib/utils/url_utility';
import FilteredSearchContainer from './container';
import VisualTokenValue from './visual_token_value';
@@ -327,7 +328,7 @@ export default class FilteredSearchVisualTokens {
return endpoint;
}
- const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
+ const queryString = objectToQuery(JSON.parse(endpointQueryParams));
return `${endpoint}?${queryString}`;
}
diff --git a/app/assets/javascripts/filtered_search/visual_token_value.js b/app/assets/javascripts/filtered_search/visual_token_value.js
index 7f4445ad4c7..707add10009 100644
--- a/app/assets/javascripts/filtered_search/visual_token_value.js
+++ b/app/assets/javascripts/filtered_search/visual_token_value.js
@@ -4,7 +4,7 @@ import * as Emoji from '~/emoji';
import FilteredSearchContainer from '~/filtered_search/container';
import DropdownUtils from '~/filtered_search/dropdown_utils';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import { __ } from '~/locale';
@@ -83,7 +83,11 @@ export default class VisualTokenValue {
matchingLabel.text_color,
);
})
- .catch(() => new Flash(__('An error occurred while fetching label colors.')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while fetching label colors.'),
+ }),
+ );
}
updateEpicLabel(tokenValueContainer) {
@@ -105,7 +109,11 @@ export default class VisualTokenValue {
VisualTokenValue.replaceEpicTitle(tokenValueContainer, matchingEpic.title, matchingEpic.id);
})
- .catch(() => new Flash(__('An error occurred while adding formatted title for epic')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while adding formatted title for epic'),
+ }),
+ );
}
static replaceEpicTitle(tokenValueContainer, epicTitle, epicId) {
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index 2edb6e79d3b..741171b185a 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -125,38 +125,11 @@ const createFlash = function createFlash({
return flashContainer;
};
-/*
- * Flash banner supports different types of Flash configurations
- * along with ability to provide actionConfig which can be used to show
- * additional action or link on banner next to message
- *
- * @param {String} message Flash message text
- * @param {String} type Type of Flash, it can be `notice`, `success`, `warning` or `alert` (default)
- * @param {Object} parent Reference to parent element under which Flash needs to appear
- * @param {Object} actionConfig Map of config to show action on banner
- * @param {String} href URL to which action config should point to (default: '#')
- * @param {String} title Title of action
- * @param {Function} clickHandler Method to call when action is clicked on
- * @param {Boolean} fadeTransition Boolean to determine whether to fade the alert out
- */
-const deprecatedCreateFlash = function deprecatedCreateFlash(
- message,
- type,
- parent,
- actionConfig,
- fadeTransition,
- addBodyClass,
-) {
- return createFlash({ message, type, parent, actionConfig, fadeTransition, addBodyClass });
-};
-
export {
createFlash as default,
- deprecatedCreateFlash,
createFlashEl,
createAction,
hideFlash,
removeFlashClickListener,
FLASH_TYPES,
};
-window.Flash = createFlash;
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 893b74a9895..0fb70fb831e 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,9 +1,8 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
-const isRefactoring = document.body.classList.contains('sidebar-refactoring');
const HIDE_INTERVAL_TIMEOUT = 300;
-const COLLAPSED_PANEL_WIDTH = isRefactoring ? 48 : 50;
+const COLLAPSED_PANEL_WIDTH = 48;
const IS_OVER_CLASS = 'is-over';
const IS_ABOVE_CLASS = 'is-above';
const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
@@ -89,12 +88,12 @@ export const moveSubItemsToPosition = (el, subItems) => {
const boundingRect = el.getBoundingClientRect();
const left = sidebar ? sidebar.offsetWidth : COLLAPSED_PANEL_WIDTH;
let top = calculateTop(boundingRect, subItems.offsetHeight);
- if (isRefactoring && hasSubItems) {
- top -= header.offsetHeight;
- } else if (isRefactoring) {
+ const isAbove = top < boundingRect.top;
+ if (hasSubItems) {
+ top = isAbove ? top : top - header.offsetHeight;
+ } else {
top = boundingRect.top;
}
- const isAbove = top < boundingRect.top;
subItems.classList.add('fly-out-list');
subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - getHeaderHeight()}px, 0)`; // eslint-disable-line no-param-reassign
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 d6fcdeb9e13..1137951ccfc 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,16 +1,18 @@
<script>
/* eslint-disable vue/require-default-prop, vue/no-v-html */
+import { GlButton } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
import { mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
import Tracking from '~/tracking';
-import Identicon from '~/vue_shared/components/identicon.vue';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
const trackingMixin = Tracking.mixin();
export default {
components: {
- Identicon,
+ GlButton,
+ ProjectAvatar,
},
mixins: [trackingMixin],
inject: ['vuexModule'],
@@ -56,24 +58,18 @@ export default {
<template>
<li class="frequent-items-list-item-container">
- <a
+ <gl-button
+ category="tertiary"
:href="webUrl"
- class="clearfix dropdown-item"
+ class="gl-text-left gl-justify-content-start!"
@click="track('click_link', { label: `${dropdownType}_dropdown_frequent_items_list_item` })"
>
- <div
- ref="frequentItemsItemAvatarContainer"
- class="frequent-items-item-avatar-container avatar-container rect-avatar s32"
- >
- <img v-if="avatarUrl" ref="frequentItemsItemAvatar" :src="avatarUrl" class="avatar s32" />
- <identicon
- v-else
- :entity-id="itemId"
- :entity-name="itemName"
- size-class="s32"
- class="rect-avatar"
- />
- </div>
+ <project-avatar
+ class="gl-float-left gl-mr-3"
+ :project-avatar-url="avatarUrl"
+ :project-name="itemName"
+ aria-hidden="true"
+ />
<div ref="frequentItemsItemMetadataContainer" class="frequent-items-item-metadata-container">
<div
ref="frequentItemsItemTitle"
@@ -90,6 +86,6 @@ export default {
{{ truncatedNamespace }}
</div>
</div>
- </a>
+ </gl-button>
</li>
</template>
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
index 90b454d1b42..65a762f54ad 100644
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ b/app/assets/javascripts/frequent_items/store/actions.js
@@ -1,4 +1,5 @@
import AccessorUtilities from '~/lib/utils/accessor';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { getGroups, getProjects } from '~/rest_api';
import { getTopFrequentItems } from '../utils';
import * as types from './mutation_types';
@@ -51,7 +52,7 @@ export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
const params = {
simple: true,
per_page: 20,
- membership: Boolean(gon.current_user_id),
+ membership: isLoggedIn(),
};
let searchFunction;
diff --git a/app/assets/javascripts/gpg_badges.js b/app/assets/javascripts/gpg_badges.js
index fa6f07edfcf..7964e762dac 100644
--- a/app/assets/javascripts/gpg_badges.js
+++ b/app/assets/javascripts/gpg_badges.js
@@ -1,7 +1,8 @@
import $ from 'jquery';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import { parseQueryStringIntoObject } from '~/lib/utils/common_utils';
+import { queryToObject } from '~/lib/utils/url_utility';
+
import { __ } from '~/locale';
export default class GpgBadges {
@@ -27,7 +28,7 @@ export default class GpgBadges {
return Promise.reject(new Error(__('Missing commit signatures endpoint!')));
}
- const params = parseQueryStringIntoObject(tag.serialize());
+ const params = queryToObject(tag.serialize());
return axios
.get(endpoint, { params })
.then(({ data }) => {
diff --git a/app/assets/javascripts/grafana_integration/store/actions.js b/app/assets/javascripts/grafana_integration/store/actions.js
index 77d2acd3393..25347ad6433 100644
--- a/app/assets/javascripts/grafana_integration/store/actions.js
+++ b/app/assets/javascripts/grafana_integration/store/actions.js
@@ -40,6 +40,5 @@ export const receiveGrafanaIntegrationUpdateError = (_, error) => {
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
- type: 'alert',
});
};
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 7e897be9e9a..aad7712a9f0 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -1,2 +1,11 @@
-/* eslint-disable @gitlab/require-i18n-strings */
-export const IssueType = 'Issue';
+export const TYPE_CI_RUNNER = 'Ci::Runner';
+export const TYPE_GROUP = 'Group';
+export const TYPE_ISSUE = 'Issue';
+export const TYPE_ITERATION = 'Iteration';
+export const TYPE_ITERATIONS_CADENCE = 'Iterations::Cadence';
+export const TYPE_MERGE_REQUEST = 'MergeRequest';
+export const TYPE_MILESTONE = 'Milestone';
+export const TYPE_SCANNER_PROFILE = 'DastScannerProfile';
+export const TYPE_SITE_PROFILE = 'DastSiteProfile';
+export const TYPE_USER = 'User';
+export const TYPE_VULNERABILITY = 'Vulnerability';
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index e64e8009a5f..18f9a50bbce 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -18,11 +18,6 @@ export const MutationOperationMode = {
};
/**
- * Possible GraphQL entity types.
- */
-export const TYPE_GROUP = 'Group';
-
-/**
* Ids generated by GraphQL endpoints are usually in the format
* gid://gitlab/Groups/123. This method takes a type and an id
* and interpolates the 2 values into the expected GraphQL ID format.
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index c1fc75fbea6..b6a1f41afb5 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import fetchGroupPathAvailability from '~/pages/groups/new/fetch_group_path_availability';
import { slugify } from './lib/utils/text_utility';
@@ -16,7 +16,7 @@ export default class Group {
if (groupName.value === '') {
groupName.addEventListener('keyup', this.updateHandler);
- groupName.addEventListener('blur', this.updateGroupPathSlugHandler);
+ groupName.addEventListener('keyup', this.updateGroupPathSlugHandler);
}
});
@@ -61,11 +61,15 @@ export default class Group {
element.value = suggestedSlug;
});
} else if (exists && !suggests.length) {
- flash(__('Unable to suggest a path. Please refresh and try again.'));
+ createFlash({
+ message: __('Unable to suggest a path. Please refresh and try again.'),
+ });
}
})
.catch(() =>
- flash(__('An error occurred while checking group path. Please refresh and try again.')),
+ createFlash({
+ message: __('An error occurred while checking group path. Please refresh and try again.'),
+ }),
);
}
}
diff --git a/app/assets/javascripts/group_label_subscription.js b/app/assets/javascripts/group_label_subscription.js
index 257f5ac9658..378259eb9c8 100644
--- a/app/assets/javascripts/group_label_subscription.js
+++ b/app/assets/javascripts/group_label_subscription.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { __ } from '~/locale';
import { fixTitle, hide } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
const tooltipTitles = {
@@ -30,7 +30,11 @@ export default class GroupLabelSubscription {
this.toggleSubscriptionButtons();
this.$unsubscribeButtons.removeAttr('data-url');
})
- .catch(() => flash(__('There was an error when unsubscribing from this label.')));
+ .catch(() =>
+ createFlash({
+ message: __('There was an error when unsubscribing from this label.'),
+ }),
+ );
}
subscribe(event) {
@@ -45,7 +49,11 @@ export default class GroupLabelSubscription {
.post(url)
.then(() => GroupLabelSubscription.setNewTooltip($btn))
.then(() => this.toggleSubscriptionButtons())
- .catch(() => flash(__('There was an error when subscribing to this label.')));
+ .catch(() =>
+ createFlash({
+ message: __('There was an error when subscribing to this label.'),
+ }),
+ );
}
toggleSubscriptionButtons() {
diff --git a/app/assets/javascripts/group_settings/components/shared_runners_form.vue b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
index a1d706f0f66..f61d96b3dfd 100644
--- a/app/assets/javascripts/group_settings/components/shared_runners_form.vue
+++ b/app/assets/javascripts/group_settings/components/shared_runners_form.vue
@@ -101,7 +101,7 @@ export default {
<h4 class="gl-display-flex gl-align-items-center">
{{ __('Set up shared runner availability') }}
- <gl-loading-icon v-if="isLoading" class="gl-ml-3" inline />
+ <gl-loading-icon v-if="isLoading" class="gl-ml-3" size="sm" inline />
</h4>
<section class="gl-mt-5">
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 9d2c7cfe581..2a95b242510 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -1,10 +1,8 @@
<script>
-/* global Flash */
-
import { GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
import { HIDDEN_CLASS } from '~/lib/utils/constants';
-import { mergeUrlParams } from '~/lib/utils/url_utility';
+import { mergeUrlParams, getParameterByName } from '~/lib/utils/url_utility';
import { __, s__, sprintf } from '~/locale';
import { COMMON_STR, CONTENT_LIST_CLASS } from '../constants';
@@ -116,7 +114,7 @@ export default {
this.isLoading = false;
window.scrollTo({ top: 0, behavior: 'smooth' });
- Flash(COMMON_STR.FAILURE);
+ createFlash({ message: COMMON_STR.FAILURE });
});
},
fetchAllGroups() {
@@ -202,7 +200,7 @@ export default {
if (err.status === 403) {
message = COMMON_STR.LEAVE_FORBIDDEN;
}
- Flash(message);
+ createFlash({ message });
this.targetGroup.isBeingRemoved = false;
});
},
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index dbad2688451..ad0b27c9693 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlAvatar,
GlLoadingIcon,
GlBadge,
GlIcon,
@@ -7,7 +8,6 @@ import {
GlSafeHtmlDirective,
} from '@gitlab/ui';
import { visitUrl } from '~/lib/utils/url_utility';
-import identicon from '~/vue_shared/components/identicon.vue';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '../constants';
import eventHub from '../event_hub';
@@ -23,11 +23,11 @@ export default {
SafeHtml: GlSafeHtmlDirective,
},
components: {
+ GlAvatar,
GlBadge,
GlLoadingIcon,
GlIcon,
UserAccessRoleBadge,
- identicon,
itemCaret,
itemTypeIcon,
itemStats,
@@ -125,21 +125,21 @@ export default {
size="lg"
class="d-none d-sm-inline-flex flex-shrink-0 gl-mr-3"
/>
- <div
- :class="{ 'd-sm-flex': !group.isChildrenLoading }"
- class="avatar-container rect-avatar s32 d-none flex-grow-0 flex-shrink-0"
+ <a
+ :class="{ 'gl-sm-display-flex': !group.isChildrenLoading }"
+ class="gl-display-none gl-text-decoration-none! gl-mr-3"
+ :href="group.relativePath"
+ :aria-label="group.name"
>
- <a :href="group.relativePath" class="no-expand">
- <img
- v-if="hasAvatar"
- :src="group.avatarUrl"
- data-testid="group-avatar"
- class="avatar s40"
- :itemprop="microdata.imageItemprop"
- />
- <identicon v-else :entity-id="group.id" :entity-name="group.name" size-class="s40" />
- </a>
- </div>
+ <gl-avatar
+ shape="rect"
+ :entity-name="group.name"
+ :src="group.avatarUrl"
+ :alt="group.name"
+ :size="32"
+ :itemprop="microdata.imageItemprop"
+ />
+ </a>
<div class="group-text-container d-flex flex-fill align-items-center">
<div class="group-text flex-grow-1 flex-shrink-1">
<div class="d-flex align-items-center flex-wrap title namespace-title gl-mr-3">
@@ -178,7 +178,7 @@ export default {
</div>
</div>
<div v-if="isGroupPendingRemoval">
- <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
+ <gl-badge variant="warning">{{ __('pending deletion') }}</gl-badge>
</div>
<div class="metadata d-flex flex-grow-1 flex-shrink-0 flex-wrap justify-content-md-between">
<item-actions
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index d407fdd2b90..59a37b2a1d5 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -1,6 +1,6 @@
<script>
import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
-import { getParameterByName } from '../../lib/utils/common_utils';
+import { getParameterByName } from '../../lib/utils/url_utility';
import eventHub from '../event_hub';
export default {
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index e09df8a5d26..7a37d1eb93d 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -73,7 +73,7 @@ export default {
icon-name="star"
/>
<div v-if="isProjectPendingRemoval">
- <gl-badge variant="warning">{{ __('pending removal') }}</gl-badge>
+ <gl-badge variant="warning">{{ __('pending deletion') }}</gl-badge>
</div>
<div v-if="isProject" class="last-updated">
<time-ago-tooltip :time="item.updatedAt" tooltip-placement="bottom" />
diff --git a/app/assets/javascripts/groups/groups_filterable_list.js b/app/assets/javascripts/groups/groups_filterable_list.js
index cedf16cd7f1..a7d44322eb1 100644
--- a/app/assets/javascripts/groups/groups_filterable_list.js
+++ b/app/assets/javascripts/groups/groups_filterable_list.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import FilterableList from '~/filterable_list';
-import { normalizeHeaders, getParameterByName } from '../lib/utils/common_utils';
+import { normalizeHeaders } from '../lib/utils/common_utils';
+import { getParameterByName } from '../lib/utils/url_utility';
import eventHub from './event_hub';
export default class GroupFilterableList extends FilterableList {
@@ -45,7 +46,7 @@ export default class GroupFilterableList extends FilterableList {
onFilterInput() {
const queryData = {};
const $form = $(this.form);
- const archivedParam = getParameterByName('archived', window.location.href);
+ const archivedParam = getParameterByName('archived');
const filterGroupsParam = $form.find(`[name="${this.filterInputField}"]`).val();
if (filterGroupsParam) {
@@ -85,11 +86,11 @@ export default class GroupFilterableList extends FilterableList {
// Get option query param, also preserve currently applied query param
const sortParam = getParameterByName(
'sort',
- isOptionFilterBySort ? e.currentTarget.href : window.location.href,
+ isOptionFilterBySort ? e.currentTarget.search : window.location.search,
);
const archivedParam = getParameterByName(
'archived',
- isOptionFilterByArchivedProjects ? e.currentTarget.href : window.location.href,
+ isOptionFilterByArchivedProjects ? e.currentTarget.search : window.location.search,
);
if (sortParam) {
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index d3a52f9f0cf..2b75d10f659 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -57,6 +57,6 @@ export default {
@primaryAction="doAction"
>
<span v-html="message.text"></span>
- <gl-loading-icon v-show="isLoading" inline class="vertical-align-middle ml-1" />
+ <gl-loading-icon v-show="isLoading" size="sm" inline class="vertical-align-middle ml-1" />
</gl-alert>
</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 0c9fd324f8c..e345e5dc099 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -130,7 +130,6 @@ export default {
<div class="ide-view flex-grow d-flex">
<template v-if="loadDeferred">
<find-file
- v-show="fileFindVisible"
:files="allBlobs"
:visible="fileFindVisible"
:loading="loading"
diff --git a/app/assets/javascripts/ide/components/ide_project_header.vue b/app/assets/javascripts/ide/components/ide_project_header.vue
index 36891505230..1c25a8e634d 100644
--- a/app/assets/javascripts/ide/components/ide_project_header.vue
+++ b/app/assets/javascripts/ide/components/ide_project_header.vue
@@ -1,9 +1,9 @@
<script>
-import ProjectAvatarDefault from '~/vue_shared/components/project_avatar/default.vue';
+import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
export default {
components: {
- ProjectAvatarDefault,
+ ProjectAvatar,
},
props: {
project: {
@@ -16,8 +16,12 @@ export default {
<template>
<div class="context-header ide-context-header">
- <a :href="project.web_url" :title="s__('IDE|Go to project')">
- <project-avatar-default :project="project" :size="48" />
+ <a :href="project.web_url" :title="s__('IDE|Go to project')" data-testid="go-to-project-link">
+ <project-avatar
+ :project-name="project.name"
+ :project-avatar-url="project.avatar_url"
+ :size="48"
+ />
<span class="ide-sidebar-project-title">
<span class="sidebar-context-title"> {{ project.name }} </span>
<span
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 6c7f084c164..938385f0b81 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -79,7 +79,7 @@ export default {
<gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
<div v-show="!stage.isCollapsed" ref="jobList" class="card-body p-0">
- <gl-loading-icon v-if="showLoadingIcon" />
+ <gl-loading-icon v-if="showLoadingIcon" size="sm" />
<template v-else>
<item v-for="job in stage.jobs" :key="job.id" :job="job" @clickViewLog="clickViewLog" />
</template>
diff --git a/app/assets/javascripts/ide/components/merge_requests/item.vue b/app/assets/javascripts/ide/components/merge_requests/item.vue
index 639937481f3..2d9f74a06ee 100644
--- a/app/assets/javascripts/ide/components/merge_requests/item.vue
+++ b/app/assets/javascripts/ide/components/merge_requests/item.vue
@@ -41,7 +41,7 @@ export default {
<template>
<a :href="mergeRequestHref" class="btn-link d-flex align-items-center">
<span class="d-flex gl-mr-3 ide-search-list-current-icon">
- <gl-icon v-if="isActive" :size="18" name="mobile-issue-close" use-deprecated-sizes />
+ <gl-icon v-if="isActive" :size="16" name="mobile-issue-close" />
</span>
<span>
<strong> {{ item.title }} </strong>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index f8dc10420d0..e8541d3a4c3 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -61,9 +61,6 @@ export default {
message: sprintf(s__('The name "%{name}" is already taken in this directory.'), {
name: this.entryName,
}),
- type: 'alert',
- parent: document,
- actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
diff --git a/app/assets/javascripts/ide/components/preview/navigator.vue b/app/assets/javascripts/ide/components/preview/navigator.vue
index 4d35e946d89..838c363a6a3 100644
--- a/app/assets/javascripts/ide/components/preview/navigator.vue
+++ b/app/assets/javascripts/ide/components/preview/navigator.vue
@@ -126,7 +126,11 @@ export default {
class="ide-navigator-location form-control bg-white"
readonly
/>
- <gl-loading-icon v-if="loading" class="position-absolute ide-preview-loading-icon" />
+ <gl-loading-icon
+ v-if="loading"
+ size="sm"
+ class="position-absolute ide-preview-loading-icon"
+ />
</div>
</header>
</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index bf2af9ffd49..5c711313ff6 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -6,9 +6,9 @@ import {
EDITOR_CODE_INSTANCE_FN,
EDITOR_DIFF_INSTANCE_FN,
} from '~/editor/constants';
-import EditorLite from '~/editor/editor_lite';
-import { EditorWebIdeExtension } from '~/editor/extensions/editor_lite_webide_ext';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import { EditorWebIdeExtension } from '~/editor/extensions/source_editor_webide_ext';
+import SourceEditor from '~/editor/source_editor';
+import createFlash from '~/flash';
import ModelManager from '~/ide/lib/common/model_manager';
import { defaultDiffEditorOptions, defaultEditorOptions } from '~/ide/lib/editor_options';
import { __ } from '~/locale';
@@ -216,7 +216,7 @@ export default {
},
mounted() {
if (!this.globalEditor) {
- this.globalEditor = new EditorLite();
+ this.globalEditor = new SourceEditor();
}
this.initEditor();
@@ -250,14 +250,11 @@ export default {
this.createEditorInstance();
})
.catch((err) => {
- flash(
- __('Error setting up editor. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ createFlash({
+ message: __('Error setting up editor. Please try again.'),
+ fadeTransition: false,
+ addBodyClass: true,
+ });
throw err;
});
},
@@ -418,7 +415,11 @@ export default {
const parentPath = getPathParent(this.file.path);
const path = `${parentPath ? `${parentPath}/` : ''}${file.name}`;
- return this.addTempImage({ name: path, rawPath: content }).then(({ name: fileName }) => {
+ return this.addTempImage({
+ name: path,
+ rawPath: URL.createObjectURL(file),
+ content: atob(content.split('base64,')[1]),
+ }).then(({ name: fileName }) => {
this.editor.replaceSelectedText(`![${fileName}](./${fileName})`);
});
});
diff --git a/app/assets/javascripts/ide/components/shared/tokened_input.vue b/app/assets/javascripts/ide/components/shared/tokened_input.vue
index ed0dab47947..14052c23a0c 100644
--- a/app/assets/javascripts/ide/components/shared/tokened_input.vue
+++ b/app/assets/javascripts/ide/components/shared/tokened_input.vue
@@ -82,7 +82,7 @@ export default {
<div class="value-container rounded">
<div class="value">{{ token.label }}</div>
<div class="remove-token inverted">
- <gl-icon :size="10" name="close" use-deprecated-sizes />
+ <gl-icon :size="16" name="close" />
</div>
</div>
</button>
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index 08fb2f5e5a0..c91a98c9527 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -93,7 +93,7 @@ export default {
<div class="d-flex flex-column flex-fill min-height-0 pr-3">
<div class="top-bar d-flex border-left-0 align-items-center">
<div v-if="loadingText" data-qa-selector="loading_container">
- <gl-loading-icon :inline="true" />
+ <gl-loading-icon size="sm" :inline="true" />
<span>{{ loadingText }}</span>
</div>
<terminal-controls
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
index 5f60bf0269d..27cedd80347 100644
--- a/app/assets/javascripts/ide/ide_router.js
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import IdeRouter from '~/ide/ide_router_extension';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -111,14 +111,11 @@ export const createRouter = (store, defaultBranch) => {
}
})
.catch((e) => {
- flash(
- __('Error while loading the project data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ createFlash({
+ message: __('Error while loading the project data. Please try again.'),
+ fadeTransition: false,
+ addBodyClass: true,
+ });
throw e;
});
}
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index 1d051062637..682914df9ec 100644
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
@@ -1,5 +1,6 @@
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';
@@ -31,7 +32,7 @@ export default class DirtyDiffController {
this.modelManager = modelManager;
this.decorationsController = decorationsController;
this.dirtyDiffWorker = new DirtyDiffWorker();
- this.throttledComputeDiff = throttle(this.computeDiff, 250);
+ this.throttledComputeDiff = throttle(this.computeDiff, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
this.decorate = this.decorate.bind(this);
this.dirtyDiffWorker.addEventListener('message', this.decorate);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 6bd28cd4fb6..ef4f47f226a 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -100,7 +100,7 @@ export default {
return Api.commitPipelines(getters.currentProject.path_with_namespace, commitSha);
},
pingUsage(projectPath) {
- const url = `${gon.relative_url_root}/${projectPath}/usage_ping/web_ide_pipelines_count`;
+ const url = `${gon.relative_url_root}/${projectPath}/service_ping/web_ide_pipelines_count`;
return axios.post(url);
},
getCiConfig(projectPath, content) {
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 062dc150805..b22e58a376d 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -1,6 +1,6 @@
import { escape } from 'lodash';
import Vue from 'vue';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -36,16 +36,13 @@ export const createTempEntry = (
const fullName = name.slice(-1) !== '/' && type === 'tree' ? `${name}/` : name;
if (getters.entryExists(name)) {
- flash(
- sprintf(__('The name "%{name}" is already taken in this directory.'), {
+ createFlash({
+ message: sprintf(__('The name "%{name}" is already taken in this directory.'), {
name: name.split('/').pop(),
}),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ fadeTransition: false,
+ addBodyClass: true,
+ });
return undefined;
}
@@ -79,11 +76,11 @@ export const createTempEntry = (
return file;
};
-export const addTempImage = ({ dispatch, getters }, { name, rawPath = '' }) =>
+export const addTempImage = ({ dispatch, getters }, { name, rawPath = '', content = '' }) =>
dispatch('createTempEntry', {
name: getters.getAvailableFileName(name),
type: 'blob',
- content: rawPath.split('base64,')[1],
+ content,
rawPath,
openFile: false,
makeFileActive: false,
@@ -284,14 +281,11 @@ export const getBranchData = ({ commit, state }, { projectId, branchId, force =
if (e.response.status === 404) {
reject(e);
} else {
- flash(
- __('Error loading branch data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ createFlash({
+ message: __('Error loading branch data. Please try again.'),
+ fadeTransition: false,
+ addBodyClass: true,
+ });
reject(
new Error(
diff --git a/app/assets/javascripts/ide/stores/actions/merge_request.js b/app/assets/javascripts/ide/stores/actions/merge_request.js
index 5e020f16104..f3f603d4ae9 100644
--- a/app/assets/javascripts/ide/stores/actions/merge_request.js
+++ b/app/assets/javascripts/ide/stores/actions/merge_request.js
@@ -36,9 +36,6 @@ export const getMergeRequestsForBranch = (
.catch((e) => {
createFlash({
message: __(`Error fetching merge requests for ${branchId}`),
- type: 'alert',
- parent: document,
- actionConfig: null,
fadeTransition: false,
addBodyClass: true,
});
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
index 120a577d44a..93ad19ba81e 100644
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -1,5 +1,5 @@
import { escape } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import api from '../../../api';
import service from '../../services';
@@ -19,14 +19,11 @@ export const getProjectData = ({ commit, state }, { namespace, projectId, force
resolve(data);
})
.catch(() => {
- flash(
- __('Error loading project data. Please try again.'),
- 'alert',
- document,
- null,
- false,
- true,
- );
+ createFlash({
+ message: __('Error loading project data. Please try again.'),
+ fadeTransition: false,
+ addBodyClass: true,
+ });
reject(new Error(`Project not loaded ${namespace}/${projectId}`));
});
} else {
@@ -45,7 +42,11 @@ export const refreshLastCommitData = ({ commit }, { projectId, branchId } = {})
});
})
.catch((e) => {
- flash(__('Error loading last commit.'), 'alert', document, null, false, true);
+ createFlash({
+ message: __('Error loading last commit.'),
+ fadeTransition: false,
+ addBodyClass: true,
+ });
throw e;
});
diff --git a/app/assets/javascripts/ide/stores/modules/clientside/actions.js b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
index 2bebf8b90ce..e36419cd7eb 100644
--- a/app/assets/javascripts/ide/stores/modules/clientside/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/clientside/actions.js
@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
export const pingUsage = ({ rootGetters }) => {
const { web_url: projectUrl } = rootGetters.currentProject;
- const url = `${projectUrl}/usage_ping/web_ide_clientside_preview`;
+ const url = `${projectUrl}/service_ping/web_ide_clientside_preview`;
return axios.post(url);
};
diff --git a/app/assets/javascripts/ide/stores/modules/commit/actions.js b/app/assets/javascripts/ide/stores/modules/commit/actions.js
index 29555799074..2ff71523b1b 100644
--- a/app/assets/javascripts/ide/stores/modules/commit/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/commit/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { addNumericSuffix } from '~/ide/utils';
import { sprintf, __ } from '~/locale';
import { leftSidebarViews } from '../../../constants';
@@ -143,7 +143,11 @@ export const commitChanges = ({ commit, state, getters, dispatch, rootState, roo
commit(types.UPDATE_LOADING, false);
if (!data.short_id) {
- flash(data.message, 'alert', document, null, false, true);
+ createFlash({
+ message: data.message,
+ fadeTransition: false,
+ addBodyClass: true,
+ });
return null;
}
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index 4019703b296..0cef3b98e61 100644
--- a/app/assets/javascripts/ide/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -252,10 +252,10 @@ export function extractMarkdownImagesFromEntries(mdFile, entries) {
.trim();
const imageContent = entries[imagePath]?.content || entries[imagePath]?.raw;
+ const imageRawPath = entries[imagePath]?.rawPath;
if (!isAbsolute(path) && imageContent) {
- const ext = path.includes('.') ? path.split('.').pop().trim() : 'jpeg';
- const src = `data:image/${ext};base64,${imageContent}`;
+ const src = imageRawPath;
i += 1;
const key = `{{${prefix}${i}}}`;
images[key] = { alt, src, title };
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
new file mode 100644
index 00000000000..44d6d17232f
--- /dev/null
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -0,0 +1,40 @@
+<script>
+import { GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDropdown,
+ GlSearchBoxByType,
+ },
+ inheritAttrs: false,
+ props: {
+ namespaces: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return { searchTerm: '' };
+ },
+ computed: {
+ filteredNamespaces() {
+ return this.namespaces.filter((ns) =>
+ ns.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+ },
+ },
+};
+</script>
+<template>
+ <gl-dropdown
+ toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ class="import-entities-namespace-dropdown gl-h-7 gl-flex-fill-1"
+ data-qa-selector="target_namespace_selector_dropdown"
+ v-bind="$attrs"
+ >
+ <template #header>
+ <gl-search-box-by-type v-model.trim="searchTerm" />
+ </template>
+ <slot :namespaces="filteredNamespaces"></slot>
+ </gl-dropdown>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index 3daa5eebcb6..cb7e3ef9632 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
@@ -227,7 +227,12 @@ export default {
</template>
</gl-sprintf>
</span>
- <gl-search-box-by-click class="gl-ml-auto" @submit="filter = $event" @clear="filter = ''" />
+ <gl-search-box-by-click
+ class="gl-ml-auto"
+ :placeholder="s__('BulkImport|Filter by source group')"
+ @submit="filter = $event"
+ @clear="filter = ''"
+ />
</div>
<gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
<template v-else>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
index 63c18f4d78e..1c3ede769e0 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue
@@ -1,7 +1,6 @@
<script>
import {
GlButton,
- GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
@@ -11,6 +10,7 @@ import {
} from '@gitlab/ui';
import { joinPaths } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
+import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import addValidationErrorMutation from '../graphql/mutations/add_validation_error.mutation.graphql';
@@ -22,8 +22,8 @@ const DEBOUNCE_INTERVAL = 300;
export default {
components: {
ImportStatus,
+ ImportGroupDropdown,
GlButton,
- GlDropdown,
GlDropdownDivider,
GlDropdownItem,
GlDropdownSectionHeader,
@@ -83,6 +83,10 @@ export default {
},
computed: {
+ availableNamespaceNames() {
+ return this.availableNamespaces.map((ns) => ns.full_path);
+ },
+
importTarget() {
return this.group.import_target;
},
@@ -153,9 +157,11 @@ export default {
disabled: isAlreadyImported,
}"
>
- <gl-dropdown
+ <import-group-dropdown
+ #default="{ namespaces }"
:text="importTarget.target_namespace"
:disabled="isAlreadyImported"
+ :namespaces="availableNamespaceNames"
toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
class="import-entities-namespace-dropdown gl-h-7 gl-flex-grow-1"
data-qa-selector="target_namespace_selector_dropdown"
@@ -163,22 +169,22 @@ export default {
<gl-dropdown-item @click="$emit('update-target-namespace', '')">{{
s__('BulkImport|No parent')
}}</gl-dropdown-item>
- <template v-if="availableNamespaces.length">
+ <template v-if="namespaces.length">
<gl-dropdown-divider />
<gl-dropdown-section-header>
{{ s__('BulkImport|Existing groups') }}
</gl-dropdown-section-header>
<gl-dropdown-item
- v-for="ns in availableNamespaces"
- :key="ns.full_path"
+ v-for="ns in namespaces"
+ :key="ns"
data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns.full_path"
- @click="$emit('update-target-namespace', ns.full_path)"
+ :data-qa-group-name="ns"
+ @click="$emit('update-target-namespace', ns)"
>
- {{ ns.full_path }}
+ {{ ns }}
</gl-dropdown-item>
</template>
- </gl-dropdown>
+ </import-group-dropdown>
<div
class="import-entities-target-select-separator gl-h-7 gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
>
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 be09052fb7e..14d08caef34 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
@@ -47,18 +47,7 @@ export default {
},
availableNamespaces() {
- const serializedNamespaces = this.namespaces.map(({ fullPath }) => ({
- id: fullPath,
- text: fullPath,
- }));
-
- return [
- { text: __('Groups'), children: serializedNamespaces },
- {
- text: __('Users'),
- children: [{ id: this.defaultTargetNamespace, text: this.defaultTargetNamespace }],
- },
- ];
+ return this.namespaces.map(({ fullPath }) => fullPath);
},
importAllButtonText() {
@@ -179,6 +168,7 @@ export default {
:key="repo.importSource.providerLink"
:repo="repo"
:available-namespaces="availableNamespaces"
+ :user-namespace="defaultTargetNamespace"
/>
</template>
</tbody>
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 a803afeb901..e2fd608d9db 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
@@ -1,8 +1,17 @@
<script>
-import { GlIcon, GlBadge, GlFormInput, GlButton, GlLink } from '@gitlab/ui';
+import {
+ GlIcon,
+ GlBadge,
+ GlFormInput,
+ GlButton,
+ GlLink,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
+} from '@gitlab/ui';
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
-import Select2Select from '~/vue_shared/components/select2_select.vue';
+import ImportGroupDropdown from '../../components/group_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
@@ -10,10 +19,13 @@ import { isProjectImportable, isIncompatible, getImportStatus } from '../utils';
export default {
name: 'ProviderRepoTableRow',
components: {
- Select2Select,
+ ImportGroupDropdown,
ImportStatus,
GlFormInput,
GlButton,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlDropdownSectionHeader,
GlIcon,
GlBadge,
GlLink,
@@ -23,6 +35,10 @@ export default {
type: Object,
required: true,
},
+ userNamespace: {
+ type: String,
+ required: true,
+ },
availableNamespaces: {
type: Array,
required: true,
@@ -61,22 +77,6 @@ export default {
return this.ciCdOnly ? __('Connect') : __('Import');
},
- select2Options() {
- return {
- data: this.availableNamespaces,
- containerCssClass: 'import-namespace-select qa-project-namespace-select gl-w-auto',
- };
- },
-
- targetNamespaceSelect: {
- get() {
- return this.importTarget.targetNamespace;
- },
- set(value) {
- this.updateImportTarget({ targetNamespace: value });
- },
- },
-
newNameInput: {
get() {
return this.importTarget.newName;
@@ -118,7 +118,29 @@ 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">
- <select2-select v-model="targetNamespaceSelect" :options="select2Options" />
+ <import-group-dropdown
+ #default="{ namespaces }"
+ :text="importTarget.targetNamespace"
+ :namespaces="availableNamespaces"
+ >
+ <template v-if="namespaces.length">
+ <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
+ <gl-dropdown-item
+ v-for="ns in namespaces"
+ :key="ns"
+ data-qa-selector="target_group_dropdown_item"
+ :data-qa-group-name="ns"
+ @click="updateImportTarget({ targetNamespace: ns })"
+ >
+ {{ ns }}
+ </gl-dropdown-item>
+ <gl-dropdown-divider />
+ </template>
+ <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
+ <gl-dropdown-item @click="updateImportTarget({ targetNamespace: ns })">{{
+ userNamespace
+ }}</gl-dropdown-item>
+ </import-group-dropdown>
<div
class="import-entities-target-select-separator gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
>
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 6b7fe23ed60..110cc77b20d 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -38,7 +38,7 @@ export function initStoreFromElement(element) {
export function initPropsFromElement(element) {
return {
- providerTitle: element.dataset.providerTitle,
+ providerTitle: element.dataset.provider,
filterable: parseBoolean(element.dataset.filterable),
paginatable: parseBoolean(element.dataset.paginatable),
};
diff --git a/app/assets/javascripts/incidents_settings/incidents_settings_service.js b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
index 83fd29a058e..93baa54956a 100644
--- a/app/assets/javascripts/incidents_settings/incidents_settings_service.js
+++ b/app/assets/javascripts/incidents_settings/incidents_settings_service.js
@@ -24,7 +24,6 @@ export default class IncidentsSettingsService {
createFlash({
message: `${ERROR_MSG} ${message}`,
- type: 'alert',
});
});
}
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index ec93980251b..1242493fb57 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -75,15 +75,6 @@ export default {
validProjectKey() {
return !this.enableJiraIssues || Boolean(this.projectKey) || !this.validated;
},
- showJiraVulnerabilitiesOptions() {
- return this.showJiraVulnerabilitiesIntegration;
- },
- showUltimateUpgrade() {
- return this.showJiraIssuesIntegration && !this.showJiraVulnerabilitiesIntegration;
- },
- showPremiumUpgrade() {
- return !this.showJiraIssuesIntegration;
- },
},
created() {
eventHub.$on('validateForm', this.validateForm);
@@ -128,23 +119,30 @@ export default {
}}
</template>
</gl-form-checkbox>
- <jira-issue-creation-vulnerabilities
- v-if="enableJiraIssues"
- :project-key="projectKey"
- :initial-is-enabled="initialEnableJiraVulnerabilities"
- :initial-issue-type-id="initialVulnerabilitiesIssuetype"
- :show-full-feature="showJiraVulnerabilitiesOptions"
- data-testid="jira-for-vulnerabilities"
- @request-get-issue-types="getJiraIssueTypes"
- />
+ <template v-if="enableJiraIssues">
+ <jira-issue-creation-vulnerabilities
+ :project-key="projectKey"
+ :initial-is-enabled="initialEnableJiraVulnerabilities"
+ :initial-issue-type-id="initialVulnerabilitiesIssuetype"
+ :show-full-feature="showJiraVulnerabilitiesIntegration"
+ data-testid="jira-for-vulnerabilities"
+ @request-get-issue-types="getJiraIssueTypes"
+ />
+ <jira-upgrade-cta
+ v-if="!showJiraVulnerabilitiesIntegration"
+ class="gl-mt-2 gl-ml-6"
+ data-testid="ultimate-upgrade-cta"
+ show-ultimate-message
+ :upgrade-plan-path="upgradePlanPath"
+ />
+ </template>
</template>
<jira-upgrade-cta
- v-if="showUltimateUpgrade || showPremiumUpgrade"
+ v-else
class="gl-mt-2"
- :class="{ 'gl-ml-6': showUltimateUpgrade }"
+ data-testid="premium-upgrade-cta"
+ show-premium-message
:upgrade-plan-path="upgradePlanPath"
- :show-ultimate-message="showUltimateUpgrade"
- :show-premium-message="showPremiumUpgrade"
/>
</div>
</gl-form-group>
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 84c8594c6b6..4aab1123af9 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -1,5 +1,6 @@
<script>
import {
+ GlFormGroup,
GlModal,
GlDropdown,
GlDropdownItem,
@@ -12,16 +13,21 @@ import {
import { partition, isString } from 'lodash';
import Api from '~/api';
import ExperimentTracking from '~/experimentation/experiment_tracking';
-import GroupSelect from '~/invite_members/components/group_select.vue';
-import MembersTokenSelect from '~/invite_members/components/members_token_select.vue';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__, sprintf } from '~/locale';
import { INVITE_MEMBERS_IN_COMMENT, GROUP_FILTERS } from '../constants';
import eventHub from '../event_hub';
+import {
+ responseMessageFromError,
+ responseMessageFromSuccess,
+} from '../utils/response_message_parser';
+import GroupSelect from './group_select.vue';
+import MembersTokenSelect from './members_token_select.vue';
export default {
name: 'InviteMembersModal',
components: {
+ GlFormGroup,
GlDatepicker,
GlLink,
GlModal,
@@ -79,9 +85,13 @@ export default {
selectedDate: undefined,
groupToBeSharedWith: {},
source: 'unknown',
+ invalidFeedbackMessage: '',
};
},
computed: {
+ validationState() {
+ return this.invalidFeedbackMessage === '' ? null : false;
+ },
isInviteGroup() {
return this.inviteeType === 'group';
},
@@ -142,6 +152,7 @@ export default {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
},
closeModal() {
+ this.resetFields();
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
},
sendInvite() {
@@ -150,7 +161,6 @@ export default {
} else {
this.submitInviteMembers();
}
- this.closeModal();
},
trackInvite() {
if (this.source === INVITE_MEMBERS_IN_COMMENT) {
@@ -158,12 +168,12 @@ export default {
tracking.event('comment_invite_success');
}
},
- cancelInvite() {
+ resetFields() {
this.selectedAccessLevel = this.defaultAccessLevel;
this.selectedDate = undefined;
this.newUsersToInvite = [];
this.groupToBeSharedWith = {};
- this.closeModal();
+ this.invalidFeedbackMessage = '';
},
changeSelectedItem(item) {
this.selectedAccessLevel = item;
@@ -175,9 +185,11 @@ export default {
apiShareWithGroup(this.id, this.shareWithGroupPostData(this.groupToBeSharedWith.id))
.then(this.showToastMessageSuccess)
- .catch(this.showToastMessageError);
+ .catch(this.showInvalidFeedbackMessage);
},
submitInviteMembers() {
+ this.invalidFeedbackMessage = '';
+
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const promises = [];
@@ -196,10 +208,11 @@ export default {
promises.push(apiAddByUserId(this.id, this.addByUserIdPostData(usersToAddById)));
}
-
this.trackInvite();
- Promise.all(promises).then(this.showToastMessageSuccess).catch(this.showToastMessageError);
+ Promise.all(promises)
+ .then(this.conditionallyShowToastSuccess)
+ .catch(this.showInvalidFeedbackMessage);
},
inviteByEmailPostData(usersToInviteByEmail) {
return {
@@ -224,13 +237,27 @@ export default {
group_access: this.selectedAccessLevel,
};
},
+ conditionallyShowToastSuccess(response) {
+ const message = responseMessageFromSuccess(response);
+
+ if (message === '') {
+ this.showToastMessageSuccess();
+
+ return;
+ }
+
+ this.invalidFeedbackMessage = message;
+ },
showToastMessageSuccess() {
this.$toast.show(this.$options.labels.toastMessageSuccessful, this.toastOptions);
+ this.closeModal();
},
- showToastMessageError(error) {
- const message = error.response.data.message || this.$options.labels.toastMessageUnsuccessful;
-
- this.$toast.show(message, this.toastOptions);
+ showInvalidFeedbackMessage(response) {
+ this.invalidFeedbackMessage =
+ responseMessageFromError(response) || this.$options.labels.invalidFeedbackMessageDefault;
+ },
+ handleMembersTokenSelectClear() {
+ this.invalidFeedbackMessage = '';
},
},
labels: {
@@ -267,8 +294,8 @@ export default {
accessLevel: s__('InviteMembersModal|Select a role'),
accessExpireDate: s__('InviteMembersModal|Access expiration date (optional)'),
toastMessageSuccessful: s__('InviteMembersModal|Members were successfully added'),
- toastMessageUnsuccessful: s__('InviteMembersModal|Some of the members could not be added'),
- readMoreText: s__(`InviteMembersModal|%{linkStart}Learn more%{linkEnd} about roles.`),
+ invalidFeedbackMessageDefault: s__('InviteMembersModal|Something went wrong'),
+ readMoreText: s__(`InviteMembersModal|%{linkStart}Read more%{linkEnd} about role permissions`),
inviteButtonText: s__('InviteMembersModal|Invite'),
cancelButtonText: s__('InviteMembersModal|Cancel'),
headerCloseLabel: s__('InviteMembersModal|Close invite team members'),
@@ -283,6 +310,7 @@ export default {
data-qa-selector="invite_members_modal_content"
:title="$options.labels[inviteeType].modalTitle"
:header-close-label="$options.labels.headerCloseLabel"
+ @close="resetFields"
>
<div>
<p ref="introText">
@@ -293,15 +321,22 @@ export default {
</gl-sprintf>
</p>
- <label :id="$options.membersTokenSelectLabelId" class="gl-font-weight-bold gl-mt-5">{{
- $options.labels[inviteeType].searchField
- }}</label>
- <div class="gl-mt-2">
+ <gl-form-group
+ class="gl-mt-2"
+ :invalid-feedback="invalidFeedbackMessage"
+ :state="validationState"
+ :description="$options.labels[inviteeType].placeHolder"
+ data-testid="members-form-group"
+ >
+ <label :id="$options.membersTokenSelectLabelId" class="col-form-label">{{
+ $options.labels[inviteeType].searchField
+ }}</label>
<members-token-select
v-if="!isInviteGroup"
v-model="newUsersToInvite"
+ :validation-state="validationState"
:aria-labelledby="$options.membersTokenSelectLabelId"
- :placeholder="$options.labels[inviteeType].placeHolder"
+ @clear="handleMembersTokenSelectClear"
/>
<group-select
v-if="isInviteGroup"
@@ -309,7 +344,7 @@ export default {
:groups-filter="groupSelectFilter"
:parent-group-id="groupSelectParentId"
/>
- </div>
+ </gl-form-group>
<label class="gl-font-weight-bold gl-mt-3">{{ $options.labels.accessLevel }}</label>
<div class="gl-mt-2 gl-w-half gl-xs-w-full">
@@ -364,15 +399,15 @@ export default {
<template #modal-footer>
<div class="gl-display-flex gl-flex-direction-row gl-justify-content-end gl-flex-wrap gl-m-0">
- <gl-button ref="cancelButton" @click="cancelInvite">
+ <gl-button data-testid="cancel-button" @click="closeModal">
{{ $options.labels.cancelButtonText }}
</gl-button>
<div class="gl-mr-3"></div>
<gl-button
- ref="inviteButton"
:disabled="inviteDisabled"
variant="success"
data-qa-selector="invite_button"
+ data-testid="invite-button"
@click="sendInvite"
>{{ $options.labels.inviteButtonText }}</gl-button
>
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 db6a7888786..7aece3b7bb4 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlSprintf } from '@gitlab/ui';
+import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui';
import { debounce } from 'lodash';
import { __ } from '~/locale';
import { getUsers } from '~/rest_api';
@@ -10,6 +10,7 @@ export default {
GlTokenSelector,
GlAvatar,
GlAvatarLabeled,
+ GlIcon,
GlSprintf,
},
props: {
@@ -22,6 +23,11 @@ export default {
type: String,
required: true,
},
+ validationState: {
+ type: Boolean,
+ required: false,
+ default: null,
+ },
},
data() {
return {
@@ -84,6 +90,13 @@ export default {
this.hasBeenFocused = true;
},
+ handleTokenRemove() {
+ if (this.selectedTokens.length) {
+ return;
+ }
+
+ this.$emit('clear');
+ },
},
queryOptions: { exclude_internal: true, active: true },
i18n: {
@@ -95,19 +108,26 @@ export default {
<template>
<gl-token-selector
v-model="selectedTokens"
+ :state="validationState"
:dropdown-items="users"
:loading="loading"
:allow-user-defined-tokens="emailIsValid"
:hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
+ :text-input-attrs="{
+ 'data-testid': 'members-token-select-input',
+ 'data-qa-selector': 'members_token_select_input',
+ }"
@blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
@focus="handleFocus"
+ @token-remove="handleTokenRemove"
>
<template #token-content="{ token }">
- <gl-avatar v-if="token.avatar_url" :src="token.avatar_url" :size="16" />
+ <gl-icon v-if="validationState === false" name="error" :size="16" class="gl-mr-2" />
+ <gl-avatar v-else-if="token.avatar_url" :src="token.avatar_url" :size="16" />
{{ token.name }}
</template>
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 0c5538d5b86..83e6cac0ac0 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const SEARCH_DELAY = 200;
export const INVITE_MEMBERS_IN_COMMENT = 'invite_members_in_comment';
@@ -6,3 +8,7 @@ export const GROUP_FILTERS = {
ALL: 'all',
DESCENDANT_GROUPS: 'descendant_groups',
};
+
+export const API_MESSAGES = {
+ EMAIL_ALREADY_INVITED: __('Invite email has already been taken'),
+};
diff --git a/app/assets/javascripts/invite_members/utils/response_message_parser.js b/app/assets/javascripts/invite_members/utils/response_message_parser.js
new file mode 100644
index 00000000000..b7bc9ea5652
--- /dev/null
+++ b/app/assets/javascripts/invite_members/utils/response_message_parser.js
@@ -0,0 +1,65 @@
+import { isString } from 'lodash';
+import { API_MESSAGES } from '~/invite_members/constants';
+
+function responseKeyedMessageParsed(keyedMessage) {
+ try {
+ const keys = Object.keys(keyedMessage);
+ const msg = keyedMessage[keys[0]];
+
+ if (msg === API_MESSAGES.EMAIL_ALREADY_INVITED) {
+ return '';
+ }
+ return msg;
+ } catch {
+ return '';
+ }
+}
+function responseMessageStringForMultiple(message) {
+ return message.includes(':');
+}
+function responseMessageStringFirstPart(message) {
+ return message.split(' and ')[0];
+}
+
+export function responseMessageFromError(response) {
+ if (!response?.response?.data) {
+ return '';
+ }
+
+ const {
+ response: { data },
+ } = response;
+
+ return (
+ data.error ||
+ data.message?.user?.[0] ||
+ data.message?.access_level?.[0] ||
+ data.message?.error ||
+ data.message ||
+ ''
+ );
+}
+
+export function responseMessageFromSuccess(response) {
+ if (!response?.[0]?.data) {
+ return '';
+ }
+
+ const { data } = response[0];
+
+ if (data.message && !data.message.user) {
+ const { message } = data;
+
+ if (isString(message)) {
+ if (responseMessageStringForMultiple(message)) {
+ return responseMessageStringFirstPart(message);
+ }
+
+ return message;
+ }
+
+ return responseKeyedMessageParsed(message);
+ }
+
+ return data.message || data.message?.user || data.error || '';
+}
diff --git a/app/assets/javascripts/issuable/components/issuable_by_email.vue b/app/assets/javascripts/issuable/components/issuable_by_email.vue
index c659dfef495..6e300831e00 100644
--- a/app/assets/javascripts/issuable/components/issuable_by_email.vue
+++ b/app/assets/javascripts/issuable/components/issuable_by_email.vue
@@ -36,7 +36,7 @@ export default {
default: null,
},
issuableType: {
- default: '',
+ default: 'issue',
},
emailsHelpPagePath: {
default: '',
@@ -78,7 +78,7 @@ export default {
} = await axios.put(this.resetPath);
this.email = newAddress;
} catch {
- this.$toast.show(__('There was an error when reseting email token.'), { type: 'error' });
+ this.$toast.show(__('There was an error when reseting email token.'));
}
},
cancelHandler() {
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue b/app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue
new file mode 100644
index 00000000000..9509399e91d
--- /dev/null
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/components/status_select.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { ISSUE_STATUS_SELECT_OPTIONS } from '../constants';
+
+export default {
+ name: 'StatusSelect',
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ },
+ data() {
+ return {
+ status: null,
+ };
+ },
+ computed: {
+ dropdownText() {
+ return this.status?.text ?? this.$options.i18n.defaultDropdownText;
+ },
+ selectedValue() {
+ return this.status?.value;
+ },
+ },
+ methods: {
+ onDropdownItemClick(statusOption) {
+ // clear status if the currently checked status is clicked again
+ if (this.status?.value === statusOption.value) {
+ this.status = null;
+ } else {
+ this.status = statusOption;
+ }
+ },
+ },
+ i18n: {
+ dropdownTitle: __('Change status'),
+ defaultDropdownText: __('Select status'),
+ },
+ ISSUE_STATUS_SELECT_OPTIONS,
+};
+</script>
+<template>
+ <div>
+ <input type="hidden" name="update[state_event]" :value="selectedValue" />
+ <gl-dropdown :text="dropdownText" :title="$options.i18n.dropdownTitle" class="gl-w-full">
+ <gl-dropdown-item
+ v-for="statusOption in $options.ISSUE_STATUS_SELECT_OPTIONS"
+ :key="statusOption.value"
+ :is-checked="selectedValue === statusOption.value"
+ is-check-item
+ :title="statusOption.text"
+ @click="onDropdownItemClick(statusOption)"
+ >
+ {{ statusOption.text }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </div>
+</template>
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/constants.js b/app/assets/javascripts/issuable_bulk_update_sidebar/constants.js
new file mode 100644
index 00000000000..ad15b25f9cf
--- /dev/null
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/constants.js
@@ -0,0 +1,17 @@
+import { __ } from '~/locale';
+
+export const ISSUE_STATUS_MODIFIERS = {
+ REOPEN: 'reopen',
+ CLOSE: 'close',
+};
+
+export const ISSUE_STATUS_SELECT_OPTIONS = [
+ {
+ value: ISSUE_STATUS_MODIFIERS.REOPEN,
+ text: __('Open'),
+ },
+ {
+ value: ISSUE_STATUS_MODIFIERS.CLOSE,
+ text: __('Closed'),
+ },
+];
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js b/app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js
new file mode 100644
index 00000000000..43179a86d70
--- /dev/null
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/init_issue_status_select.js
@@ -0,0 +1,17 @@
+import Vue from 'vue';
+import StatusSelect from './components/status_select.vue';
+
+export default function initIssueStatusSelect() {
+ const el = document.querySelector('.js-issue-status');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render(h) {
+ return h(StatusSelect);
+ },
+ });
+}
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js
index 366a9a8a883..463e0e5837e 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_actions.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { difference, intersection, union } from 'lodash';
-import { deprecatedCreateFlash as Flash } from './flash';
-import axios from './lib/utils/axios_utils';
-import { __ } from './locale';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __ } from '~/locale';
export default {
init({ form, issues, prefixId } = {}) {
@@ -32,7 +32,9 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return new Flash(__('Issue update failed'));
+ return createFlash({
+ message: __('Issue update failed'),
+ });
},
/**
diff --git a/app/assets/javascripts/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js
index 97d50dde9f7..a9d4548f8cf 100644
--- a/app/assets/javascripts/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -2,11 +2,12 @@
import $ from 'jquery';
import { property } from 'lodash';
+
+import issueableEventHub from '~/issues_list/eventhub';
+import LabelsSelect from '~/labels_select';
+import MilestoneSelect from '~/milestone_select';
+import initIssueStatusSelect from './init_issue_status_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
-import issueStatusSelect from './issue_status_select';
-import issueableEventHub from './issues_list/eventhub';
-import LabelsSelect from './labels_select';
-import MilestoneSelect from './milestone_select';
import subscriptionSelect from './subscription_select';
const HIDDEN_CLASS = 'hidden';
@@ -29,7 +30,7 @@ export default class IssuableBulkUpdateSidebar {
this.$sidebar = $('.right-sidebar');
this.$sidebarInnerContainer = this.$sidebar.find('.issuable-sidebar');
this.$bulkEditCancelBtn = $('.js-bulk-update-menu-hide');
- this.$bulkEditSubmitBtn = $('.update-selected-issues');
+ this.$bulkEditSubmitBtn = $('.js-update-selected-issues');
this.$bulkUpdateEnableBtn = $('.js-bulk-update-toggle');
this.$otherFilters = $('.issues-other-filters');
this.$checkAllContainer = $('.check-all-holder');
@@ -56,7 +57,7 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- issueStatusSelect();
+ initIssueStatusSelect();
subscriptionSelect();
if (IS_EE) {
diff --git a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
index 179c2b83c6c..179c2b83c6c 100644
--- a/app/assets/javascripts/issuable_init_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js
index 4a688d819b0..b12ac776b4f 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/issuable_bulk_update_sidebar/subscription_select.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __ } from './locale';
+import { __ } from '~/locale';
export default function subscriptionSelect() {
$('.js-subscription-event').each((i, element) => {
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index a87d4f077cc..51b5237a339 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -9,19 +9,23 @@ export default class IssuableContext {
this.userSelect = new UsersSelect(currentUser);
this.reviewersSelect = new UsersSelect(currentUser, '.js-reviewer-search');
- import(/* webpackChunkName: 'select2' */ 'select2/select2')
- .then(() => {
- // eslint-disable-next-line promise/no-nesting
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $('select.select2').select2({
- width: 'resolve',
- dropdownAutoWidth: true,
- });
- })
- .catch(() => {});
- })
- .catch(() => {});
+ const $select2 = $('select.select2');
+
+ if ($select2.length) {
+ import(/* webpackChunkName: 'select2' */ 'select2/select2')
+ .then(() => {
+ // eslint-disable-next-line promise/no-nesting
+ loadCSSFile(gon.select2_css_path)
+ .then(() => {
+ $select2.select2({
+ width: 'resolve',
+ dropdownAutoWidth: true,
+ });
+ })
+ .catch(() => {});
+ })
+ .catch(() => {});
+ }
$('.issuable-sidebar .inline-update').on('change', 'select', function onClickSelect() {
return $(this).submit();
diff --git a/app/assets/javascripts/issuable_create/components/issuable_form.vue b/app/assets/javascripts/issuable_create/components/issuable_form.vue
index 3cbd5620063..c216a05bdb0 100644
--- a/app/assets/javascripts/issuable_create/components/issuable_form.vue
+++ b/app/assets/javascripts/issuable_create/components/issuable_form.vue
@@ -72,16 +72,17 @@ export default {
:show-suggest-popover="true"
:textarea-value="issuableDescription"
>
- <textarea
- id="issuable-description"
- ref="textarea"
- slot="textarea"
- v-model="issuableDescription"
- dir="auto"
- class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
- :aria-label="__('Description')"
- :placeholder="__('Write a comment or drag your files here…')"
- ></textarea>
+ <template #textarea>
+ <textarea
+ id="issuable-description"
+ ref="textarea"
+ v-model="issuableDescription"
+ dir="auto"
+ class="note-textarea qa-issuable-form-description rspec-issuable-form-description js-gfm-input js-autosize markdown-area"
+ :aria-label="__('Description')"
+ :placeholder="__('Write a comment or drag your files here…')"
+ ></textarea>
+ </template>
</markdown-field>
</div>
</div>
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index cdeee68b762..5a57da292a0 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,4 +1,4 @@
-import issuableInitBulkUpdateSidebar from './issuable_init_bulk_update_sidebar';
+import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
export default class IssuableIndex {
constructor(pagePrefix = 'issuable_') {
diff --git a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
index a19c76cfe3f..87066a0a0b6 100644
--- a/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/issuable_list/components/issuable_list_root.vue
@@ -134,7 +134,7 @@ export default {
labelFilterParam: {
type: String,
required: false,
- default: null,
+ default: undefined,
},
isManualOrdering: {
type: Boolean,
diff --git a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
index ca057094868..011db52cbe3 100644
--- a/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/issuable_show/components/issuable_show_root.vue
@@ -153,9 +153,9 @@ export default {
</template>
</issuable-discussion>
- <issuable-sidebar @sidebar-toggle="$emit('sidebar-toggle', $event)">
- <template #right-sidebar-items="sidebarProps">
- <slot name="right-sidebar-items" v-bind="sidebarProps"></slot>
+ <issuable-sidebar>
+ <template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }">
+ <slot name="right-sidebar-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
</template>
</issuable-sidebar>
</div>
diff --git a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
index 8a159139af0..99dcccd12ed 100644
--- a/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
+++ b/app/assets/javascripts/issuable_sidebar/components/issuable_sidebar_root.vue
@@ -2,15 +2,15 @@
import { GlIcon } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Cookies from 'js-cookie';
-
import { parseBoolean } from '~/lib/utils/common_utils';
+import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants';
export default {
components: {
GlIcon,
},
data() {
- const userExpanded = !parseBoolean(Cookies.get('collapsed_gutter'));
+ const userExpanded = !parseBoolean(Cookies.get(USER_COLLAPSED_GUTTER_COOKIE));
// We're deliberately keeping two different props for sidebar status;
// 1. userExpanded reflects value based on cookie `collapsed_gutter`.
@@ -20,13 +20,6 @@ export default {
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
};
},
- watch: {
- isExpanded(expanded) {
- this.$emit('sidebar-toggle', {
- expanded,
- });
- },
- },
mounted() {
window.addEventListener('resize', this.handleWindowResize);
this.updatePageContainerClass();
@@ -49,11 +42,11 @@ export default {
this.updatePageContainerClass();
}
},
- handleToggleSidebarClick() {
+ toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.userExpanded = this.isExpanded;
- Cookies.set('collapsed_gutter', !this.userExpanded);
+ Cookies.set(USER_COLLAPSED_GUTTER_COOKIE, !this.userExpanded);
this.updatePageContainerClass();
},
},
@@ -68,8 +61,9 @@ export default {
>
<button
class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
+ data-testid="toggle-right-sidebar-button"
:title="__('Toggle sidebar')"
- @click="handleToggleSidebarClick"
+ @click="toggleSidebar"
>
<span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
__('Collapse sidebar')
@@ -83,7 +77,10 @@ export default {
/>
</button>
<div data-testid="sidebar-items" class="issuable-sidebar">
- <slot name="right-sidebar-items" v-bind="{ sidebarExpanded: isExpanded }"></slot>
+ <slot
+ name="right-sidebar-items"
+ v-bind="{ sidebarExpanded: isExpanded, toggleSidebar }"
+ ></slot>
</div>
</aside>
</template>
diff --git a/app/assets/javascripts/issuable_sidebar/constants.js b/app/assets/javascripts/issuable_sidebar/constants.js
new file mode 100644
index 00000000000..4f4b6341a1c
--- /dev/null
+++ b/app/assets/javascripts/issuable_sidebar/constants.js
@@ -0,0 +1 @@
+export const USER_COLLAPSED_GUTTER_COOKIE = 'collapsed_gutter';
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index f6eff8133a7..1e053d7daaa 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash 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,9 @@ export default class Issue {
this.createMergeRequestDropdown.checkAbilityToCreateBranch();
}
} else {
- flash(issueFailMessage);
+ createFlash({
+ message: issueFailMessage,
+ });
}
}
@@ -102,6 +104,10 @@ export default class Issue {
$container.html(data.html);
}
})
- .catch(() => flash(__('Failed to load related branches')));
+ .catch(() =>
+ createFlash({
+ message: __('Failed to load related branches'),
+ }),
+ );
}
}
diff --git a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
index bb637dea033..938b90b3f7c 100644
--- a/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
+++ b/app/assets/javascripts/issue_show/components/incidents/graphql/queries/get_alert.graphql
@@ -1,6 +1,7 @@
query getAlert($iid: String!, $fullPath: ID!) {
project(fullPath: $fullPath) {
issue(iid: $iid) {
+ id
alertManagementAlert {
iid
title
diff --git a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql
index 9c28fdded21..ec8d8f32d8b 100644
--- a/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql
+++ b/app/assets/javascripts/issue_show/queries/update_issue.mutation.graphql
@@ -1,5 +1,9 @@
mutation updateIssue($input: UpdateIssueInput!) {
updateIssue(input: $input) {
+ issuable: issue {
+ id
+ state
+ }
errors
}
}
diff --git a/app/assets/javascripts/issue_show/services/index.js b/app/assets/javascripts/issue_show/services/index.js
index 08b04ebfdaf..b1deeaae0fc 100644
--- a/app/assets/javascripts/issue_show/services/index.js
+++ b/app/assets/javascripts/issue_show/services/index.js
@@ -1,11 +1,9 @@
-import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor';
import axios from '../../lib/utils/axios_utils';
export default class Service {
constructor(endpoint) {
this.endpoint = `${endpoint}.json`;
this.realtimeEndpoint = `${endpoint}/realtime_changes`;
- registerCaptchaModalInterceptor(axios);
}
getData() {
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
deleted file mode 100644
index 2ede0837930..00000000000
--- a/app/assets/javascripts/issue_status_select.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import $ from 'jquery';
-import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { __ } from './locale';
-
-export default function issueStatusSelect() {
- $('.js-issue-status').each((i, el) => {
- const fieldName = $(el).data('fieldName');
- initDeprecatedJQueryDropdown($(el), {
- selectable: true,
- fieldName,
- toggleLabel(selected, element, instance) {
- let label = __('Author');
- const $item = instance.dropdown.find('.is-active');
- if ($item.length) {
- label = $item.text();
- }
- return label;
- },
- clicked(options) {
- return options.e.preventDefault();
- },
- id(obj, element) {
- return $(element).data('id');
- },
- });
- });
-}
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
index 51cad662ebf..b13a389b963 100644
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
@@ -6,15 +6,11 @@ import {
GlSafeHtmlDirective as SafeHtml,
} from '@gitlab/ui';
import { toNumber, omit } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import {
- scrollToElement,
- urlParamsToObject,
- historyPushState,
- getParameterByName,
-} from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { setUrlParams, urlParamsToObject, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import initManualOrdering from '~/manual_ordering';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -82,10 +78,7 @@ export default {
isBulkEditing: false,
issuables: [],
loading: false,
- page:
- getParameterByName('page', window.location.href) !== null
- ? toNumber(getParameterByName('page'))
- : 1,
+ page: getParameterByName('page') !== null ? toNumber(getParameterByName('page')) : 1,
selection: {},
totalItems: 0,
};
@@ -265,10 +258,13 @@ export default {
})
.catch(() => {
this.loading = false;
- return flash(__('An error occurred while loading issues'));
+ return createFlash({
+ message: __('An error occurred while loading issues'),
+ });
});
},
getQueryObject() {
+ // eslint-disable-next-line import/no-deprecated
return urlParamsToObject(window.location.search);
},
onPaginate(newPage) {
diff --git a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
index 70d73aca925..07492b0046c 100644
--- a/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
+++ b/app/assets/javascripts/issues_list/components/issue_card_time_info.vue
@@ -115,7 +115,7 @@ export default {
{{ timeEstimate }}
</span>
<weight-count
- class="gl-display-none gl-sm-display-inline-block gl-mr-3"
+ class="issuable-weight gl-display-none gl-sm-display-inline-block gl-mr-3"
:weight="issue.weight"
/>
<issue-health-status
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 dbf7717b248..6563094ef72 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues_list/components/issues_list_app.vue
@@ -11,45 +11,47 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
import createFlash from '~/flash';
+import { TYPE_USER } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
import IssuableList from '~/issuable_list/components/issuable_list_root.vue';
import { IssuableListTabs, IssuableStates } from '~/issuable_list/constants';
import {
- API_PARAM,
CREATED_DESC,
i18n,
initialPageParams,
+ issuesCountSmartQueryBase,
MAX_LIST_SIZE,
PAGE_SIZE,
PARAM_DUE_DATE,
PARAM_SORT,
PARAM_STATE,
- RELATIVE_POSITION_DESC,
+ RELATIVE_POSITION_ASC,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
- TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_EPIC,
TOKEN_TYPE_ITERATION,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
TOKEN_TYPE_WEIGHT,
UPDATED_DESC,
- URL_PARAM,
urlSortParams,
} from '~/issues_list/constants';
import {
- convertToParams,
+ convertToApiParams,
convertToSearchQuery,
+ convertToUrlParams,
getDueDateValue,
getFilterTokens,
getSortKey,
getSortOptions,
} from '~/issues_list/utils';
import axios from '~/lib/utils/axios_utils';
-import { getParameterByName } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import {
DEFAULT_NONE_ANY,
OPERATOR_IS_ONLY,
@@ -71,6 +73,10 @@ import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label
import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
import WeightToken from '~/vue_shared/components/filtered_search_bar/tokens/weight_token.vue';
import eventHub from '../eventhub';
+import searchIterationsQuery from '../queries/search_iterations.query.graphql';
+import searchLabelsQuery from '../queries/search_labels.query.graphql';
+import searchMilestonesQuery from '../queries/search_milestones.query.graphql';
+import searchUsersQuery from '../queries/search_users.query.graphql';
import IssueCardTimeInfo from './issue_card_time_info.vue';
export default {
@@ -95,9 +101,6 @@ export default {
autocompleteAwardEmojisPath: {
default: '',
},
- autocompleteUsersPath: {
- default: '',
- },
calendarPath: {
default: '',
},
@@ -119,6 +122,9 @@ export default {
hasIssueWeightsFeature: {
default: false,
},
+ hasIterationsFeature: {
+ default: false,
+ },
hasMultipleIssueAssigneesFeature: {
default: false,
},
@@ -140,15 +146,6 @@ export default {
newIssuePath: {
default: '',
},
- projectIterationsPath: {
- default: '',
- },
- projectLabelsPath: {
- default: '',
- },
- projectMilestonesPath: {
- default: '',
- },
projectPath: {
default: '',
},
@@ -176,26 +173,17 @@ export default {
showBulkEditSidebar: false,
sortKey: getSortKey(getParameterByName(PARAM_SORT)) || defaultSortKey,
state: state || IssuableStates.Opened,
- totalIssues: 0,
};
},
apollo: {
issues: {
query: getIssuesQuery,
variables() {
- return {
- projectPath: this.projectPath,
- search: this.searchQuery,
- sort: this.sortKey,
- state: this.state,
- ...this.pageParams,
- ...this.apiFilterParams,
- };
+ return this.queryVariables;
},
- update: ({ project }) => project.issues.nodes,
+ update: ({ project }) => project?.issues.nodes ?? [],
result({ data }) {
- this.pageInfo = data.project.issues.pageInfo;
- this.totalIssues = data.project.issues.count;
+ this.pageInfo = data.project?.issues.pageInfo ?? {};
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
},
error(error) {
@@ -206,8 +194,55 @@ export default {
},
debounce: 200,
},
+ countOpened: {
+ ...issuesCountSmartQueryBase,
+ variables() {
+ return {
+ ...this.queryVariables,
+ state: IssuableStates.Opened,
+ };
+ },
+ skip() {
+ return !this.hasProjectIssues;
+ },
+ },
+ countClosed: {
+ ...issuesCountSmartQueryBase,
+ variables() {
+ return {
+ ...this.queryVariables,
+ state: IssuableStates.Closed,
+ };
+ },
+ skip() {
+ return !this.hasProjectIssues;
+ },
+ },
+ countAll: {
+ ...issuesCountSmartQueryBase,
+ variables() {
+ return {
+ ...this.queryVariables,
+ state: IssuableStates.All,
+ };
+ },
+ skip() {
+ return !this.hasProjectIssues;
+ },
+ },
},
computed: {
+ queryVariables() {
+ return {
+ isSignedIn: this.isSignedIn,
+ projectPath: this.projectPath,
+ search: this.searchQuery,
+ sort: this.sortKey,
+ state: this.state,
+ ...this.pageParams,
+ ...this.apiFilterParams,
+ };
+ },
hasSearch() {
return this.searchQuery || Object.keys(this.urlFilterParams).length;
},
@@ -215,32 +250,30 @@ export default {
return this.showBulkEditSidebar || !this.issues.length;
},
isManualOrdering() {
- return this.sortKey === RELATIVE_POSITION_DESC;
+ return this.sortKey === RELATIVE_POSITION_ASC;
},
isOpenTab() {
return this.state === IssuableStates.Opened;
},
apiFilterParams() {
- return convertToParams(this.filterTokens, API_PARAM);
+ return convertToApiParams(this.filterTokens);
},
urlFilterParams() {
- return convertToParams(this.filterTokens, URL_PARAM);
+ return convertToUrlParams(this.filterTokens);
},
searchQuery() {
return convertToSearchQuery(this.filterTokens) || undefined;
},
searchTokens() {
- let preloadedAuthors = [];
+ const preloadedAuthors = [];
if (gon.current_user_id) {
- preloadedAuthors = [
- {
- id: gon.current_user_id,
- name: gon.current_user_fullname,
- username: gon.current_username,
- avatar_url: gon.current_user_avatar_url,
- },
- ];
+ preloadedAuthors.push({
+ id: convertToGraphQLId(TYPE_USER, gon.current_user_id),
+ name: gon.current_user_fullname,
+ username: gon.current_username,
+ avatar_url: gon.current_user_avatar_url,
+ });
}
const tokens = [
@@ -252,6 +285,7 @@ export default {
dataType: 'user',
unique: true,
defaultAuthors: [],
+ operators: OPERATOR_IS_ONLY,
fetchAuthors: this.fetchUsers,
preloadedAuthors,
},
@@ -280,7 +314,7 @@ export default {
title: TOKEN_TITLE_LABEL,
icon: 'labels',
token: LabelToken,
- defaultLabels: [],
+ defaultLabels: DEFAULT_NONE_ANY,
fetchLabels: this.fetchLabels,
},
];
@@ -310,7 +344,7 @@ export default {
});
}
- if (this.projectIterationsPath) {
+ if (this.hasIterationsFeature) {
tokens.push({
type: TOKEN_TYPE_ITERATION,
title: TOKEN_TITLE_ITERATION,
@@ -329,6 +363,7 @@ export default {
token: EpicToken,
unique: true,
idProperty: 'id',
+ useIdValue: true,
fetchEpics: this.fetchEpics,
});
}
@@ -346,37 +381,28 @@ export default {
return tokens;
},
showPaginationControls() {
- return this.issues.length > 0;
+ return this.issues.length > 0 && (this.pageInfo.hasNextPage || this.pageInfo.hasPreviousPage);
},
sortOptions() {
return getSortOptions(this.hasIssueWeightsFeature, this.hasBlockedIssuesFeature);
},
tabCounts() {
- return Object.values(IssuableStates).reduce(
- (acc, state) => ({
- ...acc,
- [state]: this.state === state ? this.totalIssues : undefined,
- }),
- {},
- );
+ return {
+ [IssuableStates.Opened]: this.countOpened,
+ [IssuableStates.Closed]: this.countClosed,
+ [IssuableStates.All]: this.countAll,
+ };
+ },
+ currentTabCount() {
+ return this.tabCounts[this.state] ?? 0;
},
urlParams() {
- const filterParams = {
- ...this.urlFilterParams,
- };
-
- if (filterParams.epic_id) {
- filterParams.epic_id = encodeURIComponent(filterParams.epic_id);
- } else if (filterParams['not[epic_id]']) {
- filterParams['not[epic_id]'] = encodeURIComponent(filterParams['not[epic_id]']);
- }
-
return {
due_date: this.dueDateFilter,
search: this.searchQuery,
+ sort: urlSortParams[this.sortKey],
state: this.state,
- ...urlSortParams[this.sortKey],
- ...filterParams,
+ ...this.urlFilterParams,
};
},
},
@@ -418,16 +444,42 @@ export default {
: epics.filter((epic) => epic.id === number);
},
fetchLabels(search) {
- return this.fetchWithCache(this.projectLabelsPath, 'labels', 'title', search);
+ return this.$apollo
+ .query({
+ query: searchLabelsQuery,
+ variables: { projectPath: this.projectPath, search },
+ })
+ .then(({ data }) => data.project.labels.nodes);
},
fetchMilestones(search) {
- return this.fetchWithCache(this.projectMilestonesPath, 'milestones', 'title', search, true);
+ return this.$apollo
+ .query({
+ query: searchMilestonesQuery,
+ variables: { projectPath: this.projectPath, search },
+ })
+ .then(({ data }) => data.project.milestones.nodes);
},
fetchIterations(search) {
- return axios.get(this.projectIterationsPath, { params: { search } });
+ const id = Number(search);
+ const variables =
+ !search || Number.isNaN(id)
+ ? { projectPath: this.projectPath, search }
+ : { projectPath: this.projectPath, id };
+
+ return this.$apollo
+ .query({
+ query: searchIterationsQuery,
+ variables,
+ })
+ .then(({ data }) => data.project.iterations.nodes);
},
fetchUsers(search) {
- return axios.get(this.autocompleteUsersPath, { params: { search } });
+ return this.$apollo
+ .query({
+ query: searchUsersQuery,
+ variables: { projectPath: this.projectPath, search },
+ })
+ .then(({ data }) => data.project.projectMembers.nodes.map((member) => member.user));
},
getExportCsvPathWithQuery() {
return `${this.exportCsvPath}${window.location.search}`;
@@ -450,7 +502,9 @@ export default {
},
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
- const initBulkUpdateSidebar = await import('~/issuable_init_bulk_update_sidebar');
+ const initBulkUpdateSidebar = await import(
+ '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar'
+ );
initBulkUpdateSidebar.default.init('issuable_');
const usersSelect = await import('~/users_select');
@@ -469,6 +523,7 @@ export default {
this.state = state;
},
handleFilter(filter) {
+ this.pageParams = initialPageParams;
this.filterTokens = filter;
},
handleNextPage() {
@@ -581,7 +636,7 @@ export default {
v-if="isSignedIn"
class="gl-md-mr-3"
:export-csv-path="exportCsvPathWithQuery"
- :issuable-count="totalIssues"
+ :issuable-count="currentTabCount"
/>
<gl-button
v-if="canBulkUpdate"
@@ -609,7 +664,7 @@ export default {
v-gl-tooltip
class="gl-display-none gl-sm-display-block"
:title="$options.i18n.relatedMergeRequests"
- data-testid="issuable-mr"
+ data-testid="merge-requests"
>
<gl-icon name="merge-request" />
{{ issuable.mergeRequestsCount }}
@@ -617,7 +672,7 @@ export default {
<li
v-if="issuable.upvotes"
v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
+ class="issuable-upvotes gl-display-none gl-sm-display-block"
:title="$options.i18n.upvotes"
data-testid="issuable-upvotes"
>
@@ -627,7 +682,7 @@ export default {
<li
v-if="issuable.downvotes"
v-gl-tooltip
- class="gl-display-none gl-sm-display-block"
+ class="issuable-downvotes gl-display-none gl-sm-display-block"
:title="$options.i18n.downvotes"
data-testid="issuable-downvotes"
>
@@ -635,9 +690,10 @@ export default {
{{ issuable.downvotes }}
</li>
<blocking-issues-count
- class="gl-display-none gl-sm-display-block"
- :blocking-issues-count="issuable.blockedByCount"
+ class="blocking-issues gl-display-none gl-sm-display-block"
+ :blocking-issues-count="issuable.blockingCount"
:is-list-item="true"
+ data-testid="blocking-issues"
/>
</template>
@@ -692,7 +748,7 @@ export default {
<csv-import-export-buttons
class="gl-mr-3"
:export-csv-path="exportCsvPathWithQuery"
- :issuable-count="totalIssues"
+ :issuable-count="currentTabCount"
/>
</template>
</gl-empty-state>
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues_list/constants.js
index 76006f9081d..d94d4b9a19a 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues_list/constants.js
@@ -1,3 +1,5 @@
+import getIssuesCountQuery from 'ee_else_ce/issues_list/queries/get_issues_count.query.graphql';
+import createFlash from '~/flash';
import { __, s__ } from '~/locale';
import {
FILTER_ANY,
@@ -68,6 +70,7 @@ export const i18n = {
confidentialYes: __('Yes'),
downvotes: __('Downvotes'),
editIssues: __('Edit issues'),
+ errorFetchingCounts: __('An error occurred while getting issue counts'),
errorFetchingIssues: __('An error occurred while loading issues'),
jiraIntegrationMessage: s__(
'JiraService|%{jiraDocsLinkStart}Enable the Jira integration%{jiraDocsLinkEnd} to view your Jira issues in GitLab.',
@@ -94,7 +97,7 @@ export const i18n = {
relatedMergeRequests: __('Related merge requests'),
reorderError: __('An error occurred while reordering issues.'),
rssLabel: __('Subscribe to RSS feed'),
- searchPlaceholder: __('Search or filter results…'),
+ searchPlaceholder: __('Search or filter results...'),
upvotes: __('Upvotes'),
};
@@ -128,21 +131,21 @@ 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_DESC = 'RELATIVE_POSITION_DESC';
+export const RELATIVE_POSITION_ASC = 'RELATIVE_POSITION_ASC';
export const UPDATED_ASC = 'UPDATED_ASC';
export const UPDATED_DESC = 'UPDATED_DESC';
export const WEIGHT_ASC = 'WEIGHT_ASC';
export const WEIGHT_DESC = 'WEIGHT_DESC';
-const SORT_ASC = 'asc';
-const SORT_DESC = 'desc';
-
+const PRIORITY_ASC_SORT = 'priority_asc';
const CREATED_DATE_SORT = 'created_date';
const CREATED_ASC_SORT = 'created_asc';
const UPDATED_DESC_SORT = 'updated_desc';
@@ -150,129 +153,30 @@ const UPDATED_ASC_SORT = 'updated_asc';
const MILESTONE_SORT = 'milestone';
const MILESTONE_DUE_DESC_SORT = 'milestone_due_desc';
const DUE_DATE_DESC_SORT = 'due_date_desc';
+const LABEL_PRIORITY_ASC_SORT = 'label_priority_asc';
const POPULARITY_ASC_SORT = 'popularity_asc';
const WEIGHT_DESC_SORT = 'weight_desc';
const BLOCKING_ISSUES_DESC_SORT = 'blocking_issues_desc';
-const BLOCKING_ISSUES = 'blocking_issues';
-
-export const apiSortParams = {
- [PRIORITY_DESC]: {
- order_by: PRIORITY,
- sort: SORT_DESC,
- },
- [CREATED_ASC]: {
- order_by: CREATED_AT,
- sort: SORT_ASC,
- },
- [CREATED_DESC]: {
- order_by: CREATED_AT,
- sort: SORT_DESC,
- },
- [UPDATED_ASC]: {
- order_by: UPDATED_AT,
- sort: SORT_ASC,
- },
- [UPDATED_DESC]: {
- order_by: UPDATED_AT,
- sort: SORT_DESC,
- },
- [MILESTONE_DUE_ASC]: {
- order_by: MILESTONE_DUE,
- sort: SORT_ASC,
- },
- [MILESTONE_DUE_DESC]: {
- order_by: MILESTONE_DUE,
- sort: SORT_DESC,
- },
- [DUE_DATE_ASC]: {
- order_by: DUE_DATE,
- sort: SORT_ASC,
- },
- [DUE_DATE_DESC]: {
- order_by: DUE_DATE,
- sort: SORT_DESC,
- },
- [POPULARITY_ASC]: {
- order_by: POPULARITY,
- sort: SORT_ASC,
- },
- [POPULARITY_DESC]: {
- order_by: POPULARITY,
- sort: SORT_DESC,
- },
- [LABEL_PRIORITY_DESC]: {
- order_by: LABEL_PRIORITY,
- sort: SORT_DESC,
- },
- [RELATIVE_POSITION_DESC]: {
- order_by: RELATIVE_POSITION,
- per_page: 100,
- sort: SORT_ASC,
- },
- [WEIGHT_ASC]: {
- order_by: WEIGHT,
- sort: SORT_ASC,
- },
- [WEIGHT_DESC]: {
- order_by: WEIGHT,
- sort: SORT_DESC,
- },
- [BLOCKING_ISSUES_DESC]: {
- order_by: BLOCKING_ISSUES,
- sort: SORT_DESC,
- },
-};
export const urlSortParams = {
- [PRIORITY_DESC]: {
- sort: PRIORITY,
- },
- [CREATED_ASC]: {
- sort: CREATED_ASC_SORT,
- },
- [CREATED_DESC]: {
- sort: CREATED_DATE_SORT,
- },
- [UPDATED_ASC]: {
- sort: UPDATED_ASC_SORT,
- },
- [UPDATED_DESC]: {
- sort: UPDATED_DESC_SORT,
- },
- [MILESTONE_DUE_ASC]: {
- sort: MILESTONE_SORT,
- },
- [MILESTONE_DUE_DESC]: {
- sort: MILESTONE_DUE_DESC_SORT,
- },
- [DUE_DATE_ASC]: {
- sort: DUE_DATE,
- },
- [DUE_DATE_DESC]: {
- sort: DUE_DATE_DESC_SORT,
- },
- [POPULARITY_ASC]: {
- sort: POPULARITY_ASC_SORT,
- },
- [POPULARITY_DESC]: {
- sort: POPULARITY,
- },
- [LABEL_PRIORITY_DESC]: {
- sort: LABEL_PRIORITY,
- },
- [RELATIVE_POSITION_DESC]: {
- sort: RELATIVE_POSITION,
- per_page: 100,
- },
- [WEIGHT_ASC]: {
- sort: WEIGHT,
- },
- [WEIGHT_DESC]: {
- sort: WEIGHT_DESC_SORT,
- },
- [BLOCKING_ISSUES_DESC]: {
- sort: BLOCKING_ISSUES_DESC_SORT,
- },
+ [PRIORITY_ASC]: PRIORITY_ASC_SORT,
+ [PRIORITY_DESC]: PRIORITY,
+ [CREATED_ASC]: CREATED_ASC_SORT,
+ [CREATED_DESC]: CREATED_DATE_SORT,
+ [UPDATED_ASC]: UPDATED_ASC_SORT,
+ [UPDATED_DESC]: UPDATED_DESC_SORT,
+ [MILESTONE_DUE_ASC]: MILESTONE_SORT,
+ [MILESTONE_DUE_DESC]: MILESTONE_DUE_DESC_SORT,
+ [DUE_DATE_ASC]: DUE_DATE,
+ [DUE_DATE_DESC]: DUE_DATE_DESC_SORT,
+ [POPULARITY_ASC]: POPULARITY_ASC_SORT,
+ [POPULARITY_DESC]: POPULARITY,
+ [LABEL_PRIORITY_ASC]: LABEL_PRIORITY_ASC_SORT,
+ [LABEL_PRIORITY_DESC]: LABEL_PRIORITY,
+ [RELATIVE_POSITION_ASC]: RELATIVE_POSITION,
+ [WEIGHT_ASC]: WEIGHT,
+ [WEIGHT_DESC]: WEIGHT_DESC_SORT,
+ [BLOCKING_ISSUES_DESC]: BLOCKING_ISSUES_DESC_SORT,
};
export const MAX_LIST_SIZE = 10;
@@ -297,12 +201,7 @@ export const TOKEN_TYPE_WEIGHT = 'weight';
export const filters = {
[TOKEN_TYPE_AUTHOR]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'author_username',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[author_username]',
- },
+ [NORMAL_FILTER]: 'authorUsername',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -315,13 +214,8 @@ export const filters = {
},
[TOKEN_TYPE_ASSIGNEE]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'assignee_username',
- [SPECIAL_FILTER]: 'assignee_id',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[assignee_username]',
- },
+ [NORMAL_FILTER]: 'assigneeUsernames',
+ [SPECIAL_FILTER]: 'assigneeId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -336,12 +230,7 @@ export const filters = {
},
[TOKEN_TYPE_MILESTONE]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'milestone',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[milestone]',
- },
+ [NORMAL_FILTER]: 'milestoneTitle',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -354,16 +243,13 @@ export const filters = {
},
[TOKEN_TYPE_LABEL]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'labels',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[labels]',
- },
+ [NORMAL_FILTER]: 'labelName',
+ [SPECIAL_FILTER]: 'labelName',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
[NORMAL_FILTER]: 'label_name[]',
+ [SPECIAL_FILTER]: 'label_name[]',
},
[OPERATOR_IS_NOT]: {
[NORMAL_FILTER]: 'not[label_name][]',
@@ -372,10 +258,8 @@ export const filters = {
},
[TOKEN_TYPE_MY_REACTION]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'my_reaction_emoji',
- [SPECIAL_FILTER]: 'my_reaction_emoji',
- },
+ [NORMAL_FILTER]: 'myReactionEmoji',
+ [SPECIAL_FILTER]: 'myReactionEmoji',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -386,9 +270,7 @@ export const filters = {
},
[TOKEN_TYPE_CONFIDENTIAL]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'confidential',
- },
+ [NORMAL_FILTER]: 'confidential',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -398,33 +280,23 @@ export const filters = {
},
[TOKEN_TYPE_ITERATION]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'iteration_title',
- [SPECIAL_FILTER]: 'iteration_id',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[iteration_title]',
- },
+ [NORMAL_FILTER]: 'iterationId',
+ [SPECIAL_FILTER]: 'iterationWildcardId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
- [NORMAL_FILTER]: 'iteration_title',
+ [NORMAL_FILTER]: 'iteration_id',
[SPECIAL_FILTER]: 'iteration_id',
},
[OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[iteration_title]',
+ [NORMAL_FILTER]: 'not[iteration_id]',
},
},
},
[TOKEN_TYPE_EPIC]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'epic_id',
- [SPECIAL_FILTER]: 'epic_id',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[epic_id]',
- },
+ [NORMAL_FILTER]: 'epicId',
+ [SPECIAL_FILTER]: 'epicId',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -438,13 +310,8 @@ export const filters = {
},
[TOKEN_TYPE_WEIGHT]: {
[API_PARAM]: {
- [OPERATOR_IS]: {
- [NORMAL_FILTER]: 'weight',
- [SPECIAL_FILTER]: 'weight',
- },
- [OPERATOR_IS_NOT]: {
- [NORMAL_FILTER]: 'not[weight]',
- },
+ [NORMAL_FILTER]: 'weight',
+ [SPECIAL_FILTER]: 'weight',
},
[URL_PARAM]: {
[OPERATOR_IS]: {
@@ -457,3 +324,15 @@ export const filters = {
},
},
};
+
+export const issuesCountSmartQueryBase = {
+ query: getIssuesCountQuery,
+ context: {
+ isSingleRequest: true,
+ },
+ update: ({ project }) => project?.issues.count,
+ error(error) {
+ createFlash({ message: i18n.errorFetchingCounts, captureError: true, error });
+ },
+ debounce: 200,
+};
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues_list/index.js
index 97b9a9a115d..71ceb9bef55 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues_list/index.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { IssuableType } from '~/issue_show/constants';
import IssuesListApp from '~/issues_list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
@@ -82,7 +81,6 @@ export function mountIssuesListApp() {
const {
autocompleteAwardEmojisPath,
- autocompleteUsersPath,
calendarPath,
canBulkUpdate,
canEdit,
@@ -95,6 +93,7 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
hasIssueWeightsFeature,
+ hasIterationsFeature,
hasMultipleIssueAssigneesFeature,
hasProjectIssues,
importCsvIssuesPath,
@@ -106,9 +105,6 @@ export function mountIssuesListApp() {
maxAttachmentSize,
newIssuePath,
projectImportJiraPath,
- projectIterationsPath,
- projectLabelsPath,
- projectMilestonesPath,
projectPath,
quickActionsHelpPath,
resetPath,
@@ -122,7 +118,6 @@ export function mountIssuesListApp() {
apolloProvider,
provide: {
autocompleteAwardEmojisPath,
- autocompleteUsersPath,
calendarPath,
canBulkUpdate: parseBoolean(canBulkUpdate),
emptyStateSvgPath,
@@ -130,15 +125,13 @@ export function mountIssuesListApp() {
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
+ hasIterationsFeature: parseBoolean(hasIterationsFeature),
hasMultipleIssueAssigneesFeature: parseBoolean(hasMultipleIssueAssigneesFeature),
hasProjectIssues: parseBoolean(hasProjectIssues),
isSignedIn: parseBoolean(isSignedIn),
issuesPath,
jiraIntegrationPath,
newIssuePath,
- projectIterationsPath,
- projectLabelsPath,
- projectMilestonesPath,
projectPath,
rssPath,
showNewIssueLink: parseBoolean(showNewIssueLink),
@@ -156,7 +149,6 @@ export function mountIssuesListApp() {
// For IssuableByEmail component
emailsHelpPagePath,
initialEmail,
- issuableType: IssuableType.Issue,
markdownHelpPath,
quickActionsHelpPath,
resetPath,
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 afd53084ca0..124190915c0 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
@@ -2,6 +2,7 @@
#import "./issue.fragment.graphql"
query getProjectIssues(
+ $isSignedIn: Boolean = false
$projectPath: ID!
$search: String
$sort: IssueSort
@@ -33,7 +34,6 @@ query getProjectIssues(
first: $firstPageSize
last: $lastPageSize
) {
- count
pageInfo {
...PageInfo
}
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
new file mode 100644
index 00000000000..a1742859640
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/get_issues_count.query.graphql
@@ -0,0 +1,26 @@
+query getProjectIssuesCount(
+ $projectPath: ID!
+ $search: String
+ $state: IssuableState
+ $assigneeId: String
+ $assigneeUsernames: [String!]
+ $authorUsername: String
+ $labelName: [String]
+ $milestoneTitle: [String]
+ $not: NegatedIssueFilterInput
+) {
+ project(fullPath: $projectPath) {
+ issues(
+ search: $search
+ state: $state
+ assigneeId: $assigneeId
+ assigneeUsernames: $assigneeUsernames
+ authorUsername: $authorUsername
+ labelName: $labelName
+ milestoneTitle: $milestoneTitle
+ not: $not
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
index de30d8b4bf6..f7ebf64ffb8 100644
--- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
@@ -11,7 +11,7 @@ fragment IssueFragment on Issue {
title
updatedAt
upvotes
- userDiscussionsCount
+ userDiscussionsCount @include(if: $isSignedIn)
webUrl
assignees {
nodes {
diff --git a/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
new file mode 100644
index 00000000000..11d9dcea573
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/search_iterations.query.graphql
@@ -0,0 +1,10 @@
+query searchIterations($projectPath: ID!, $search: String, $id: ID) {
+ project(fullPath: $projectPath) {
+ iterations(title: $search, id: $id) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
new file mode 100644
index 00000000000..de884e1221c
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
@@ -0,0 +1,12 @@
+query searchLabels($projectPath: ID!, $search: String) {
+ project(fullPath: $projectPath) {
+ labels(searchTerm: $search, includeAncestorGroups: true) {
+ nodes {
+ id
+ color
+ textColor
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
new file mode 100644
index 00000000000..91f74fd220b
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
@@ -0,0 +1,10 @@
+query searchMilestones($projectPath: ID!, $search: String) {
+ project(fullPath: $projectPath) {
+ milestones(searchTitle: $search, includeAncestors: true) {
+ nodes {
+ id
+ title
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/queries/search_users.query.graphql b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
new file mode 100644
index 00000000000..953157cfe3a
--- /dev/null
+++ b/app/assets/javascripts/issues_list/queries/search_users.query.graphql
@@ -0,0 +1,14 @@
+query searchUsers($projectPath: ID!, $search: String) {
+ project(fullPath: $projectPath) {
+ projectMembers(search: $search) {
+ nodes {
+ user {
+ id
+ avatarUrl
+ name
+ username
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues_list/utils.js
index b5ec44198da..49f937cc453 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues_list/utils.js
@@ -1,4 +1,5 @@
import {
+ API_PARAM,
BLOCKING_ISSUES_DESC,
CREATED_ASC,
CREATED_DESC,
@@ -6,29 +7,36 @@ import {
DUE_DATE_DESC,
DUE_DATE_VALUES,
filters,
+ LABEL_PRIORITY_ASC,
LABEL_PRIORITY_DESC,
MILESTONE_DUE_ASC,
MILESTONE_DUE_DESC,
NORMAL_FILTER,
POPULARITY_ASC,
POPULARITY_DESC,
+ PRIORITY_ASC,
PRIORITY_DESC,
- RELATIVE_POSITION_DESC,
+ RELATIVE_POSITION_ASC,
SPECIAL_FILTER,
SPECIAL_FILTER_VALUES,
TOKEN_TYPE_ASSIGNEE,
+ TOKEN_TYPE_ITERATION,
UPDATED_ASC,
UPDATED_DESC,
+ URL_PARAM,
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
} from '~/issues_list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
-import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
+import {
+ FILTERED_SEARCH_TERM,
+ OPERATOR_IS_NOT,
+} from '~/vue_shared/components/filtered_search_bar/constants';
export const getSortKey = (sort) =>
- Object.keys(urlSortParams).find((key) => urlSortParams[key].sort === sort);
+ Object.keys(urlSortParams).find((key) => urlSortParams[key] === sort);
export const getDueDateValue = (value) => (DUE_DATE_VALUES.includes(value) ? value : undefined);
@@ -38,7 +46,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 1,
title: __('Priority'),
sortDirection: {
- ascending: PRIORITY_DESC,
+ ascending: PRIORITY_ASC,
descending: PRIORITY_DESC,
},
},
@@ -86,7 +94,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 7,
title: __('Label priority'),
sortDirection: {
- ascending: LABEL_PRIORITY_DESC,
+ ascending: LABEL_PRIORITY_ASC,
descending: LABEL_PRIORITY_DESC,
},
},
@@ -94,8 +102,8 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
id: 8,
title: __('Manual'),
sortDirection: {
- ascending: RELATIVE_POSITION_DESC,
- descending: RELATIVE_POSITION_DESC,
+ ascending: RELATIVE_POSITION_ASC,
+ descending: RELATIVE_POSITION_ASC,
},
},
];
@@ -128,7 +136,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
const tokenTypes = Object.keys(filters);
const getUrlParams = (tokenType) =>
- Object.values(filters[tokenType].urlParam).flatMap((filterObj) => Object.values(filterObj));
+ Object.values(filters[tokenType][URL_PARAM]).flatMap((filterObj) => Object.values(filterObj));
const urlParamKeys = tokenTypes.flatMap(getUrlParams);
@@ -136,7 +144,7 @@ const getTokenTypeFromUrlParamKey = (urlParamKey) =>
tokenTypes.find((tokenType) => getUrlParams(tokenType).includes(urlParamKey));
const getOperatorFromUrlParamKey = (tokenType, urlParamKey) =>
- Object.entries(filters[tokenType].urlParam).find(([, filterObj]) =>
+ Object.entries(filters[tokenType][URL_PARAM]).find(([, filterObj]) =>
Object.values(filterObj).includes(urlParamKey),
)[0];
@@ -178,12 +186,36 @@ const getFilterType = (data, tokenType = '') =>
? SPECIAL_FILTER
: NORMAL_FILTER;
-export const convertToParams = (filterTokens, paramType) =>
+const isIterationSpecialValue = (tokenType, value) =>
+ tokenType === TOKEN_TYPE_ITERATION && SPECIAL_FILTER_VALUES.includes(value);
+
+export const convertToApiParams = (filterTokens) => {
+ const params = {};
+ const not = {};
+
+ 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 obj = token.value.operator === OPERATOR_IS_NOT ? not : params;
+ const data = isIterationSpecialValue(token.type, token.value.data)
+ ? token.value.data.toUpperCase()
+ : token.value.data;
+ Object.assign(obj, {
+ [field]: obj[field] ? [obj[field], data].flat() : data,
+ });
+ });
+
+ return Object.keys(not).length ? Object.assign(params, { not }) : params;
+};
+
+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][paramType][token.value.operator]?.[filterType];
+ const param = 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,
});
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
new file mode 100644
index 00000000000..c1f57be7f97
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { PROJECTS_PER_PAGE } from '../constants';
+import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
+
+export default {
+ PROJECTS_PER_PAGE,
+ projectQueryPageInfo: {
+ endCursor: '',
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ },
+ props: {
+ selectedProject: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ initialProjectsLoading: true,
+ projectSearchQuery: '',
+ };
+ },
+ apollo: {
+ projects: {
+ query: getProjectsQuery,
+ variables() {
+ return {
+ search: this.projectSearchQuery,
+ first: this.$options.PROJECTS_PER_PAGE,
+ after: this.$options.projectQueryPageInfo.endCursor,
+ searchNamespaces: true,
+ sort: 'similarity',
+ };
+ },
+ update(data) {
+ return data?.projects?.nodes.filter((project) => !project.repository.empty) ?? [];
+ },
+ result() {
+ this.initialProjectsLoading = false;
+ },
+ error() {
+ this.onError({ message: __('Failed to load projects') });
+ },
+ },
+ },
+ computed: {
+ projectsLoading() {
+ return Boolean(this.$apollo.queries.projects.loading);
+ },
+ projectDropdownText() {
+ return this.selectedProject?.nameWithNamespace || __('Select a project');
+ },
+ },
+ methods: {
+ async onProjectSelect(project) {
+ this.$emit('change', project);
+ },
+ onError({ message } = {}) {
+ this.$emit('error', { message });
+ },
+ isProjectSelected(project) {
+ return project.id === this.selectedProject?.id;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown :text="projectDropdownText" :loading="initialProjectsLoading">
+ <template #header>
+ <gl-search-box-by-type v-model.trim="projectSearchQuery" :debounce="250" />
+ </template>
+
+ <gl-loading-icon v-show="projectsLoading" />
+ <template v-if="!projectsLoading">
+ <gl-dropdown-item
+ v-for="project in projects"
+ :key="project.id"
+ is-check-item
+ :is-checked="isProjectSelected(project)"
+ @click="onProjectSelect(project)"
+ >
+ {{ project.nameWithNamespace }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+</template>
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
new file mode 100644
index 00000000000..0e2d8821f36
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/components/source_branch_dropdown.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType, GlLoadingIcon } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { BRANCHES_PER_PAGE } from '../constants';
+import getProjectQuery from '../graphql/queries/get_project.query.graphql';
+
+export default {
+ BRANCHES_PER_PAGE,
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlLoadingIcon,
+ },
+ props: {
+ selectedProject: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ selectedBranchName: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ data() {
+ return {
+ sourceBranchSearchQuery: '',
+ initialSourceBranchNamesLoading: false,
+ sourceBranchNamesLoading: false,
+ sourceBranchNames: [],
+ };
+ },
+ computed: {
+ hasSelectedProject() {
+ return Boolean(this.selectedProject);
+ },
+ hasSelectedSourceBranch() {
+ return Boolean(this.selectedBranchName);
+ },
+ branchDropdownText() {
+ return this.selectedBranchName || __('Select a branch');
+ },
+ },
+ watch: {
+ selectedProject: {
+ immediate: true,
+ async handler(selectedProject) {
+ if (!selectedProject) return;
+
+ this.initialSourceBranchNamesLoading = true;
+ await this.fetchSourceBranchNames({ projectPath: selectedProject.fullPath });
+ this.initialSourceBranchNamesLoading = false;
+ },
+ },
+ },
+ methods: {
+ onSourceBranchSelect(branchName) {
+ this.$emit('change', branchName);
+ },
+ onSourceBranchSearchQuery(branchSearchQuery) {
+ this.branchSearchQuery = branchSearchQuery;
+ this.fetchSourceBranchNames({
+ projectPath: this.selectedProject.fullPath,
+ searchPattern: this.branchSearchQuery,
+ });
+ },
+ onError({ message } = {}) {
+ this.$emit('error', { message });
+ },
+ async fetchSourceBranchNames({ projectPath, searchPattern } = {}) {
+ this.sourceBranchNamesLoading = true;
+ try {
+ const { data } = await this.$apollo.query({
+ query: getProjectQuery,
+ variables: {
+ projectPath,
+ branchNamesLimit: this.$options.BRANCHES_PER_PAGE,
+ branchNamesOffset: 0,
+ branchNamesSearchPattern: searchPattern ? `*${searchPattern}*` : '*',
+ },
+ });
+
+ const { branchNames, rootRef } = data?.project.repository || {};
+ this.sourceBranchNames = branchNames || [];
+
+ // Use root ref as the default selection
+ if (rootRef && !this.hasSelectedSourceBranch) {
+ this.onSourceBranchSelect(rootRef);
+ }
+ } catch (err) {
+ this.onError({
+ message: __('Something went wrong while fetching source branches.'),
+ });
+ } finally {
+ this.sourceBranchNamesLoading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-dropdown
+ :text="branchDropdownText"
+ :loading="initialSourceBranchNamesLoading"
+ :disabled="!hasSelectedProject"
+ :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>
+</template>
diff --git a/app/assets/javascripts/jira_connect/branches/constants.js b/app/assets/javascripts/jira_connect/branches/constants.js
new file mode 100644
index 00000000000..987c8d356b4
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/constants.js
@@ -0,0 +1,2 @@
+export const BRANCHES_PER_PAGE = 20;
+export const PROJECTS_PER_PAGE = 20;
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql
new file mode 100644
index 00000000000..f3428e816d7
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_project.query.graphql
@@ -0,0 +1,17 @@
+query getProject(
+ $projectPath: ID!
+ $branchNamesLimit: Int!
+ $branchNamesOffset: Int!
+ $branchNamesSearchPattern: String!
+) {
+ project(fullPath: $projectPath) {
+ repository {
+ branchNames(
+ limit: $branchNamesLimit
+ offset: $branchNamesOffset
+ searchPattern: $branchNamesSearchPattern
+ )
+ rootRef
+ }
+ }
+}
diff --git a/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
new file mode 100644
index 00000000000..e768154e210
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/branches/graphql/queries/get_projects.query.graphql
@@ -0,0 +1,34 @@
+#import "~/graphql_shared/fragments/pageInfo.fragment.graphql"
+
+query getProjects(
+ $search: String!
+ $after: String = ""
+ $first: Int!
+ $searchNamespaces: Boolean = false
+ $sort: String
+ $membership: Boolean = true
+) {
+ projects(
+ search: $search
+ after: $after
+ first: $first
+ membership: $membership
+ searchNamespaces: $searchNamespaces
+ sort: $sort
+ ) {
+ nodes {
+ id
+ name
+ nameWithNamespace
+ fullPath
+ avatarUrl
+ path
+ repository {
+ empty
+ }
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+}
diff --git a/app/assets/javascripts/jira_connect/components/groups_list.vue b/app/assets/javascripts/jira_connect/components/groups_list.vue
index d764f778a9d..55233bb6326 100644
--- a/app/assets/javascripts/jira_connect/components/groups_list.vue
+++ b/app/assets/javascripts/jira_connect/components/groups_list.vue
@@ -89,6 +89,7 @@ export default {
debounce="500"
:placeholder="__('Search by name')"
:is-loading="isLoadingMore"
+ :value="searchTerm"
@input="onGroupSearch"
/>
diff --git a/app/assets/javascripts/jira_import/components/jira_import_form.vue b/app/assets/javascripts/jira_import/components/jira_import_form.vue
index e7816f6d187..1b6e365fdb2 100644
--- a/app/assets/javascripts/jira_import/components/jira_import_form.vue
+++ b/app/assets/javascripts/jira_import/components/jira_import_form.vue
@@ -310,7 +310,7 @@ export default {
>
<gl-search-box-by-type v-model.trim="searchTerm" />
- <gl-loading-icon v-if="isFetching" />
+ <gl-loading-icon v-if="isFetching" size="sm" />
<gl-dropdown-item
v-for="user in users"
@@ -328,7 +328,7 @@ export default {
</template>
</gl-table>
- <gl-loading-icon v-if="isInitialLoadingState" />
+ <gl-loading-icon v-if="isInitialLoadingState" size="sm" />
<gl-button
v-if="hasMoreUsers"
diff --git a/app/assets/javascripts/jobs/components/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
index 35b16d73cc7..e31c13f40b0 100644
--- a/app/assets/javascripts/jobs/components/empty_state.vue
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -35,11 +35,6 @@ export default {
required: false,
default: false,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
action: {
type: Object,
required: false,
@@ -75,11 +70,7 @@ export default {
<p v-if="content" data-testid="job-empty-state-content">{{ content }}</p>
</div>
- <manual-variables-form
- v-if="shouldRenderManualVariables"
- :action="action"
- :variables-settings-url="variablesSettingsUrl"
- />
+ <manual-variables-form v-if="shouldRenderManualVariables" :action="action" />
<div class="text-content">
<div v-if="action && !shouldRenderManualVariables" class="text-center">
<gl-link
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index be95001a396..fa9ee56c049 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -50,11 +50,6 @@ export default {
required: false,
default: null,
},
- variablesSettingsUrl: {
- type: String,
- required: false,
- default: null,
- },
deploymentHelpUrl: {
type: String,
required: false,
@@ -315,7 +310,6 @@ export default {
:action="emptyStateAction"
:playable="job.playable"
:scheduled="job.scheduled"
- :variables-settings-url="variablesSettingsUrl"
/>
<!-- EO empty state -->
diff --git a/app/assets/javascripts/jobs/components/log/collapsible_section.vue b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
index 55cdfb691f4..c0d5fac0e8d 100644
--- a/app/assets/javascripts/jobs/components/log/collapsible_section.vue
+++ b/app/assets/javascripts/jobs/components/log/collapsible_section.vue
@@ -1,4 +1,6 @@
<script>
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants';
import LogLine from './line.vue';
import LogLineHeader from './line_header.vue';
@@ -7,7 +9,9 @@ export default {
components: {
LogLine,
LogLineHeader,
+ CollapsibleLogSection: () => import('./collapsible_section.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
props: {
section: {
type: Object,
@@ -22,6 +26,9 @@ export default {
badgeDuration() {
return this.section.line && this.section.line.section_duration;
},
+ infinitelyCollapsibleSectionsFlag() {
+ return this.glFeatures?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF];
+ },
},
methods: {
handleOnClickCollapsibleLine(section) {
@@ -40,12 +47,26 @@ export default {
@toggleLine="handleOnClickCollapsibleLine(section)"
/>
<template v-if="!section.isClosed">
- <log-line
- v-for="line in section.lines"
- :key="line.offset"
- :line="line"
- :path="traceEndpoint"
- />
+ <template v-if="infinitelyCollapsibleSectionsFlag">
+ <template v-for="line in section.lines">
+ <collapsible-log-section
+ v-if="line.isHeader"
+ :key="line.line.offset"
+ :section="line"
+ :trace-endpoint="traceEndpoint"
+ @onClickCollapsibleLine="handleOnClickCollapsibleLine"
+ />
+ <log-line v-else :key="line.offset" :line="line" :path="traceEndpoint" />
+ </template>
+ </template>
+ <template v-else>
+ <log-line
+ v-for="line in section.lines"
+ :key="line.offset"
+ :line="line"
+ :path="traceEndpoint"
+ />
+ </template>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/components/log/line_number.vue b/app/assets/javascripts/jobs/components/log/line_number.vue
index 7ca9154d2fe..c8ceac2c7ff 100644
--- a/app/assets/javascripts/jobs/components/log/line_number.vue
+++ b/app/assets/javascripts/jobs/components/log/line_number.vue
@@ -1,4 +1,6 @@
<script>
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../../constants';
+
export default {
functional: true,
props: {
@@ -14,7 +16,9 @@ export default {
render(h, { props }) {
const { lineNumber, path } = props;
- const parsedLineNumber = lineNumber + 1;
+ const parsedLineNumber = gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF]
+ ? lineNumber
+ : lineNumber + 1;
const lineId = `L${parsedLineNumber}`;
const lineHref = `${path}#${lineId}`;
diff --git a/app/assets/javascripts/jobs/components/manual_variables_form.vue b/app/assets/javascripts/jobs/components/manual_variables_form.vue
index d45012d2023..269551ff9aa 100644
--- a/app/assets/javascripts/jobs/components/manual_variables_form.vue
+++ b/app/assets/javascripts/jobs/components/manual_variables_form.vue
@@ -1,14 +1,16 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlButton } from '@gitlab/ui';
+import { GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { mapActions } from 'vuex';
-import { s__, sprintf } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
export default {
name: 'ManualVariablesForm',
components: {
GlButton,
+ GlLink,
+ GlSprintf,
},
props: {
action: {
@@ -24,11 +26,6 @@ export default {
);
},
},
- variablesSettingsUrl: {
- type: String,
- required: true,
- default: '',
- },
},
inputTypes: {
key: 'key',
@@ -37,6 +34,9 @@ export default {
i18n: {
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 {
@@ -47,17 +47,8 @@ export default {
};
},
computed: {
- helpText() {
- return sprintf(
- 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',
- ),
- {
- linkStart: `<a href="${this.variablesSettingsUrl}">`,
- linkEnd: '</a>',
- },
- false,
- );
+ variableSettings() {
+ return helpPagePath('ci/variables/index', { anchor: 'add-a-cicd-variable-to-a-project' });
},
},
watch: {
@@ -188,8 +179,14 @@ export default {
</div>
</div>
</div>
- <div class="d-flex gl-mt-3 justify-content-center">
- <p class="text-muted" data-testid="form-help-text" v-html="helpText"></p>
+ <div class="gl-text-center gl-mt-3">
+ <gl-sprintf :message="$options.i18n.formHelpText">
+ <template #link="{ content }">
+ <gl-link :href="variableSettings" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
</div>
<div class="d-flex justify-content-center">
<gl-button
diff --git a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
index 98badb96ed7..a6eff743ce9 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -46,7 +46,7 @@ export default {
return timeIntervalInWords(this.job.queued);
},
runnerHelpUrl() {
- return helpPagePath('ci/runners/README.html', {
+ return helpPagePath('ci/runners/index.html', {
anchor: 'set-maximum-job-timeout-for-a-runner',
});
},
diff --git a/app/assets/javascripts/jobs/constants.js b/app/assets/javascripts/jobs/constants.js
index 3040d4e2379..97f31eee57c 100644
--- a/app/assets/javascripts/jobs/constants.js
+++ b/app/assets/javascripts/jobs/constants.js
@@ -24,3 +24,5 @@ export const JOB_RETRY_FORWARD_DEPLOYMENT_MODAL = {
};
export const SUCCESS_STATUS = 'SUCCESS';
+
+export const INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF = 'infinitelyCollapsibleSections';
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 260190f5043..1fb6a6f9850 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -15,7 +15,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
@@ -41,7 +40,6 @@ export default () => {
deploymentHelpUrl,
codeQualityHelpUrl,
runnerSettingsUrl,
- variablesSettingsUrl,
subscriptionsMoreMinutesUrl,
endpoint,
pagePath,
diff --git a/app/assets/javascripts/jobs/store/actions.js b/app/assets/javascripts/jobs/store/actions.js
index c89aeada69d..a8be5d8d039 100644
--- a/app/assets/javascripts/jobs/store/actions.js
+++ b/app/assets/javascripts/jobs/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
import httpStatusCodes from '~/lib/utils/http_status';
@@ -99,7 +99,9 @@ export const receiveJobSuccess = ({ commit }, data = {}) => {
};
export const receiveJobError = ({ commit }) => {
commit(types.RECEIVE_JOB_ERROR);
- flash(__('An error occurred while fetching the job.'));
+ createFlash({
+ message: __('An error occurred while fetching the job.'),
+ });
resetFavicon();
};
@@ -197,11 +199,15 @@ export const stopPollingTrace = ({ state, commit }) => {
export const receiveTraceSuccess = ({ commit }, log) => commit(types.RECEIVE_TRACE_SUCCESS, log);
export const receiveTraceError = ({ dispatch }) => {
dispatch('stopPollingTrace');
- flash(__('An error occurred while fetching the job log.'));
+ createFlash({
+ message: __('An error occurred while fetching the job log.'),
+ });
};
export const receiveTraceUnauthorizedError = ({ dispatch }) => {
dispatch('stopPollingTrace');
- flash(__('The current user is not authorized to access the job log.'));
+ createFlash({
+ message: __('The current user is not authorized to access the job log.'),
+ });
};
/**
* When the user clicks a collapsible line in the job
@@ -240,7 +246,9 @@ export const receiveJobsForStageSuccess = ({ commit }, data) =>
commit(types.RECEIVE_JOBS_FOR_STAGE_SUCCESS, data);
export const receiveJobsForStageError = ({ commit }) => {
commit(types.RECEIVE_JOBS_FOR_STAGE_ERROR);
- flash(__('An error occurred while fetching the jobs.'));
+ createFlash({
+ message: __('An error occurred while fetching the jobs.'),
+ });
};
export const triggerManualJob = ({ state }, variables) => {
@@ -254,5 +262,9 @@ export const triggerManualJob = ({ state }, variables) => {
.post(state.job.status.action.path, {
job_variables_attributes: parsedVariables,
})
- .catch(() => flash(__('An error occurred while triggering the job.')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while triggering the job.'),
+ }),
+ );
};
diff --git a/app/assets/javascripts/jobs/store/mutations.js b/app/assets/javascripts/jobs/store/mutations.js
index 924b811d0d6..4045d8a0c16 100644
--- a/app/assets/javascripts/jobs/store/mutations.js
+++ b/app/assets/javascripts/jobs/store/mutations.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
+import { INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF } from '../constants';
import * as types from './mutation_types';
-import { logLinesParser, updateIncrementalTrace } from './utils';
+import { logLinesParser, logLinesParserLegacy, updateIncrementalTrace } from './utils';
export default {
[types.SET_JOB_ENDPOINT](state, endpoint) {
@@ -20,12 +21,26 @@ export default {
},
[types.RECEIVE_TRACE_SUCCESS](state, log = {}) {
+ const infinitelyCollapsibleSectionsFlag =
+ gon.features?.[INFINITELY_NESTED_COLLAPSIBLE_SECTIONS_FF];
if (log.state) {
state.traceState = log.state;
}
if (log.append) {
- state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
+ if (infinitelyCollapsibleSectionsFlag) {
+ if (log.lines) {
+ const parsedResult = logLinesParser(
+ log.lines,
+ state.auxiliaryPartialTraceHelpers,
+ state.trace,
+ );
+ state.trace = parsedResult.parsedLines;
+ state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ }
+ } else {
+ state.trace = log.lines ? updateIncrementalTrace(log.lines, state.trace) : state.trace;
+ }
state.traceSize += log.size;
} else {
@@ -33,7 +48,14 @@ export default {
// the trace response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
- state.trace = log.lines ? logLinesParser(log.lines) : state.trace;
+
+ if (infinitelyCollapsibleSectionsFlag) {
+ const parsedResult = logLinesParser(log.lines);
+ state.trace = parsedResult.parsedLines;
+ state.auxiliaryPartialTraceHelpers = parsedResult.auxiliaryPartialTraceHelpers;
+ } else {
+ state.trace = log.lines ? logLinesParserLegacy(log.lines) : state.trace;
+ }
state.traceSize = log.size || state.traceSize;
}
diff --git a/app/assets/javascripts/jobs/store/state.js b/app/assets/javascripts/jobs/store/state.js
index 2fe945b2985..718324c8bad 100644
--- a/app/assets/javascripts/jobs/store/state.js
+++ b/app/assets/javascripts/jobs/store/state.js
@@ -30,4 +30,7 @@ export default () => ({
selectedStage: '',
stages: [],
jobs: [],
+
+ // to parse partial logs
+ auxiliaryPartialTraceHelpers: {},
});
diff --git a/app/assets/javascripts/jobs/store/utils.js b/app/assets/javascripts/jobs/store/utils.js
index a0e0a0fb8bd..36391a4d433 100644
--- a/app/assets/javascripts/jobs/store/utils.js
+++ b/app/assets/javascripts/jobs/store/utils.js
@@ -104,7 +104,7 @@ export const getIncrementalLineNumber = (acc) => {
* @param Array accumulator
* @returns Array parsed log lines
*/
-export const logLinesParser = (lines = [], accumulator = []) =>
+export const logLinesParserLegacy = (lines = [], accumulator = []) =>
lines.reduce(
(acc, line, index) => {
const lineNumber = accumulator.length > 0 ? getIncrementalLineNumber(acc) : index;
@@ -131,6 +131,77 @@ export const logLinesParser = (lines = [], accumulator = []) =>
[...accumulator],
);
+export const logLinesParser = (lines = [], previousTraceState = {}, prevParsedLines = []) => {
+ let currentLine = previousTraceState?.prevLineCount ?? 0;
+ let currentHeader = previousTraceState?.currentHeader;
+ let isPreviousLineHeader = previousTraceState?.isPreviousLineHeader ?? false;
+ const parsedLines = prevParsedLines.length > 0 ? prevParsedLines : [];
+ const sectionsQueue = previousTraceState?.sectionsQueue ?? [];
+
+ for (let i = 0; i < lines.length; i += 1) {
+ const line = lines[i];
+ // First run we can use the current index, later runs we have to retrieve the last number of lines
+ currentLine = previousTraceState?.prevLineCount ? currentLine + 1 : i + 1;
+
+ if (line.section_header && !isPreviousLineHeader) {
+ // If there's no previous line header that means we're at the root of the log
+
+ isPreviousLineHeader = true;
+ parsedLines.push(parseHeaderLine(line, currentLine));
+ currentHeader = { index: parsedLines.length - 1 };
+ } else if (line.section_header && isPreviousLineHeader) {
+ // If there's a current section, we can't push to the parsedLines array
+ sectionsQueue.push(currentHeader);
+ currentHeader = parseHeaderLine(line, currentLine); // Let's parse the incoming header line
+ } else if (line.section && !line.section_duration) {
+ // We're inside a collapsible section and want to parse a standard line
+ if (currentHeader?.index) {
+ // If the current section header is only an index, add the line as part of the lines
+ // array of the current collapsible section
+ parsedLines[currentHeader.index].lines.push(parseLine(line, currentLine));
+ } else {
+ // Otherwise add it to the innermost collapsible section lines array
+ currentHeader.lines.push(parseLine(line, currentLine));
+ }
+ } else if (line.section && line.section_duration) {
+ // NOTE: This marks the end of a section_header
+ const previousSection = sectionsQueue.pop();
+
+ // Add the duration to section header
+ // If at the root, just push the end to the current parsedLine,
+ // otherwise, push it to the previous sections queue
+ if (currentHeader?.index) {
+ parsedLines[currentHeader.index].line.section_duration = line.section_duration;
+ isPreviousLineHeader = false;
+ currentHeader = null;
+ } else {
+ currentHeader.line.section_duration = line.section_duration;
+
+ if (previousSection && previousSection?.index) {
+ // Is the previous section on root?
+ parsedLines[previousSection.index].lines.push(currentHeader);
+ } else if (previousSection && !previousSection?.index) {
+ previousSection.lines.push(currentHeader);
+ }
+
+ currentHeader = previousSection;
+ }
+ } else {
+ parsedLines.push(parseLine(line, currentLine));
+ }
+ }
+
+ return {
+ parsedLines,
+ auxiliaryPartialTraceHelpers: {
+ isPreviousLineHeader,
+ currentHeader,
+ sectionsQueue,
+ prevLineCount: lines.length,
+ },
+ };
+};
+
/**
* Finds the repeated offset, removes the old one
*
@@ -177,5 +248,5 @@ export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
export const updateIncrementalTrace = (newLog = [], oldParsed = []) => {
const parsedLog = findOffsetAndRemove(newLog, oldParsed);
- return logLinesParser(newLog, parsedLog);
+ return logLinesParserLegacy(newLog, parsedLog);
};
diff --git a/app/assets/javascripts/jobs/utils.js b/app/assets/javascripts/jobs/utils.js
index 122f23a5bb5..1ccecf3eb53 100644
--- a/app/assets/javascripts/jobs/utils.js
+++ b/app/assets/javascripts/jobs/utils.js
@@ -3,10 +3,10 @@
* https?:\/\/
*
* up until a disallowed character or whitespace
- * [^"<>\\^`{|}\s]+
+ * [^"<>()\\^`{|}\s]+
*
* and a disallowed character or whitespace, including non-ending chars .,:;!?
- * [^"<>\\^`{|}\s.,:;!?]
+ * [^"<>()\\^`{|}\s.,:;!?]
*/
-export const linkRegex = /(https?:\/\/[^"<>\\^`{|}\s]+[^"<>\\^`{|}\s.,:;!?])/g;
+export const linkRegex = /(https?:\/\/[^"<>()\\^`{|}\s]+[^"<>()\\^`{|}\s.,:;!?])/g;
export default { linkRegex };
diff --git a/app/assets/javascripts/label_manager.js b/app/assets/javascripts/label_manager.js
index 2a020a66fd2..e0068edbb9b 100644
--- a/app/assets/javascripts/label_manager.js
+++ b/app/assets/javascripts/label_manager.js
@@ -3,7 +3,7 @@
import $ from 'jquery';
import Sortable from 'sortablejs';
import { dispose } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -111,7 +111,11 @@ export default class LabelManager {
}
onPrioritySortUpdate() {
- this.savePrioritySort().catch(() => flash(this.errorMessage));
+ this.savePrioritySort().catch(() =>
+ createFlash({
+ message: this.errorMessage,
+ }),
+ );
}
savePrioritySort() {
@@ -123,7 +127,9 @@ export default class LabelManager {
rollbackLabelPosition($label, originalAction) {
const action = originalAction === 'remove' ? 'add' : 'remove';
this.toggleLabelPriority($label, action, false);
- flash(this.errorMessage);
+ createFlash({
+ message: this.errorMessage,
+ });
}
getSortedLabelsIds() {
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index fb88e48c9a6..a62ab301227 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -5,11 +5,11 @@
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 { isScopedLabel } from '~/lib/utils/common_utils';
import boardsStore from './boards/stores/boards_store';
import CreateLabelDropdown from './create_label';
-import { deprecatedCreateFlash as flash } from './flash';
-import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, __ } from './locale';
@@ -148,7 +148,11 @@ export default class LabelsSelect {
container: 'body',
});
})
- .catch(() => flash(__('Error saving label update.')));
+ .catch(() =>
+ createFlash({
+ message: __('Error saving label update.'),
+ }),
+ );
};
initDeprecatedJQueryDropdown($dropdown, {
showMenuAbove,
@@ -183,7 +187,11 @@ export default class LabelsSelect {
$dropdown.data('deprecatedJQueryDropdown').positionMenuAbove();
}
})
- .catch(() => flash(__('Error fetching labels.')));
+ .catch(() =>
+ createFlash({
+ message: __('Error fetching labels.'),
+ }),
+ );
},
renderRow(label) {
let colorEl;
diff --git a/app/assets/javascripts/lib/dompurify.js b/app/assets/javascripts/lib/dompurify.js
index 76624c81ed5..4357918672d 100644
--- a/app/assets/javascripts/lib/dompurify.js
+++ b/app/assets/javascripts/lib/dompurify.js
@@ -7,6 +7,8 @@ const defaultConfig = {
ADD_TAGS: ['use'],
};
+const forbiddenDataAttrs = ['data-remote', 'data-url', 'data-type', 'data-method'];
+
// Only icons urls from `gon` are allowed
const getAllowedIconUrls = (gon = window.gon) =>
[gon.sprite_file_icons, gon.sprite_icons].filter(Boolean);
@@ -44,10 +46,19 @@ const sanitizeSvgIcon = (node) => {
removeUnsafeHref(node, 'xlink:href');
};
+const sanitizeHTMLAttributes = (node) => {
+ forbiddenDataAttrs.forEach((attr) => {
+ if (node.hasAttribute(attr)) {
+ node.removeAttribute(attr);
+ }
+ });
+};
+
addHook('afterSanitizeAttributes', (node) => {
if (node.tagName.toLowerCase() === 'use') {
sanitizeSvgIcon(node);
}
+ sanitizeHTMLAttributes(node);
});
export const sanitize = (val, config = defaultConfig) => dompurifySanitize(val, config);
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index cec689a44ca..0804213cafa 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -2,12 +2,13 @@ import { InMemoryCache } from 'apollo-cache-inmemory';
import { ApolloClient } from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { BatchHttpLink } from 'apollo-link-batch-http';
-import { createHttpLink } from 'apollo-link-http';
+import { HttpLink } from 'apollo-link-http';
import { createUploadLink } from 'apollo-upload-client';
import ActionCableLink from '~/actioncable_link';
import { apolloCaptchaLink } from '~/captcha/apollo_captcha_link';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
import csrf from '~/lib/utils/csrf';
+import { objectToQuery, queryToObject } from '~/lib/utils/url_utility';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
export const fetchPolicies = {
@@ -18,6 +19,31 @@ export const fetchPolicies = {
CACHE_ONLY: 'cache-only',
};
+export const stripWhitespaceFromQuery = (url, path) => {
+ /* eslint-disable-next-line no-unused-vars */
+ const [_, params] = url.split(path);
+
+ if (!params) {
+ return url;
+ }
+
+ const decoded = decodeURIComponent(params);
+ const paramsObj = queryToObject(decoded);
+
+ if (!paramsObj.query) {
+ return url;
+ }
+
+ const stripped = paramsObj.query
+ .split(/\s+|\n/)
+ .join(' ')
+ .trim();
+ paramsObj.query = stripped;
+
+ const reassembled = objectToQuery(paramsObj);
+ return `${path}?${reassembled}`;
+};
+
export default (resolvers = {}, config = {}) => {
const {
assumeImmutableResults,
@@ -58,10 +84,31 @@ export default (resolvers = {}, config = {}) => {
});
});
+ /*
+ This custom fetcher intervention is to deal with an issue where we are using GET to access
+ eTag polling, but Apollo Client adds excessive whitespace, which causes the
+ request to fail on certain self-hosted stacks. When we can move
+ to subscriptions entirely or can land an upstream PR, this can be removed.
+
+ Related links
+ Bug report: https://gitlab.com/gitlab-org/gitlab/-/issues/329895
+ Moving to subscriptions: https://gitlab.com/gitlab-org/gitlab/-/issues/332485
+ Apollo Client issue: https://github.com/apollographql/apollo-feature-requests/issues/182
+ */
+
+ const fetchIntervention = (url, options) => {
+ return fetch(stripWhitespaceFromQuery(url, uri), options);
+ };
+
+ const requestLink = ApolloLink.split(
+ () => useGet,
+ new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
+ new BatchHttpLink(httpOptions),
+ );
+
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload || operation.getContext().isSingleRequest,
createUploadLink(httpOptions),
- useGet ? createHttpLink(httpOptions) : new BatchHttpLink(httpOptions),
);
const performanceBarLink = new ApolloLink((operation, forward) => {
@@ -99,6 +146,7 @@ export default (resolvers = {}, config = {}) => {
new StartupJSLink(),
apolloCaptchaLink,
uploadsLink,
+ requestLink,
]),
);
diff --git a/app/assets/javascripts/lib/utils/axios_utils.js b/app/assets/javascripts/lib/utils/axios_utils.js
index 204c84b879e..0a26f78e253 100644
--- a/app/assets/javascripts/lib/utils/axios_utils.js
+++ b/app/assets/javascripts/lib/utils/axios_utils.js
@@ -1,4 +1,5 @@
import axios from 'axios';
+import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_interceptor';
import setupAxiosStartupCalls from './axios_startup_calls';
import csrf from './csrf';
import suppressAjaxErrorsDuringNavigation from './suppress_ajax_errors_during_navigation';
@@ -41,6 +42,8 @@ axios.interceptors.response.use(
(err) => suppressAjaxErrorsDuringNavigation(err, isUserNavigating),
);
+registerCaptchaModalInterceptor(axios);
+
export default axios;
/**
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 8666d325c1b..8a051041fbe 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -11,31 +11,10 @@ import { isObject } from './type_utility';
import { getLocationHash } from './url_utility';
export const getPagePath = (index = 0) => {
- const page = $('body').attr('data-page') || '';
-
+ const { page = '' } = document?.body?.dataset;
return page.split(':')[index];
};
-export const getDashPath = (path = window.location.pathname) => path.split('/-/')[1] || null;
-
-export const isInGroupsPage = () => getPagePath() === 'groups';
-
-export const isInProjectPage = () => getPagePath() === 'projects';
-
-export const getProjectSlug = () => {
- if (isInProjectPage()) {
- return $('body').data('project');
- }
- return null;
-};
-
-export const getGroupSlug = () => {
- if (isInProjectPage() || isInGroupsPage()) {
- return $('body').data('group');
- }
- return null;
-};
-
export const checkPageAndAction = (page, action) => {
const pagePath = getPagePath(1);
const actionPath = getPagePath(2);
@@ -49,6 +28,8 @@ export const isInDesignPage = () => checkPageAndAction('issues', 'designs');
export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
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;
@@ -162,53 +143,6 @@ export const parseUrlPathname = (url) => {
return parsedUrl.pathname.charAt(0) === '/' ? parsedUrl.pathname : `/${parsedUrl.pathname}`;
};
-const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
-
-export const urlParamsToArray = (path = '') =>
- splitPath(path)
- .filter((param) => param.length > 0)
- .map((param) => {
- const split = param.split('=');
- return [decodeURI(split[0]), split[1]].join('=');
- });
-
-export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
-
-/**
- * Accepts encoding string which includes query params being
- * sent to URL.
- *
- * @param {string} path Query param string
- *
- * @returns {object} Query params object containing key-value pairs
- * with both key and values decoded into plain string.
- */
-export const urlParamsToObject = (path = '') =>
- splitPath(path).reduce((dataParam, filterParam) => {
- if (filterParam === '') {
- return dataParam;
- }
-
- const data = dataParam;
- let [key, value] = filterParam.split('=');
- key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
- const isArray = key.includes('[]');
- key = key.replace('[]', '');
- value = decodeURIComponent(value.replace(/\+/g, ' '));
-
- if (isArray) {
- if (!data[key]) {
- data[key] = [];
- }
-
- data[key].push(value);
- } else {
- data[key] = value;
- }
-
- return data;
- }, {});
-
export const isMetaKey = (e) => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// Identify following special clicks
@@ -301,21 +235,6 @@ export const debounceByAnimationFrame = (fn) => {
};
};
-/**
- this will take in the `name` of the param you want to parse in the url
- if the name does not exist this function will return `null`
- otherwise it will return the value of the param key provided
-*/
-export const getParameterByName = (name, urlToParse) => {
- const url = urlToParse || window.location.href;
- const parsedName = name.replace(/[[\]]/g, '\\$&');
- const regex = new RegExp(`[?&]${parsedName}(=([^&#]*)|&|#|$)`);
- const results = regex.exec(url);
- if (!results) return null;
- if (!results[2]) return '';
- return decodeURIComponent(results[2].replace(/\+/g, ' '));
-};
-
const handleSelectedRange = (range, restrictToNode) => {
// Make sure this range is within the restricting container
if (restrictToNode && !range.intersectsNode(restrictToNode)) return null;
@@ -390,8 +309,8 @@ export const insertText = (target, text) => {
};
/**
- this will take in the headers from an API response and normalize them
- this way we don't run into production issues when nginx gives us lowercased header keys
+ this will take in the headers from an API response and normalize them
+ this way we don't run into production issues when nginx gives us lowercased header keys
*/
export const normalizeHeaders = (headers) => {
const upperCaseHeaders = {};
@@ -418,39 +337,6 @@ export const parseIntPagination = (paginationInformation) => ({
previousPage: parseInt(paginationInformation['X-PREV-PAGE'], 10),
});
-/**
- * Given a string of query parameters creates an object.
- *
- * @example
- * `scope=all&page=2` -> { scope: 'all', page: '2'}
- * `scope=all` -> { scope: 'all' }
- * ``-> {}
- * @param {String} query
- * @returns {Object}
- */
-export const parseQueryStringIntoObject = (query = '') => {
- if (query === '') return {};
-
- return query.split('&').reduce((acc, element) => {
- const val = element.split('=');
- Object.assign(acc, {
- [val[0]]: decodeURIComponent(val[1]),
- });
- return acc;
- }, {});
-};
-
-/**
- * Converts object with key-value pairs
- * into query-param string
- *
- * @param {Object} params
- */
-export const objectToQueryString = (params = {}) =>
- Object.keys(params)
- .map((param) => `${param}=${params[param]}`)
- .join('&');
-
export const buildUrlWithCurrentLocation = (param) => {
if (param) return `${window.location.pathname}${param}`;
@@ -789,7 +675,18 @@ export const searchBy = (query = '', searchSpace = {}) => {
* @param {Object} label
* @returns Boolean
*/
-export const isScopedLabel = ({ title = '' }) => title.indexOf('::') !== -1;
+export const isScopedLabel = ({ title = '' } = {}) => title.indexOf('::') !== -1;
+
+/**
+ * Returns the base value of the scoped label
+ *
+ * Expected Label to be an Object with `title` as a key:
+ * { title: 'LabelTitle', ...otherProperties };
+ *
+ * @param {Object} label
+ * @returns String
+ */
+export const scopedLabelKey = ({ title = '' }) => isScopedLabel({ title }) && title.split('::')[0];
// Methods to set and get Cookie
export const setCookie = (name, value) => Cookies.set(name, value, { expires: 365 });
@@ -821,3 +718,5 @@ export const isFeatureFlagEnabled = (flag) => window.gon.features?.[flag];
* @returns {Array[String]} Converted array
*/
export const convertArrayToCamelCase = (array) => array.map((i) => convertToCamelCase(i));
+
+export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index 2d4765f54b9..e41de72ded4 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -1,4 +1,5 @@
export const BYTES_IN_KIB = 1024;
+export const DEFAULT_DEBOUNCE_AND_THROTTLE_MS = 250;
export const HIDDEN_CLASS = 'hidden';
export const TRUNCATE_WIDTH_DEFAULT_WIDTH = 80;
export const TRUNCATE_WIDTH_DEFAULT_FONT_SIZE = 12;
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index 512b1f079a1..d68682ebed1 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -1,10 +1,7 @@
-import $ from 'jquery';
import * as timeago from 'timeago.js';
-import { languageCode, s__ } from '../../../locale';
+import { languageCode, s__, createDateTimeFormat } from '../../../locale';
import { formatDate } from './date_format_utility';
-window.timeago = timeago;
-
/**
* Timeago uses underscores instead of dashes to separate language from country code.
*
@@ -76,24 +73,44 @@ const memoizedLocale = () => {
timeago.register(timeagoLanguageCode, memoizedLocale());
timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
-export const getTimeago = () => timeago;
+let memoizedFormatter = null;
+
+function setupAbsoluteFormatter() {
+ if (memoizedFormatter === null) {
+ const formatter = createDateTimeFormat({
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ });
+
+ memoizedFormatter = {
+ format(date) {
+ return formatter.format(date instanceof Date ? date : new Date(date));
+ },
+ };
+ }
+ return memoizedFormatter;
+}
+
+export const getTimeago = () =>
+ window.gon?.time_display_relative === false ? setupAbsoluteFormatter() : timeago;
/**
* For the given elements, sets a tooltip with a formatted date.
- * @param {JQuery} $timeagoEls
- * @param {Boolean} setTimeago
+ * @param {Array<Node>|NodeList} elements
+ * @param {Boolean} updateTooltip
*/
-export const localTimeAgo = ($timeagoEls, setTimeago = true) => {
- $timeagoEls.each((i, el) => {
- $(el).text(timeago.format($(el).attr('datetime'), timeagoLanguageCode));
+export const localTimeAgo = (elements, updateTooltip = true) => {
+ const { format } = getTimeago();
+ elements.forEach((el) => {
+ el.innerText = format(el.dateTime, timeagoLanguageCode);
});
- if (!setTimeago) {
+ if (!updateTooltip) {
return;
}
function addTimeAgoTooltip() {
- $timeagoEls.each((i, el) => {
+ elements.forEach((el) => {
// Recreate with custom template
el.setAttribute('title', formatDate(el.dateTime));
});
@@ -116,9 +133,3 @@ export const timeFor = (time, expiredLabel) => {
}
return timeago.format(time, `${timeagoLanguageCode}-remaining`).trim();
};
-
-window.gl = window.gl || {};
-window.gl.utils = {
- ...(window.gl.utils || {}),
- localTimeAgo,
-};
diff --git a/app/assets/javascripts/lib/utils/finite_state_machine.js b/app/assets/javascripts/lib/utils/finite_state_machine.js
new file mode 100644
index 00000000000..99eeb7cb947
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/finite_state_machine.js
@@ -0,0 +1,101 @@
+/**
+ * @module finite_state_machine
+ */
+
+/**
+ * The states to be used with state machine definitions
+ * @typedef {Object} FiniteStateMachineStates
+ * @property {!Object} ANY_KEY - Any key that maps to a known state
+ * @property {!Object} ANY_KEY.on - A dictionary of transition events for the ANY_KEY state that map to a different state
+ * @property {!String} ANY_KEY.on.ANY_EVENT - The resulting state that the machine should end at
+ */
+
+/**
+ * An object whose minimum definition defined here can be used to guard UI state transitions
+ * @typedef {Object} StatelessFiniteStateMachineDefinition
+ * @property {FiniteStateMachineStates} states
+ */
+
+/**
+ * An object whose minimum definition defined here can be used to create a live finite state machine
+ * @typedef {Object} LiveFiniteStateMachineDefinition
+ * @property {String} initial - The initial state for this machine
+ * @property {FiniteStateMachineStates} states
+ */
+
+/**
+ * An object that allows interacting with a stateful, live finite state machine
+ * @typedef {Object} LiveStateMachine
+ * @property {String} value - The current state of this machine
+ * @property {Object} states - The states from when the machine definition was constructed
+ * @property {Function} is - {@link module:finite_state_machine~is LiveStateMachine.is}
+ * @property {Function} send - {@link module:finite_state_machine~send LiveStatemachine.send}
+ */
+
+// This is not user-facing functionality
+/* eslint-disable @gitlab/require-i18n-strings */
+
+function hasKeys(object, keys) {
+ return keys.every((key) => Object.keys(object).includes(key));
+}
+
+/**
+ * Get an updated state given a machine definition, a starting state, and a transition event
+ * @param {StatelessFiniteStateMachineDefinition} definition
+ * @param {String} current - The current known state
+ * @param {String} event - A transition event
+ * @returns {String} A state value
+ */
+export function transition(definition, current, event) {
+ return definition?.states?.[current]?.on[event] || current;
+}
+
+function startMachine({ states, initial } = {}) {
+ let current = initial;
+
+ return {
+ /**
+ * A convenience function to test arbitrary input against the machine's current state
+ * @param {String} testState - The value to test against the machine's current state
+ */
+ is(testState) {
+ return current === testState;
+ },
+ /**
+ * A function to transition the live state machine using an arbitrary event
+ * @param {String} event - The event to send to the machine
+ * @returns {String} A string representing the current state. Note this may not have changed if the current state + transition event combination are not valid.
+ */
+ send(event) {
+ current = transition({ states }, current, event);
+
+ return current;
+ },
+ get value() {
+ return current;
+ },
+ set value(forcedState) {
+ current = forcedState;
+ },
+ states,
+ };
+}
+
+/**
+ * Create a live state machine
+ * @param {LiveFiniteStateMachineDefinition} definition
+ * @returns {LiveStateMachine} A live state machine
+ */
+export function machine(definition) {
+ if (!hasKeys(definition, ['initial', 'states'])) {
+ throw new Error(
+ 'A state machine must have an initial state (`.initial`) and a dictionary of possible states (`.states`)',
+ );
+ } else if (!hasKeys(definition.states, [definition.initial])) {
+ throw new Error(
+ `Cannot initialize the state machine to state '${definition.initial}'. Is that one of the machine's defined states?`,
+ );
+ } else {
+ return startMachine(definition);
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index eaf396a7a59..5ee00464a8b 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -421,3 +421,61 @@ export const isValidSha1Hash = (str) => {
export function insertFinalNewline(content, endOfLine = '\n') {
return content.slice(-endOfLine.length) !== endOfLine ? `${content}${endOfLine}` : content;
}
+
+export const markdownConfig = {
+ // allowedTags from GitLab's inline HTML guidelines
+ // https://docs.gitlab.com/ee/user/markdown.html#inline-html
+ ALLOWED_TAGS: [
+ 'a',
+ 'abbr',
+ 'b',
+ 'blockquote',
+ 'br',
+ 'code',
+ 'dd',
+ 'del',
+ 'div',
+ 'dl',
+ 'dt',
+ 'em',
+ 'h1',
+ 'h2',
+ 'h3',
+ 'h4',
+ 'h5',
+ 'h6',
+ 'hr',
+ 'i',
+ 'img',
+ 'ins',
+ 'kbd',
+ 'li',
+ 'ol',
+ 'p',
+ 'pre',
+ 'q',
+ 'rp',
+ 'rt',
+ 'ruby',
+ 's',
+ 'samp',
+ 'span',
+ 'strike',
+ 'strong',
+ 'sub',
+ 'summary',
+ 'sup',
+ 'table',
+ 'tbody',
+ 'td',
+ 'tfoot',
+ 'th',
+ 'thead',
+ 'tr',
+ 'tt',
+ 'ul',
+ 'var',
+ ],
+ ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
+ ALLOW_DATA_ATTR: false,
+};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index d68b41b7f7a..7922ff22a70 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -209,11 +209,7 @@ export function removeParams(params, url = window.location.href, skipEncoding =
return `${root}${writableQuery}${writableFragment}`;
}
-export function getLocationHash(url = window.location.href) {
- const hashIndex = url.indexOf('#');
-
- return hashIndex === -1 ? null : url.substring(hashIndex + 1);
-}
+export const getLocationHash = (hash = window.location.hash) => hash.split('#')[1];
/**
* Returns a boolean indicating whether the URL hash contains the given string value
@@ -409,6 +405,55 @@ export function getWebSocketUrl(path) {
return `${getWebSocketProtocol()}//${joinPaths(window.location.host, path)}`;
}
+const splitPath = (path = '') => path.replace(/^\?/, '').split('&');
+
+export const urlParamsToArray = (path = '') =>
+ splitPath(path)
+ .filter((param) => param.length > 0)
+ .map((param) => {
+ const split = param.split('=');
+ return [decodeURI(split[0]), split[1]].join('=');
+ });
+
+export const getUrlParamsArray = () => urlParamsToArray(window.location.search);
+
+/**
+ * Accepts encoding string which includes query params being
+ * sent to URL.
+ *
+ * @param {string} path Query param string
+ *
+ * @returns {object} Query params object containing key-value pairs
+ * with both key and values decoded into plain string.
+ *
+ * @deprecated Please use `queryToObject(query, { gatherArrays: true });` instead. See https://gitlab.com/gitlab-org/gitlab/-/issues/328845
+ */
+export const urlParamsToObject = (path = '') =>
+ splitPath(path).reduce((dataParam, filterParam) => {
+ if (filterParam === '') {
+ return dataParam;
+ }
+
+ const data = dataParam;
+ let [key, value] = filterParam.split('=');
+ key = /%\w+/g.test(key) ? decodeURIComponent(key) : key;
+ const isArray = key.includes('[]');
+ key = key.replace('[]', '');
+ value = decodeURIComponent(value.replace(/\+/g, ' '));
+
+ if (isArray) {
+ if (!data[key]) {
+ data[key] = [];
+ }
+
+ data[key].push(value);
+ } else {
+ data[key] = value;
+ }
+
+ return data;
+ }, {});
+
/**
* Convert search query into an object
*
@@ -450,17 +495,30 @@ export function queryToObject(query, { gatherArrays = false, legacySpacesDecode
}
/**
+ * This function accepts the `name` of the param to parse in the url
+ * if the name does not exist this function will return `null`
+ * otherwise it will return the value of the param key provided
+ *
+ * @param {String} name
+ * @param {String?} urlToParse
+ * @returns value of the parameter as string
+ */
+export const getParameterByName = (name, query = window.location.search) => {
+ return queryToObject(query)[name] || null;
+};
+
+/**
* Convert search query object back into a search query
*
- * @param {Object} obj that needs to be converted
+ * @param {Object?} params that needs to be converted
* @returns {String}
*
* ex: {one: 1, two: 2} into "one=1&two=2"
*
*/
-export function objectToQuery(obj) {
- return Object.keys(obj)
- .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(obj[k])}`)
+export function objectToQuery(params = {}) {
+ return Object.keys(params)
+ .map((k) => `${encodeURIComponent(k)}=${encodeURIComponent(params[k])}`)
.join('&');
}
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index aaa8ee40966..a1f59aa1b54 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -117,8 +117,8 @@ LineHighlighter.prototype.clearHighlight = function () {
//
// Returns an Array
LineHighlighter.prototype.hashToRange = function (hash) {
- // ?L(\d+)(?:-(\d+))?$/)
- const matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
+ // ?L(\d+)(?:-L?(\d+))?$/)
+ const matches = hash.match(/^#?L(\d+)(?:-L?(\d+))?$/);
if (matches && matches.length) {
const first = parseInt(matches[1], 10);
const last = matches[2] ? parseInt(matches[2], 10) : null;
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 10518fa73d9..ad01da2eb17 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -2,7 +2,10 @@ import Jed from 'jed';
import ensureSingleLine from './ensure_single_line';
import sprintf from './sprintf';
-const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en';
+const GITLAB_FALLBACK_LANGUAGE = 'en';
+
+const languageCode = () =>
+ document.querySelector('html').getAttribute('lang') || GITLAB_FALLBACK_LANGUAGE;
const locale = new Jed(window.translations || {});
delete window.translations;
@@ -51,12 +54,52 @@ const pgettext = (keyOrContext, key) => {
};
/**
+ * Filters navigator languages by the set GitLab language.
+ *
+ * This allows us to decide better what a user wants as a locale, for using with the Intl browser APIs.
+ * If they have set their GitLab to a language, it will check whether `navigator.languages` contains matching ones.
+ * This function always adds `en` as a fallback in order to have date renders if all fails before it.
+ *
+ * - Example one: GitLab language is `en` and browser languages are:
+ * `['en-GB', 'en-US']`. This function returns `['en-GB', 'en-US', 'en']` as
+ * the preferred locales, the Intl APIs would try to format first as British English,
+ * if that isn't available US or any English.
+ * - Example two: GitLab language is `en` and browser languages are:
+ * `['de-DE', 'de']`. This function returns `['en']`, so the Intl APIs would prefer English
+ * formatting in order to not have German dates mixed with English GitLab UI texts.
+ * If the user wants for example British English formatting (24h, etc),
+ * they could set their browser languages to `['de-DE', 'de', 'en-GB']`.
+ * - Example three: GitLab language is `de` and browser languages are `['en-US', 'en']`.
+ * This function returns `['de', 'en']`, aligning German dates with the chosen translation of GitLab.
+ *
+ * @returns {string[]}
+ */
+export const getPreferredLocales = () => {
+ const gitlabLanguage = languageCode();
+ // The GitLab language may or may not contain a country code,
+ // so we create the short version as well, e.g. de-AT => de
+ const lang = gitlabLanguage.substring(0, 2);
+ const locales = navigator.languages.filter((l) => l.startsWith(lang));
+ if (!locales.includes(gitlabLanguage)) {
+ locales.push(gitlabLanguage);
+ }
+ if (!locales.includes(lang)) {
+ locales.push(lang);
+ }
+ if (!locales.includes(GITLAB_FALLBACK_LANGUAGE)) {
+ locales.push(GITLAB_FALLBACK_LANGUAGE);
+ }
+ return locales;
+};
+
+/**
Creates an instance of Intl.DateTimeFormat for the current locale.
@param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
@returns {Intl.DateTimeFormat}
*/
-const createDateTimeFormat = (formatOptions) => Intl.DateTimeFormat(languageCode(), formatOptions);
+const createDateTimeFormat = (formatOptions) =>
+ Intl.DateTimeFormat(getPreferredLocales(), formatOptions);
/**
* Formats a number as a string using `toLocaleString`.
diff --git a/app/assets/javascripts/logs/components/environment_logs.vue b/app/assets/javascripts/logs/components/environment_logs.vue
index 39041aa1447..3db9fa01629 100644
--- a/app/assets/javascripts/logs/components/environment_logs.vue
+++ b/app/assets/javascripts/logs/components/environment_logs.vue
@@ -29,9 +29,6 @@ export default {
LogAdvancedFilters,
LogControlButtons,
},
- filters: {
- formatDate,
- },
props: {
environmentName: {
type: String,
@@ -114,6 +111,7 @@ export default {
const { scrollTop = 0, clientHeight = 0, scrollHeight = 0 } = target;
this.scrollDownButtonDisabled = scrollTop + clientHeight === scrollHeight;
}, 200),
+ formatDate,
},
};
</script>
@@ -229,8 +227,8 @@ export default {
<div ref="logFooter" class="py-2 px-3 text-white bg-secondary-900">
<gl-sprintf :message="s__('Environments|Logs from %{start} to %{end}.')">
- <template #start>{{ timeRange.current.start | formatDate }}</template>
- <template #end>{{ timeRange.current.end | formatDate }}</template>
+ <template #start>{{ formatDate(timeRange.current.start) }}</template>
+ <template #end>{{ formatDate(timeRange.current.end) }}</template>
</gl-sprintf>
<gl-sprintf
v-if="!logs.isComplete"
diff --git a/app/assets/javascripts/logs/components/tokens/token_with_loading_state.vue b/app/assets/javascripts/logs/components/tokens/token_with_loading_state.vue
index f8ce704942b..4e672c1d121 100644
--- a/app/assets/javascripts/logs/components/tokens/token_with_loading_state.vue
+++ b/app/assets/javascripts/logs/components/tokens/token_with_loading_state.vue
@@ -20,7 +20,7 @@ export default {
<gl-filtered-search-token :config="config" v-bind="{ ...$attrs }" v-on="$listeners">
<template #suggestions>
<div class="m-1">
- <gl-loading-icon v-if="config.loading" />
+ <gl-loading-icon v-if="config.loading" size="sm" />
<div v-else class="py-1 px-2 text-muted">
{{ config.noOptionsText }}
</div>
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 2309f7a420f..5c14000a2aa 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -31,7 +31,7 @@ import initFrequentItemDropdowns from './frequent_items';
import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
-import initUsagePingConsent from './usage_ping_consent';
+import initServicePingConsent from './service_ping_consent';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
@@ -46,6 +46,9 @@ applyGitLabUIConfig();
window.jQuery = jQuery;
window.$ = jQuery;
+// ensure that window.gl is set up
+window.gl = window.gl || {};
+
// inject test utilities if necessary
if (process.env.NODE_ENV !== 'production' && gon?.test_env) {
import(/* webpackMode: "eager" */ './test_utils/');
@@ -86,7 +89,7 @@ function deferredInitialisation() {
initBreadcrumbs();
initTodoToggle();
initLogoAnimation();
- initUsagePingConsent();
+ initServicePingConsent();
initUserPopovers();
initBroadcastNotifications();
initFrequentItemDropdowns();
@@ -183,7 +186,7 @@ document.addEventListener('DOMContentLoaded', () => {
return true;
});
- localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ localTimeAgo(document.querySelectorAll('abbr.timeago, .js-timeago'), true);
/**
* This disables form buttons while a form is submitting
diff --git a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
index 1e9f79927ea..0c20f935d50 100644
--- a/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
+++ b/app/assets/javascripts/members/components/action_buttons/user_action_buttons.vue
@@ -38,6 +38,7 @@ export default {
usersName: user.name,
source: source.fullName,
},
+ false,
);
}
diff --git a/app/assets/javascripts/members/components/app.vue b/app/assets/javascripts/members/components/app.vue
index a08518584f3..0ec39f58930 100644
--- a/app/assets/javascripts/members/components/app.vue
+++ b/app/assets/javascripts/members/components/app.vue
@@ -19,6 +19,11 @@ export default {
type: String,
required: true,
},
+ tabQueryParamValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
...mapState({
@@ -55,6 +60,6 @@ export default {
errorMessage
}}</gl-alert>
<filter-sort-container />
- <members-table />
+ <members-table :tab-query-param-value="tabQueryParamValue" />
</div>
</template>
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 cc0533391df..33d86dec767 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
@@ -1,10 +1,13 @@
<script>
import { GlFilteredSearchToken } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { getParameterByName, urlParamsToObject } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { getParameterByName, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
-import { SEARCH_TOKEN_TYPE, SORT_PARAM } from '~/members/constants';
+import {
+ SEARCH_TOKEN_TYPE,
+ SORT_QUERY_PARAM_NAME,
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+} from '~/members/constants';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
@@ -64,7 +67,7 @@ export default {
},
},
created() {
- const query = urlParamsToObject(window.location.search);
+ const query = queryToObject(window.location.search);
const tokens = this.tokens
.filter((token) => query[token.type])
@@ -116,10 +119,15 @@ export default {
return accumulator;
}, {});
- const sortParam = getParameterByName(SORT_PARAM);
+ const sortParamValue = getParameterByName(SORT_QUERY_PARAM_NAME);
+ const activeTabParamValue = getParameterByName(ACTIVE_TAB_QUERY_PARAM_NAME);
window.location.href = setUrlParams(
- { ...params, ...(sortParam && { sort: sortParam }) },
+ {
+ ...params,
+ ...(sortParamValue && { [SORT_QUERY_PARAM_NAME]: sortParamValue }),
+ ...(activeTabParamValue && { [ACTIVE_TAB_QUERY_PARAM_NAME]: activeTabParamValue }),
+ },
window.location.href,
true,
);
diff --git a/app/assets/javascripts/members/components/members_tabs.vue b/app/assets/javascripts/members/components/members_tabs.vue
index 37b9135126d..7c21e33d892 100644
--- a/app/assets/javascripts/members/components/members_tabs.vue
+++ b/app/assets/javascripts/members/components/members_tabs.vue
@@ -1,16 +1,18 @@
<script>
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
import { mapState } from 'vuex';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
+// eslint-disable-next-line import/no-deprecated
+import { urlParamsToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
-import { MEMBER_TYPES } from '../constants';
+import { MEMBER_TYPES, TAB_QUERY_PARAM_VALUES, ACTIVE_TAB_QUERY_PARAM_NAME } from '../constants';
import MembersApp from './app.vue';
const countComputed = (state, namespace) => state[namespace]?.pagination?.totalItems || 0;
export default {
name: 'MembersTabs',
- tabs: [
+ ACTIVE_TAB_QUERY_PARAM_NAME,
+ TABS: [
{
namespace: MEMBER_TYPES.user,
title: __('Members'),
@@ -19,19 +21,21 @@ export default {
namespace: MEMBER_TYPES.group,
title: __('Groups'),
attrs: { 'data-qa-selector': 'groups_list_tab' },
+ queryParamValue: TAB_QUERY_PARAM_VALUES.group,
},
{
namespace: MEMBER_TYPES.invite,
title: __('Invited'),
canManageMembersPermissionsRequired: true,
+ queryParamValue: TAB_QUERY_PARAM_VALUES.invite,
},
{
namespace: MEMBER_TYPES.accessRequest,
title: __('Access requests'),
canManageMembersPermissionsRequired: true,
+ queryParamValue: TAB_QUERY_PARAM_VALUES.accessRequest,
},
],
- urlParams: [],
components: { MembersApp, GlTabs, GlTab, GlBadge },
inject: ['canManageMembers'],
data() {
@@ -55,32 +59,22 @@ export default {
},
}),
urlParams() {
+ // eslint-disable-next-line import/no-deprecated
return Object.keys(urlParamsToObject(window.location.search));
},
activeTabIndexCalculatedFromUrlParams() {
- return this.$options.tabs.findIndex(({ namespace }) => {
+ return this.$options.TABS.findIndex(({ namespace }) => {
return this.getTabUrlParams(namespace).some((urlParam) =>
this.urlParams.includes(urlParam),
);
});
},
},
- created() {
- if (this.activeTabIndexCalculatedFromUrlParams === -1) {
- return;
- }
-
- this.selectedTabIndex = this.activeTabIndexCalculatedFromUrlParams;
- },
methods: {
getTabUrlParams(namespace) {
const state = this.$store.state[namespace];
const urlParams = [];
- if (state?.pagination?.paramName) {
- urlParams.push(state.pagination.paramName);
- }
-
if (state?.filteredSearchBar?.searchParam) {
urlParams.push(state.filteredSearchBar.searchParam);
}
@@ -110,14 +104,23 @@ export default {
</script>
<template>
- <gl-tabs v-model="selectedTabIndex">
- <template v-for="(tab, index) in $options.tabs">
- <gl-tab v-if="showTab(tab, index)" :key="tab.namespace" :title-link-attributes="tab.attrs">
- <template slot="title">
+ <gl-tabs
+ v-model="selectedTabIndex"
+ sync-active-tab-with-query-params
+ :query-param-name="$options.ACTIVE_TAB_QUERY_PARAM_NAME"
+ >
+ <template v-for="(tab, index) in $options.TABS">
+ <gl-tab
+ v-if="showTab(tab, index)"
+ :key="tab.namespace"
+ :title-link-attributes="tab.attrs"
+ :query-param-value="tab.queryParamValue"
+ >
+ <template #title>
<span>{{ tab.title }}</span>
<gl-badge size="sm" class="gl-tab-counter-badge">{{ getTabCount(tab) }}</gl-badge>
</template>
- <members-app :namespace="tab.namespace" />
+ <members-app :namespace="tab.namespace" :tab-query-param-value="tab.queryParamValue" />
</gl-tab>
</template>
</gl-tabs>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 09ef98ec411..b9c80edbc49 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -5,7 +5,7 @@ import MembersTableCell from 'ee_else_ce/members/components/table/members_table_
import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members/utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
-import { FIELDS } from '../../constants';
+import { FIELDS, ACTIVE_TAB_QUERY_PARAM_NAME } from '../../constants';
import RemoveGroupLinkModal from '../modals/remove_group_link_modal.vue';
import CreatedAt from './created_at.vue';
import ExpirationDatepicker from './expiration_datepicker.vue';
@@ -34,6 +34,13 @@ export default {
import('ee_component/members/components/ldap/ldap_override_confirmation_modal.vue'),
},
inject: ['namespace', 'currentUserId'],
+ props: {
+ tabQueryParamValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
computed: {
...mapState({
members(state) {
@@ -112,7 +119,15 @@ export default {
paginationLinkGenerator(page) {
const { params = {}, paramName } = this.pagination;
- return mergeUrlParams({ ...params, [paramName]: page }, window.location.href);
+ return mergeUrlParams(
+ {
+ ...params,
+ [ACTIVE_TAB_QUERY_PARAM_NAME]:
+ this.tabQueryParamValue !== '' ? this.tabQueryParamValue : null,
+ [paramName]: page,
+ },
+ window.location.href,
+ );
},
},
};
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index f68a8814fee..6f465245d20 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -89,6 +89,12 @@ export const MEMBER_TYPES = {
accessRequest: 'accessRequest',
};
+export const TAB_QUERY_PARAM_VALUES = {
+ group: 'groups',
+ invite: 'invited',
+ accessRequest: 'access_requests',
+};
+
export const DAYS_TO_EXPIRE_SOON = 7;
export const LEAVE_MODAL_ID = 'member-leave-modal';
@@ -97,7 +103,8 @@ export const REMOVE_GROUP_LINK_MODAL_ID = 'remove-group-link-modal-id';
export const SEARCH_TOKEN_TYPE = 'filtered-search-term';
-export const SORT_PARAM = 'sort';
+export const SORT_QUERY_PARAM_NAME = 'sort';
+export const ACTIVE_TAB_QUERY_PARAM_NAME = 'tab';
export const MEMBER_ACCESS_LEVEL_PROPERTY_NAME = 'access_level';
diff --git a/app/assets/javascripts/members/utils.js b/app/assets/javascripts/members/utils.js
index be549b40885..05f086c8f4f 100644
--- a/app/assets/javascripts/members/utils.js
+++ b/app/assets/javascripts/members/utils.js
@@ -1,6 +1,6 @@
import { isUndefined } from 'lodash';
-import { getParameterByName, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { getParameterByName, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import {
FIELDS,
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
index 04e493712ec..7168efa28ad 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapActions } from 'vuex';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { INTERACTIVE_RESOLVE_MODE } from '../constants';
@@ -50,13 +50,13 @@ export default {
methods: {
...mapActions(['setFileResolveMode', 'setPromptConfirmationState', 'updateFile']),
loadEditor() {
- const EditorPromise = import(/* webpackChunkName: 'EditorLite' */ '~/editor/editor_lite');
+ const EditorPromise = import(/* webpackChunkName: 'SourceEditor' */ '~/editor/source_editor');
const DataPromise = axios.get(this.file.content_path);
Promise.all([EditorPromise, DataPromise])
.then(
([
- { default: EditorLite },
+ { default: SourceEditor },
{
data: { content, new_path: path },
},
@@ -66,7 +66,7 @@ export default {
this.originalContent = content;
this.fileLoaded = true;
- this.editor = new EditorLite().createInstance({
+ this.editor = new SourceEditor().createInstance({
el: contentEl,
blobPath: path,
blobContent: content,
@@ -75,7 +75,9 @@ export default {
},
)
.catch(() => {
- flash(__('An error occurred while loading the file'));
+ createFlash({
+ message: __('An error occurred while loading the file'),
+ });
});
},
saveDiffResolution() {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
index 3e31e2e93ae..5fcc778a714 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
@@ -120,7 +120,7 @@ export default {
>
<div class="js-file-title file-title file-title-flex-parent cursor-default">
<div class="file-header-content" data-testid="file-name">
- <file-icon :file-name="file.filePath" :size="18" css-classes="gl-mr-2" />
+ <file-icon :file-name="file.filePath" :size="16" css-classes="gl-mr-2" />
<strong class="file-title-name">{{ file.filePath }}</strong>
</div>
<div class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start">
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index feaf8b0d996..0ddb2c2334c 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -148,14 +148,6 @@ MergeRequest.prototype.initCommitMessageListeners = function () {
});
};
-MergeRequest.setStatusBoxToMerged = function () {
- $('.detail-page-header .status-box')
- .removeClass('status-box-open')
- .addClass('status-box-mr-merged')
- .find('span')
- .text(__('Merged'));
-};
-
MergeRequest.decreaseCounter = function (by = 1) {
const $el = $('.js-merge-counter');
const count = Math.max(parseInt($el.text().replace(/[^\d]/, ''), 10) - by, 0);
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index d5db9f43d09..1d1c0a23fab 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -3,12 +3,10 @@ import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
import Cookies from 'js-cookie';
import Vue from 'vue';
-import CommitPipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import createEventHub from '~/helpers/event_hub_factory';
-import initAddContextCommitsTriggers from './add_context_commits_modal';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import Diff from './diff';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import initChangesDropdown from './init_changes_dropdown';
import axios from './lib/utils/axios_utils';
import {
@@ -335,17 +333,22 @@ export default class MergeRequestTabs {
axios
.get(`${source}.json`)
.then(({ data }) => {
- document.querySelector('div#commits').innerHTML = data.html;
- localTimeAgo($('.js-timeago', 'div#commits'));
+ const commitsDiv = document.querySelector('div#commits');
+ commitsDiv.innerHTML = data.html;
+ localTimeAgo(commitsDiv.querySelectorAll('.js-timeago'));
this.commitsLoaded = true;
this.scrollToContainerElement('#commits');
this.toggleLoading(false);
- initAddContextCommitsTriggers();
+
+ return import('./add_context_commits_modal');
})
+ .then((m) => m.default())
.catch(() => {
this.toggleLoading(false);
- flash(__('An error occurred while fetching this tab.'));
+ createFlash({
+ message: __('An error occurred while fetching this tab.'),
+ });
});
}
@@ -354,13 +357,16 @@ export default class MergeRequestTabs {
const { mrWidgetData } = gl;
this.commitPipelinesTable = new Vue({
+ components: {
+ CommitPipelinesTable: () => import('~/commit/pipelines/pipelines_table.vue'),
+ },
provide: {
artifactsEndpoint: pipelineTableViewEl.dataset.artifactsEndpoint,
artifactsEndpointPlaceholder: pipelineTableViewEl.dataset.artifactsEndpointPlaceholder,
targetProjectFullPath: mrWidgetData?.target_project_full_path || '',
},
render(createElement) {
- return createElement(CommitPipelinesTable, {
+ return createElement('commit-pipelines-table', {
props: {
endpoint: pipelineTableViewEl.dataset.endpoint,
emptyStateSvgPath: pipelineTableViewEl.dataset.emptyStateSvgPath,
@@ -402,7 +408,7 @@ export default class MergeRequestTabs {
initChangesDropdown(this.stickyTop);
- localTimeAgo($('.js-timeago', 'div#diffs'));
+ localTimeAgo(document.querySelectorAll('#diffs .js-timeago'));
syntaxHighlight($('#diffs .js-syntax-highlight'));
if (this.isDiffAction(this.currentAction)) {
@@ -446,7 +452,9 @@ export default class MergeRequestTabs {
})
.catch(() => {
this.toggleLoading(false);
- flash(__('An error occurred while fetching this tab.'));
+ createFlash({
+ message: __('An error occurred while fetching this tab.'),
+ });
});
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index 280613bda49..b4e53c1fab6 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -39,7 +39,11 @@ export default class Milestone {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
})
- .catch(() => flash(__('Error loading milestone tab')));
+ .catch(() =>
+ createFlash({
+ message: __('Error loading milestone tab'),
+ }),
+ );
}
}
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index b992eaff779..0d9a2eef01a 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -7,6 +7,7 @@ import { template, escape } from 'lodash';
import Api from '~/api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { __, sprintf } from '~/locale';
+import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
import boardsStore, {
boardStoreIssueSet,
boardStoreIssueDelete,
@@ -93,21 +94,7 @@ export default class MilestoneSelect {
// Public API includes `title` instead of `name`.
name: m.title,
}))
- .sort((mA, mB) => {
- const dueDateA = mA.due_date ? parsePikadayDate(mA.due_date) : null;
- const dueDateB = mB.due_date ? parsePikadayDate(mB.due_date) : null;
-
- // Move all expired milestones to the bottom.
- if (mA.expired) return 1;
- if (mB.expired) return -1;
-
- // Move milestones without due dates just above expired milestones.
- if (!dueDateA) return 1;
- if (!dueDateB) return -1;
-
- // Sort by due date in ascending order.
- return dueDateA - dueDateB;
- }),
+ .sort(sortMilestonesByDueDate),
)
.then((data) => {
const extraOptions = [];
diff --git a/app/assets/javascripts/milestones/components/milestone_combobox.vue b/app/assets/javascripts/milestones/components/milestone_combobox.vue
index 1db2d10db20..e8499015210 100644
--- a/app/assets/javascripts/milestones/components/milestone_combobox.vue
+++ b/app/assets/javascripts/milestones/components/milestone_combobox.vue
@@ -171,7 +171,7 @@ export default {
<template>
<gl-dropdown v-bind="$attrs" class="milestone-combobox" @shown="focusSearchBox">
- <template slot="button-content">
+ <template #button-content>
<span data-testid="milestone-combobox-button-content" class="gl-flex-grow-1 text-muted">{{
selectedMilestonesLabel
}}</span>
@@ -202,7 +202,7 @@ export default {
<gl-dropdown-divider />
<template v-if="isLoading">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
<gl-dropdown-divider />
</template>
<template v-else-if="showNoResults">
diff --git a/app/assets/javascripts/milestones/milestone_utils.js b/app/assets/javascripts/milestones/milestone_utils.js
new file mode 100644
index 00000000000..3ae5e676138
--- /dev/null
+++ b/app/assets/javascripts/milestones/milestone_utils.js
@@ -0,0 +1,32 @@
+import { parsePikadayDate } from '~/lib/utils/datetime_utility';
+
+/**
+ * This method is to be used with `Array.prototype.sort` function
+ * where array contains milestones with `due_date`/`dueDate` and/or
+ * `expired` properties.
+ * This method sorts given milestone params based on their expiration
+ * status by putting expired milestones at the bottom and upcoming
+ * milestones at the top of the list.
+ *
+ * @param {object} milestoneA
+ * @param {object} milestoneB
+ */
+export function sortMilestonesByDueDate(milestoneA, milestoneB) {
+ const rawDueDateA = milestoneA.due_date || milestoneA.dueDate;
+ const rawDueDateB = milestoneB.due_date || milestoneB.dueDate;
+ const dueDateA = rawDueDateA ? parsePikadayDate(rawDueDateA) : null;
+ const dueDateB = rawDueDateB ? parsePikadayDate(rawDueDateB) : null;
+ const expiredA = milestoneA.expired || Date.now() > dueDateA?.getTime();
+ const expiredB = milestoneB.expired || Date.now() > dueDateB?.getTime();
+
+ // Move all expired milestones to the bottom.
+ if (expiredA) return 1;
+ if (expiredB) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!dueDateA) return 1;
+ if (!dueDateB) return -1;
+
+ // Sort by due date in ascending order.
+ return dueDateA - dueDateB;
+}
diff --git a/app/assets/javascripts/mirrors/mirror_repos.js b/app/assets/javascripts/mirrors/mirror_repos.js
index a26c8f85958..e59da18fb77 100644
--- a/app/assets/javascripts/mirrors/mirror_repos.js
+++ b/app/assets/javascripts/mirrors/mirror_repos.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import { hide } from '~/tooltips';
@@ -111,7 +111,11 @@ export default class MirrorRepos {
return axios
.put(this.mirrorEndpoint, payload)
.then(() => this.removeRow($target))
- .catch(() => Flash(__('Failed to remove mirror.')));
+ .catch(() =>
+ createFlash({
+ message: __('Failed to remove mirror.'),
+ }),
+ );
}
/* eslint-disable class-methods-use-this */
diff --git a/app/assets/javascripts/mirrors/ssh_mirror.js b/app/assets/javascripts/mirrors/ssh_mirror.js
index 15ded478405..5138c450feb 100644
--- a/app/assets/javascripts/mirrors/ssh_mirror.js
+++ b/app/assets/javascripts/mirrors/ssh_mirror.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { escape } from 'lodash';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { backOff } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
@@ -115,7 +115,9 @@ export default class SSHMirror {
const failureMessage = response.data
? response.data.message
: __('An error occurred while detecting host keys');
- Flash(failureMessage);
+ createFlash({
+ message: failureMessage,
+ });
$btnLoadSpinner.addClass('hidden');
this.$btnDetectHostKeys.enable();
diff --git a/app/assets/javascripts/monitoring/components/alert_widget.vue b/app/assets/javascripts/monitoring/components/alert_widget.vue
index c18c13f2574..e5d7e2ea2eb 100644
--- a/app/assets/javascripts/monitoring/components/alert_widget.vue
+++ b/app/assets/javascripts/monitoring/components/alert_widget.vue
@@ -227,7 +227,7 @@ export default {
<template>
<div class="prometheus-alert-widget dropdown flex-grow-2 overflow-hidden">
- <gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" />
+ <gl-loading-icon v-if="shouldShowLoadingIcon" :inline="true" size="sm" />
<span v-else-if="errorMessage" ref="alertErrorMessage" class="alert-error-message">{{
errorMessage
}}</span>
diff --git a/app/assets/javascripts/monitoring/components/charts/time_series.vue b/app/assets/javascripts/monitoring/components/charts/time_series.vue
index 99008d047af..12f5e7efc96 100644
--- a/app/assets/javascripts/monitoring/components/charts/time_series.vue
+++ b/app/assets/javascripts/monitoring/components/charts/time_series.vue
@@ -402,22 +402,20 @@ export default {
@created="onChartCreated"
@updated="onChartUpdated"
>
- <template v-if="tooltip.type === 'deployments'">
- <template slot="tooltip-title">
+ <template #tooltip-title>
+ <template v-if="tooltip.type === 'deployments'">
{{ __('Deployed') }}
</template>
- <div slot="tooltip-content" class="d-flex align-items-center">
+ <div v-else class="text-nowrap">
+ {{ tooltip.title }}
+ </div>
+ </template>
+ <template #tooltip-content>
+ <div v-if="tooltip.type === 'deployments'" class="d-flex align-items-center">
<gl-icon name="commit" class="mr-2" />
<gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
- </template>
- <template v-else>
- <template slot="tooltip-title">
- <div class="text-nowrap">
- {{ tooltip.title }}
- </div>
- </template>
- <template slot="tooltip-content" :tooltip="tooltip">
+ <template v-else>
<div
v-for="(content, key) in tooltip.content"
:key="key"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
index 94cfb562ce3..8e5a0b5cda2 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue
@@ -138,10 +138,10 @@ export default {
</script>
<template>
- <!--
+ <!--
This component should be replaced with a variant developed
as part of https://gitlab.com/gitlab-org/gitlab-ui/-/issues/936
- The variant will create a dropdown with an icon, no text and no caret
+ The variant will create a dropdown with an icon, no text and no caret
-->
<gl-dropdown
v-gl-tooltip
@@ -177,20 +177,22 @@ export default {
@formValidation="setFormValidity"
/>
</form>
- <div slot="modal-footer">
- <gl-button @click="hideAddMetricModal">
- {{ __('Cancel') }}
- </gl-button>
- <gl-button
- v-track-event="getAddMetricTrackingOptions()"
- data-testid="add-metric-modal-submit-button"
- :disabled="!customMetricsFormIsValid"
- variant="success"
- @click="submitCustomMetricsForm"
- >
- {{ __('Save changes') }}
- </gl-button>
- </div>
+ <template #modal-footer>
+ <div>
+ <gl-button @click="hideAddMetricModal">
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ v-track-event="getAddMetricTrackingOptions()"
+ data-testid="add-metric-modal-submit-button"
+ :disabled="!customMetricsFormIsValid"
+ variant="success"
+ @click="submitCustomMetricsForm"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
+ </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard_header.vue b/app/assets/javascripts/monitoring/components/dashboard_header.vue
index 05b5b760f0a..f53f78a3f13 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_header.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_header.vue
@@ -197,7 +197,7 @@ export default {
<gl-dropdown-section-header>{{ __('Environment') }}</gl-dropdown-section-header>
<gl-search-box-by-type @input="debouncedEnvironmentsSearch" />
- <gl-loading-icon v-if="environmentsLoading" :inline="true" />
+ <gl-loading-icon v-if="environmentsLoading" size="sm" :inline="true" />
<div v-else class="flex-fill overflow-auto">
<gl-dropdown-item
v-for="environment in filteredEnvironments"
diff --git a/app/assets/javascripts/monitoring/components/dashboard_panel.vue b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
index 202d18ac721..b786d015f3b 100644
--- a/app/assets/javascripts/monitoring/components/dashboard_panel.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard_panel.vue
@@ -389,7 +389,7 @@ export default {
/>
<div class="flex-grow-1"></div>
<div v-if="graphDataIsLoading" class="mx-1 mt-1">
- <gl-loading-icon />
+ <gl-loading-icon size="sm" />
</div>
<div
v-if="isContextualMenuShown"
diff --git a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
index 49d7e3a48a7..fd07a41ec37 100644
--- a/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
+++ b/app/assets/javascripts/monitoring/components/duplicate_dashboard_modal.vue
@@ -88,7 +88,7 @@ export default {
@change="formChange"
/>
<template #modal-ok>
- <gl-loading-icon v-if="loading" inline color="light" />
+ <gl-loading-icon v-if="loading" size="sm" inline color="light" />
{{ okButtonText }}
</template>
</gl-modal>
diff --git a/app/assets/javascripts/monitoring/components/graph_group.vue b/app/assets/javascripts/monitoring/components/graph_group.vue
index ecb8ef4a0d0..5b73fb4e10d 100644
--- a/app/assets/javascripts/monitoring/components/graph_group.vue
+++ b/app/assets/javascripts/monitoring/components/graph_group.vue
@@ -61,7 +61,7 @@ export default {
<div v-if="showPanels" ref="graph-group" class="card prometheus-panel">
<div class="card-header d-flex align-items-center">
<h4 class="flex-grow-1">{{ name }}</h4>
- <gl-loading-icon v-if="isLoading" name="loading" />
+ <gl-loading-icon v-if="isLoading" size="sm" name="loading" />
<a
data-testid="group-toggle-button"
:aria-label="__('Toggle collapse')"
diff --git a/app/assets/javascripts/namespaces/leave_by_url.js b/app/assets/javascripts/namespaces/leave_by_url.js
index 094590804c1..e00c2abfbef 100644
--- a/app/assets/javascripts/namespaces/leave_by_url.js
+++ b/app/assets/javascripts/namespaces/leave_by_url.js
@@ -1,6 +1,6 @@
-import { deprecatedCreateFlash as Flash } from '~/flash';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import createFlash from '~/flash';
import { initRails } from '~/lib/utils/rails_ujs';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
const PARAMETER_NAME = 'leave';
@@ -18,8 +18,10 @@ export default function leaveByUrl(namespaceType) {
if (leaveLink) {
leaveLink.click();
} else {
- Flash(
- sprintf(__('You do not have permission to leave this %{namespaceType}.'), { namespaceType }),
- );
+ createFlash({
+ message: sprintf(__('You do not have permission to leave this %{namespaceType}.'), {
+ namespaceType,
+ }),
+ });
}
}
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
index cac8fecb6b1..97856eaf256 100644
--- a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
+++ b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
@@ -72,7 +72,7 @@ export default {
<template>
<div class="gl-display-flex gl-align-items-stretch">
<div
- class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-py-3 gl-px-5"
+ class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-p-3"
:class="menuClass"
data-testid="menu-sidebar"
>
@@ -81,7 +81,7 @@ export default {
<keep-alive-slots
v-show="activeView"
:slot-key="activeView"
- class="gl-w-grid-size-40 gl-overflow-hidden gl-py-3 gl-px-5"
+ class="gl-w-grid-size-40 gl-overflow-hidden gl-p-3"
data-testid="menu-subview"
data-qa-selector="menu_subview_container"
>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
index 08b2fbf2ed1..07c6fa7773a 100644
--- a/app/assets/javascripts/nav/components/top_nav_menu_item.vue
+++ b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
@@ -42,7 +42,7 @@ export default {
v-on="$listeners"
>
<span class="gl-display-flex">
- <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-2!': !iconOnly }" />
+ <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-3!': !iconOnly }" />
<template v-if="!iconOnly">
{{ menuItem.title }}
<gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
index 442af512350..b8555df53df 100644
--- a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
+++ b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
@@ -1,7 +1,7 @@
<script>
import TopNavMenuItem from './top_nav_menu_item.vue';
-const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-100';
+const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid gl-border-gray-50';
export default {
components: {
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index a7fcce02ab3..0f4cec67ce8 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -3,7 +3,7 @@
import katex from 'katex';
import marked from 'marked';
import { sanitize } from '~/lib/dompurify';
-import { hasContent } from '~/lib/utils/text_utility';
+import { hasContent, markdownConfig } from '~/lib/utils/text_utility';
import Prompt from './prompt.vue';
const renderer = new marked.Renderer();
@@ -140,63 +140,7 @@ export default {
markdown() {
renderer.attachments = this.cell.attachments;
- return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), {
- // allowedTags from GitLab's inline HTML guidelines
- // https://docs.gitlab.com/ee/user/markdown.html#inline-html
- ALLOWED_TAGS: [
- 'a',
- 'abbr',
- 'b',
- 'blockquote',
- 'br',
- 'code',
- 'dd',
- 'del',
- 'div',
- 'dl',
- 'dt',
- 'em',
- 'h1',
- 'h2',
- 'h3',
- 'h4',
- 'h5',
- 'h6',
- 'hr',
- 'i',
- 'img',
- 'ins',
- 'kbd',
- 'li',
- 'ol',
- 'p',
- 'pre',
- 'q',
- 'rp',
- 'rt',
- 'ruby',
- 's',
- 'samp',
- 'span',
- 'strike',
- 'strong',
- 'sub',
- 'summary',
- 'sup',
- 'table',
- 'tbody',
- 'td',
- 'tfoot',
- 'th',
- 'thead',
- 'tr',
- 'tt',
- 'ul',
- 'var',
- ],
- ALLOWED_ATTR: ['class', 'style', 'href', 'src'],
- ALLOW_DATA_ATTR: false,
- });
+ return sanitize(marked(this.cell.source.join('').replace(/\\/g, '\\\\')), markdownConfig);
},
},
};
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c324c846f47..ef51587734d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -358,7 +358,7 @@ export default class Notes {
setupNewNote($note) {
// Update datetime format on the recent note
- localTimeAgo($note.find('.js-timeago'), false);
+ localTimeAgo($note.find('.js-timeago').get(), false);
this.collapseLongCommitList();
this.taskList.init();
@@ -511,7 +511,7 @@ export default class Notes {
Notes.animateAppendNote(noteEntity.html, discussionContainer);
}
- localTimeAgo($('.js-timeago'), false);
+ localTimeAgo(document.querySelectorAll('.js-timeago'), false);
Notes.checkMergeRequestStatus();
return this.updateNotesCount(1);
}
@@ -628,7 +628,6 @@ export default class Notes {
message: __(
'Your comment could not be submitted! Please check your network connection and try again.',
),
- type: 'alert',
parent: formParentTimeline.get(0),
});
}
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 7213658bdf2..9504ed78778 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -14,7 +14,7 @@ import $ from 'jquery';
import { mapActions, mapGetters, mapState } from 'vuex';
import Autosave from '~/autosave';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { statusBoxState } from '~/issuable/components/status_box.vue';
import httpStatusCodes from '~/lib/utils/http_status';
import {
@@ -293,7 +293,11 @@ export default {
toggleState()
.then(() => statusBoxState.updateStatus && statusBoxState.updateStatus())
.then(refreshUserMergeRequestCounts)
- .catch(() => Flash(constants.toggleStateErrorMessage[this.noteableType][this.openState]));
+ .catch(() =>
+ createFlash({
+ message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
+ }),
+ );
},
discard(shouldClear = true) {
// `blur` is needed to clear slash commands autocomplete cache if event fired.
diff --git a/app/assets/javascripts/notes/components/discussion_notes.vue b/app/assets/javascripts/notes/components/discussion_notes.vue
index dfe2763d8bd..0892276ff3b 100644
--- a/app/assets/javascripts/notes/components/discussion_notes.vue
+++ b/app/assets/javascripts/notes/components/discussion_notes.vue
@@ -130,15 +130,18 @@ export default {
@handleDeleteNote="$emit('deleteNote')"
@startReplying="$emit('startReplying')"
>
- <note-edited-text
- v-if="discussion.resolved"
- slot="discussion-resolved-text"
- :edited-at="discussion.resolved_at"
- :edited-by="discussion.resolved_by"
- :action-text="resolvedText"
- class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
- />
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #discussion-resolved-text>
+ <note-edited-text
+ v-if="discussion.resolved"
+ :edited-at="discussion.resolved_at"
+ :edited-by="discussion.resolved_by"
+ :action-text="resolvedText"
+ class-name="discussion-headline-light js-discussion-headline discussion-resolved-text"
+ />
+ </template>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
</component>
<discussion-notes-replies-wrapper :is-diff-discussion="discussion.diff_discussion">
<toggle-replies-widget
@@ -175,7 +178,9 @@ export default {
:discussion-resolve-path="discussion.resolve_path"
@handleDeleteNote="$emit('deleteNote')"
>
- <slot v-if="index === 0" slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot v-if="index === 0" name="avatar-badge"></slot>
+ </template>
</component>
<slot :show-replies="isExpanded || !hasReplies" name="footer"></slot>
</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 0f72b4f2dba..44d0c741d5a 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlIcon, GlButton, GlDropdownItem } from '@gitlab/ui
import { mapActions, mapGetters } from 'vuex';
import Api from '~/api';
import resolvedStatusMixin from '~/batch_comments/mixins/resolved_status';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import eventHub from '~/sidebar/event_hub';
@@ -234,7 +234,11 @@ export default {
assignee_ids: assignees.map((assignee) => assignee.id),
})
.then(() => this.handleAssigneeUpdate(assignees))
- .catch(() => flash(__('Something went wrong while updating assignees')));
+ .catch(() =>
+ createFlash({
+ message: __('Something went wrong while updating assignees'),
+ }),
+ );
}
},
setAwardEmoji(awardName) {
diff --git a/app/assets/javascripts/notes/components/note_awards_list.vue b/app/assets/javascripts/notes/components/note_awards_list.vue
index 9eb7b928ea4..835750cc137 100644
--- a/app/assets/javascripts/notes/components/note_awards_list.vue
+++ b/app/assets/javascripts/notes/components/note_awards_list.vue
@@ -1,8 +1,8 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import AwardsList from '~/vue_shared/components/awards_list.vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
export default {
components: {
@@ -48,7 +48,11 @@ export default {
awardName,
};
- this.toggleAwardRequest(data).catch(() => Flash(__('Something went wrong on our end.')));
+ this.toggleAwardRequest(data).catch(() =>
+ createFlash({
+ message: __('Something went wrong on our end.'),
+ }),
+ );
},
},
};
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 6932af61c69..1a4a6c137a6 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -216,6 +216,7 @@ export default {
<gl-loading-icon
v-if="showSpinner"
ref="spinner"
+ size="sm"
class="editing-spinner"
:label="__('Comment is being updated')"
/>
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 1af9e4be373..b99579fb9a7 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -2,11 +2,12 @@
import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { mapActions, mapGetters } from 'vuex';
import DraftNote from '~/batch_comments/components/draft_note.vue';
+import createFlash from '~/flash';
import { clearDraft, getDiscussionReplyKey } from '~/lib/utils/autosave';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { s__, __ } from '~/locale';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
@@ -85,7 +86,7 @@ export default {
return this.getUserData;
},
isLoggedIn() {
- return Boolean(gon.current_user_id);
+ return isLoggedIn();
},
autosaveKey() {
return getDiscussionReplyKey(this.firstNote.noteable_type, this.discussion.id);
@@ -220,7 +221,10 @@ export default {
const msg = __(
'Your comment could not be submitted! Please check your network connection and try again.',
);
- Flash(msg, 'alert', this.$el);
+ createFlash({
+ message: msg,
+ parent: this.$el,
+ });
this.$refs.noteForm.note = noteText;
callback(err);
});
@@ -262,7 +266,9 @@ export default {
@startReplying="showReplyForm"
@deleteNote="deleteNoteHandler"
>
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
<template #footer="{ showReplies }">
<draft-note
v-if="showDraft(discussion.reply_id)"
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 0feb77be653..5ea431224ce 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -4,15 +4,16 @@ import $ from 'jquery';
import { escape, isEmpty } from 'lodash';
import { mapGetters, mapActions } from 'vuex';
import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
+import createFlash from '~/flash';
import httpStatusCodes from '~/lib/utils/http_status';
import { truncateSha } from '~/lib/utils/text_utility';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
-import { deprecatedCreateFlash as Flash } from '../../flash';
import { __, s__, sprintf } from '../../locale';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '../event_hub';
import noteable from '../mixins/noteable';
import resolvable from '../mixins/resolvable';
+import { renderMarkdown } from '../utils';
import {
getStartLineNumber,
getEndLineNumber,
@@ -247,7 +248,9 @@ export default {
this.isDeleting = false;
})
.catch(() => {
- Flash(__('Something went wrong while deleting your note. Please try again.'));
+ createFlash({
+ message: __('Something went wrong while deleting your note. Please try again.'),
+ });
this.isDeleting = false;
});
}
@@ -298,7 +301,7 @@ export default {
this.isRequesting = true;
this.oldContent = this.note.note_html;
// eslint-disable-next-line vue/no-mutating-props
- this.note.note_html = escape(noteText);
+ this.note.note_html = renderMarkdown(noteText);
this.updateNote(data)
.then(() => {
@@ -316,7 +319,10 @@ export default {
this.setSelectedCommentPositionHover();
this.$nextTick(() => {
const msg = __('Something went wrong while editing your comment. Please try again.');
- Flash(msg, 'alert', this.$el);
+ createFlash({
+ message: msg,
+ parent: this.$el,
+ });
this.recoverNoteContent(noteText);
callback();
});
@@ -387,7 +393,9 @@ export default {
:img-alt="author.name"
:img-size="40"
>
- <slot slot="avatar-badge" name="avatar-badge"></slot>
+ <template #avatar-badge>
+ <slot name="avatar-badge"></slot>
+ </template>
</user-avatar-link>
</div>
<div class="timeline-content">
@@ -398,7 +406,9 @@ export default {
:note-id="note.id"
:is-confidential="note.confidential"
>
- <slot slot="note-header-info" name="note-header-info"></slot>
+ <template #note-header-info>
+ <slot name="note-header-info"></slot>
+ </template>
<span v-if="commit" v-safe-html="actionText"></span>
<span v-else-if="note.created_at" class="d-none d-sm-inline">&middot;</span>
</note-header>
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 433f75a752d..29c60b96d8a 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,13 +1,13 @@
<script>
import { mapGetters, mapActions } from 'vuex';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import initUserPopovers from '~/user_popovers';
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 { deprecatedCreateFlash as Flash } from '../../flash';
import { getLocationHash, doesHashExistInUrl } 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';
@@ -66,6 +66,7 @@ export default {
data() {
return {
currentFilter: null,
+ renderSkeleton: !this.shouldShow,
};
},
computed: {
@@ -93,7 +94,7 @@ export default {
return this.noteableData.noteableType;
},
allDiscussions() {
- if (this.isLoading) {
+ if (this.renderSkeleton || this.isLoading) {
const prerenderedNotesCount = parseInt(this.notesData.prerenderedNotesCount, 10) || 0;
return new Array(prerenderedNotesCount).fill({
@@ -122,6 +123,10 @@ export default {
if (!this.isNotesFetched) {
this.fetchNotes();
}
+
+ setTimeout(() => {
+ this.renderSkeleton = !this.shouldShow;
+ });
},
discussionTabCounterText(val) {
if (this.discussionsCount) {
@@ -216,7 +221,9 @@ export default {
.catch(() => {
this.setLoadingState(false);
this.setNotesFetchedState(true);
- Flash(__('Something went wrong while fetching comments. Please try again.'));
+ createFlash({
+ message: __('Something went wrong while fetching comments. Please try again.'),
+ });
});
},
initPolling() {
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
index 27ed8e203b0..9783def1b46 100644
--- a/app/assets/javascripts/notes/mixins/resolvable.js
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
export default {
@@ -46,7 +46,10 @@ export default {
this.isResolving = false;
const msg = __('Something went wrong while resolving this discussion. Please try again.');
- Flash(msg, 'alert', this.$el);
+ createFlash({
+ message: msg,
+ parent: this.$el,
+ });
});
},
},
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 086e9122c60..6a4a3263e4a 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import Visibility from 'visibilityjs';
import Vue from 'vue';
import Api from '~/api';
+import createFlash from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
@@ -9,7 +10,6 @@ import { confidentialWidget } from '~/sidebar/components/confidential/sidebar_co
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 loadAwardsHandler from '../../awards_handler';
-import { deprecatedCreateFlash as Flash } from '../../flash';
import { isInViewport, scrollToElement, isInMRPage } from '../../lib/utils/common_utils';
import Poll from '../../lib/utils/poll';
import { create } from '../../lib/utils/recurrence';
@@ -312,25 +312,23 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.notes-form .flash-container').hide(); // hide previous flash notification
commit(types.REMOVE_PLACEHOLDER_NOTES); // remove previous placeholders
- if (replyId) {
- if (hasQuickActions) {
- placeholderText = utils.stripQuickActions(placeholderText);
- }
+ if (hasQuickActions) {
+ placeholderText = utils.stripQuickActions(placeholderText);
+ }
- if (placeholderText.length) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- noteBody: placeholderText,
- replyId,
- });
- }
+ if (placeholderText.length) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ noteBody: placeholderText,
+ replyId,
+ });
+ }
- if (hasQuickActions) {
- commit(types.SHOW_PLACEHOLDER_NOTE, {
- isSystemNote: true,
- noteBody: utils.getQuickActionText(note),
- replyId,
- });
- }
+ if (hasQuickActions) {
+ commit(types.SHOW_PLACEHOLDER_NOTE, {
+ isSystemNote: true,
+ noteBody: utils.getQuickActionText(note),
+ replyId,
+ });
}
const processQuickActions = (res) => {
@@ -354,7 +352,11 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
$('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash(message || __('Commands applied'), 'notice', noteData.flashContainer);
+ createFlash({
+ message: message || __('Commands applied'),
+ type: 'notice',
+ parent: noteData.flashContainer,
+ });
}
return res;
@@ -375,11 +377,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
awardsHandler.scrollToAwards();
})
.catch(() => {
- Flash(
- __('Something went wrong while adding your award. Please try again.'),
- 'alert',
- noteData.flashContainer,
- );
+ createFlash({
+ message: __('Something went wrong while adding your award. Please try again.'),
+ parent: noteData.flashContainer,
+ });
})
.then(() => res);
};
@@ -397,9 +398,7 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
};
const removePlaceholder = (res) => {
- if (replyId) {
- commit(types.REMOVE_PLACEHOLDER_NOTES);
- }
+ commit(types.REMOVE_PLACEHOLDER_NOTES);
return res;
};
@@ -417,7 +416,10 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const errorMsg = sprintf(__('Your comment could not be submitted because %{error}'), {
error: base[0].toLowerCase(),
});
- Flash(errorMsg, 'alert', noteData.flashContainer);
+ createFlash({
+ message: errorMsg,
+ parent: noteData.flashContainer,
+ });
return { ...data, hasFlash: true };
}
}
@@ -480,7 +482,9 @@ 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 = Flash(__('Something went wrong while fetching latest comments.'));
+ flashContainer = createFlash({
+ message: __('Something went wrong while fetching latest comments.'),
+ });
setTimeout(() => eTagPoll.restart(), NOTES_POLLING_INTERVAL);
});
@@ -570,7 +574,9 @@ export const filterDiscussion = ({ dispatch }, { path, filter, persistFilter })
.catch(() => {
dispatch('setLoadingState', false);
dispatch('setNotesFetchedState', true);
- Flash(__('Something went wrong while fetching comments. Please try again.'));
+ createFlash({
+ message: __('Something went wrong while fetching comments. Please try again.'),
+ });
});
};
@@ -613,7 +619,10 @@ export const submitSuggestion = (
const flashMessage = errorMessage || defaultMessage;
- Flash(__(flashMessage), 'alert', flashContainer);
+ createFlash({
+ message: __(flashMessage),
+ parent: flashContainer,
+ });
})
.finally(() => {
commit(types.SET_RESOLVING_DISCUSSION, false);
@@ -646,7 +655,10 @@ export const submitSuggestionBatch = ({ commit, dispatch, state }, { flashContai
const flashMessage = errorMessage || defaultMessage;
- Flash(__(flashMessage), 'alert', flashContainer);
+ createFlash({
+ message: __(flashMessage),
+ parent: flashContainer,
+ });
})
.finally(() => {
commit(types.SET_APPLYING_BATCH_STATE, false);
@@ -685,7 +697,9 @@ export const fetchDescriptionVersion = ({ dispatch }, { endpoint, startingVersio
})
.catch((error) => {
dispatch('receiveDescriptionVersionError', error);
- Flash(__('Something went wrong while fetching description changes. Please try again.'));
+ createFlash({
+ message: __('Something went wrong while fetching description changes. Please try again.'),
+ });
});
};
@@ -717,7 +731,9 @@ export const softDeleteDescriptionVersion = (
})
.catch((error) => {
dispatch('receiveDeleteDescriptionVersionError', error);
- Flash(__('Something went wrong while deleting description changes. Please try again.'));
+ createFlash({
+ message: __('Something went wrong while deleting description changes. Please try again.'),
+ });
// Throw an error here because a component like SystemNote -
// needs to know if the request failed to reset its internal state.
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index b04b1d28ffa..956221d69ae 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -279,7 +279,7 @@ export const getDiscussion = (state) => (discussionId) =>
export const commentsDisabled = (state) => state.commentsDisabled;
export const suggestionsCount = (state, getters) =>
- Object.values(getters.notesById).filter((n) => n.suggestions.length).length;
+ Object.values(getters.notesById).filter((n) => n.suggestions?.length).length;
export const hasDrafts = (state, getters, rootState, rootGetters) =>
Boolean(rootGetters['batchComments/hasDrafts']);
diff --git a/app/assets/javascripts/notes/utils.js b/app/assets/javascripts/notes/utils.js
index 7966a884eab..ec18a570960 100644
--- a/app/assets/javascripts/notes/utils.js
+++ b/app/assets/javascripts/notes/utils.js
@@ -1,4 +1,7 @@
/* eslint-disable @gitlab/require-i18n-strings */
+import marked from 'marked';
+import { sanitize } from '~/lib/dompurify';
+import { markdownConfig } from '~/lib/utils/text_utility';
/**
* Tracks snowplow event when User toggles timeline view
@@ -10,3 +13,7 @@ export const trackToggleTimelineView = (enabled) => ({
label: 'Status',
property: enabled,
});
+
+export const renderMarkdown = (rawMarkdown) => {
+ return sanitize(marked(rawMarkdown), markdownConfig);
+};
diff --git a/app/assets/javascripts/notifications/components/custom_notifications_modal.vue b/app/assets/javascripts/notifications/components/custom_notifications_modal.vue
index 2b5cff35fc8..182948c39f4 100644
--- a/app/assets/javascripts/notifications/components/custom_notifications_modal.vue
+++ b/app/assets/javascripts/notifications/components/custom_notifications_modal.vue
@@ -73,7 +73,7 @@ export default {
this.events = this.buildEvents(events);
} catch (error) {
- this.$toast.show(this.$options.i18n.loadNotificationLevelErrorMessage, { type: 'error' });
+ this.$toast.show(this.$options.i18n.loadNotificationLevelErrorMessage);
} finally {
this.isLoading = false;
}
@@ -93,7 +93,7 @@ export default {
this.events = this.buildEvents(events);
} catch (error) {
- this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
+ this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage);
}
},
},
@@ -132,7 +132,7 @@ export default {
@change="updateEvent($event, event)"
>
<strong>{{ event.name }}</strong
- ><gl-loading-icon v-if="event.loading" :inline="true" class="gl-ml-2" />
+ ><gl-loading-icon v-if="event.loading" size="sm" :inline="true" class="gl-ml-2" />
</gl-form-checkbox>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/notifications/components/notifications_dropdown.vue b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
index 4963b9386c1..69eb2115bf4 100644
--- a/app/assets/javascripts/notifications/components/notifications_dropdown.vue
+++ b/app/assets/javascripts/notifications/components/notifications_dropdown.vue
@@ -104,7 +104,7 @@ export default {
this.selectedNotificationLevel = level;
this.openNotificationsModal();
} catch (error) {
- this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage, { type: 'error' });
+ this.$toast.show(this.$options.i18n.updateNotificationLevelErrorMessage);
} finally {
this.isLoading = false;
}
diff --git a/app/assets/javascripts/operation_settings/store/actions.js b/app/assets/javascripts/operation_settings/store/actions.js
index 969904bc6d0..529eb7d207b 100644
--- a/app/assets/javascripts/operation_settings/store/actions.js
+++ b/app/assets/javascripts/operation_settings/store/actions.js
@@ -37,6 +37,5 @@ export const receiveSaveChangesError = (_, error) => {
createFlash({
message: `${__('There was an error saving your changes.')} ${message}`,
- type: 'alert',
});
};
diff --git a/app/assets/javascripts/packages/details/components/app.vue b/app/assets/javascripts/packages/details/components/app.vue
index 55ffe10a608..59da32e6666 100644
--- a/app/assets/javascripts/packages/details/components/app.vue
+++ b/app/assets/javascripts/packages/details/components/app.vue
@@ -11,8 +11,8 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapState } from 'vuex';
-import { objectToQueryString } from '~/lib/utils/common_utils';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { objectToQuery } from '~/lib/utils/url_utility';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
import PackageListRow from '../../shared/components/package_list_row.vue';
@@ -114,7 +114,7 @@ export default {
!this.groupListUrl || document.referrer.includes(this.projectName)
? this.projectListUrl
: this.groupListUrl; // to avoid security issue url are supplied from backend
- const modalQuery = objectToQueryString({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+ const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
window.location.replace(`${returnTo}?${modalQuery}`);
},
handleFileDelete(file) {
diff --git a/app/assets/javascripts/packages/list/constants.js b/app/assets/javascripts/packages/list/constants.js
index d871c2e4d24..2c6fd94024e 100644
--- a/app/assets/javascripts/packages/list/constants.js
+++ b/app/assets/javascripts/packages/list/constants.js
@@ -86,6 +86,14 @@ export const PACKAGE_TYPES = [
title: s__('PackageRegistry|RubyGems'),
type: PackageType.RUBYGEMS,
},
+ {
+ title: s__('PackageRegistry|Debian'),
+ type: PackageType.DEBIAN,
+ },
+ {
+ title: s__('PackageRegistry|Helm'),
+ type: PackageType.HELM,
+ },
];
export const LIST_TITLE_TEXT = s__('PackageRegistry|Package Registry');
diff --git a/app/assets/javascripts/packages/shared/constants.js b/app/assets/javascripts/packages/shared/constants.js
index 0ef6a3d0d12..b4cdca34d92 100644
--- a/app/assets/javascripts/packages/shared/constants.js
+++ b/app/assets/javascripts/packages/shared/constants.js
@@ -9,6 +9,8 @@ export const PackageType = {
COMPOSER: 'composer',
RUBYGEMS: 'rubygems',
GENERIC: 'generic',
+ DEBIAN: 'debian',
+ HELM: 'helm',
};
// we want this separated from the main dictionary to avoid it being pulled in the search of package
diff --git a/app/assets/javascripts/packages/shared/utils.js b/app/assets/javascripts/packages/shared/utils.js
index bd35a47ca4d..7e86e5b2991 100644
--- a/app/assets/javascripts/packages/shared/utils.js
+++ b/app/assets/javascripts/packages/shared/utils.js
@@ -25,6 +25,10 @@ export const getPackageTypeLabel = (packageType) => {
return s__('PackageRegistry|Composer');
case PackageType.GENERIC:
return s__('PackageRegistry|Generic');
+ case PackageType.DEBIAN:
+ return s__('PackageRegistry|Debian');
+ case PackageType.HELM:
+ return s__('PackageRegistry|Helm');
default:
return null;
}
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
new file mode 100644
index 00000000000..e2a2fb1430d
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
@@ -0,0 +1,301 @@
+<script>
+/*
+ * The commented part of this component needs to be re-enabled in the refactor process,
+ * See here for more info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64939
+ */
+import {
+ GlBadge,
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlTooltipDirective,
+ GlEmptyState,
+ GlTab,
+ GlTabs,
+ GlSprintf,
+} from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { objectToQuery } from '~/lib/utils/url_utility';
+import { s__, __ } from '~/locale';
+// import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
+// import DependencyRow from '~/packages/details/components/dependency_row.vue';
+// import InstallationCommands from '~/packages/details/components/installation_commands.vue';
+// import PackageFiles from '~/packages/details/components/package_files.vue';
+// import PackageHistory from '~/packages/details/components/package_history.vue';
+// import PackageListRow from '~/packages/shared/components/package_list_row.vue';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import {
+ PackageType,
+ TrackingActions,
+ SHOW_DELETE_SUCCESS_ALERT,
+} from '~/packages/shared/constants';
+import { packageTypeToTrackCategory } from '~/packages/shared/utils';
+import Tracking from '~/tracking';
+
+export default {
+ name: 'PackagesApp',
+ components: {
+ GlBadge,
+ GlButton,
+ GlEmptyState,
+ GlModal,
+ GlTab,
+ GlTabs,
+ GlSprintf,
+ PackageTitle: () => import('~/packages/details/components/package_title.vue'),
+ TerraformTitle: () =>
+ import('~/packages_and_registries/infrastructure_registry/components/details_title.vue'),
+ PackagesListLoader,
+ // PackageListRow,
+ // DependencyRow,
+ // PackageHistory,
+ // AdditionalMetadata,
+ // InstallationCommands,
+ // PackageFiles,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ GlModal: GlModalDirective,
+ },
+ mixins: [Tracking.mixin()],
+ inject: [
+ 'titleComponent',
+ 'projectName',
+ 'canDelete',
+ 'svgPath',
+ 'npmPath',
+ 'npmHelpPath',
+ 'projectListUrl',
+ 'groupListUrl',
+ ],
+ trackingActions: { ...TrackingActions },
+ data() {
+ return {
+ fileToDelete: null,
+ packageEntity: {},
+ };
+ },
+ computed: {
+ packageFiles() {
+ return this.packageEntity.packageFiles;
+ },
+ isLoading() {
+ return false;
+ },
+ isValidPackage() {
+ return Boolean(this.packageEntity.name);
+ },
+ tracking() {
+ return {
+ category: packageTypeToTrackCategory(this.packageEntity.package_type),
+ };
+ },
+ hasVersions() {
+ return this.packageEntity.versions?.length > 0;
+ },
+ packageDependencies() {
+ return this.packageEntity.dependency_links || [];
+ },
+ showDependencies() {
+ return this.packageEntity.package_type === PackageType.NUGET;
+ },
+ showFiles() {
+ return this.packageEntity?.package_type !== PackageType.COMPOSER;
+ },
+ },
+ methods: {
+ formatSize(size) {
+ return numberToHumanSize(size);
+ },
+ getPackageVersions() {
+ if (!this.packageEntity.versions) {
+ // this.fetchPackageVersions();
+ }
+ },
+ async confirmPackageDeletion() {
+ this.track(TrackingActions.DELETE_PACKAGE);
+
+ await this.deletePackage();
+
+ const returnTo =
+ !this.groupListUrl || document.referrer.includes(this.projectName)
+ ? this.projectListUrl
+ : this.groupListUrl; // to avoid security issue url are supplied from backend
+
+ const modalQuery = objectToQuery({ [SHOW_DELETE_SUCCESS_ALERT]: true });
+
+ window.location.replace(`${returnTo}?${modalQuery}`);
+ },
+ handleFileDelete(file) {
+ this.track(TrackingActions.REQUEST_DELETE_PACKAGE_FILE);
+ this.fileToDelete = { ...file };
+ this.$refs.deleteFileModal.show();
+ },
+ confirmFileDelete() {
+ this.track(TrackingActions.DELETE_PACKAGE_FILE);
+ // this.deletePackageFile(this.fileToDelete.id);
+ this.fileToDelete = null;
+ },
+ },
+ i18n: {
+ deleteModalTitle: s__(`PackageRegistry|Delete Package Version`),
+ deleteModalContent: s__(
+ `PackageRegistry|You are about to delete version %{version} of %{name}. Are you sure?`,
+ ),
+ deleteFileModalTitle: s__(`PackageRegistry|Delete Package File`),
+ deleteFileModalContent: s__(
+ `PackageRegistry|You are about to delete %{filename}. This is a destructive action that may render your package unusable. Are you sure?`,
+ ),
+ },
+ modal: {
+ packageDeletePrimaryAction: {
+ text: __('Delete'),
+ attributes: [
+ { variant: 'danger' },
+ { category: 'primary' },
+ { 'data-qa-selector': 'delete_modal_button' },
+ ],
+ },
+ fileDeletePrimaryAction: {
+ text: __('Delete'),
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
+ },
+ cancelAction: {
+ text: __('Cancel'),
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ v-if="!isValidPackage"
+ :title="s__('PackageRegistry|Unable to load package')"
+ :description="s__('PackageRegistry|There was a problem fetching the details for this package.')"
+ :svg-path="svgPath"
+ />
+
+ <div v-else class="packages-app">
+ <component :is="titleComponent">
+ <template #delete-button>
+ <gl-button
+ v-if="canDelete"
+ v-gl-modal="'delete-modal'"
+ class="js-delete-button"
+ variant="danger"
+ category="primary"
+ data-qa-selector="delete_button"
+ >
+ {{ __('Delete') }}
+ </gl-button>
+ </template>
+ </component>
+
+ <gl-tabs>
+ <gl-tab :title="__('Detail')">
+ <div data-qa-selector="package_information_content">
+ <!-- <package-history :package-entity="packageEntity" :project-name="projectName" />
+
+ <installation-commands
+ :package-entity="packageEntity"
+ :npm-path="npmPath"
+ :npm-help-path="npmHelpPath"
+ />
+
+ <additional-metadata :package-entity="packageEntity" /> -->
+ </div>
+
+ <!-- <package-files
+ v-if="showFiles"
+ :package-files="packageFiles"
+ :can-delete="canDelete"
+ @download-file="track($options.trackingActions.PULL_PACKAGE)"
+ @delete-file="handleFileDelete"
+ /> -->
+ </gl-tab>
+
+ <gl-tab v-if="showDependencies" title-item-class="js-dependencies-tab">
+ <template #title>
+ <span>{{ __('Dependencies') }}</span>
+ <gl-badge size="sm" data-testid="dependencies-badge">{{
+ packageDependencies.length
+ }}</gl-badge>
+ </template>
+
+ <template v-if="packageDependencies.length > 0">
+ <dependency-row
+ v-for="(dep, index) in packageDependencies"
+ :key="index"
+ :dependency="dep"
+ />
+ </template>
+
+ <p v-else class="gl-mt-3" data-testid="no-dependencies-message">
+ {{ s__('PackageRegistry|This NuGet package has no dependencies.') }}
+ </p>
+ </gl-tab>
+
+ <gl-tab
+ :title="__('Other versions')"
+ title-item-class="js-versions-tab"
+ @click="getPackageVersions"
+ >
+ <template v-if="isLoading && !hasVersions">
+ <packages-list-loader />
+ </template>
+
+ <template v-else-if="hasVersions">
+ <!-- <package-list-row
+ v-for="v in packageEntity.versions"
+ :key="v.id"
+ :package-entity="{ name: packageEntity.name, ...v }"
+ :package-link="v.id.toString()"
+ :disable-delete="true"
+ :show-package-type="false"
+ /> -->
+ </template>
+
+ <p v-else class="gl-mt-3" data-testid="no-versions-message">
+ {{ s__('PackageRegistry|There are no other versions of this package.') }}
+ </p>
+ </gl-tab>
+ </gl-tabs>
+
+ <gl-modal
+ ref="deleteModal"
+ class="js-delete-modal"
+ modal-id="delete-modal"
+ :action-primary="$options.modal.packageDeletePrimaryAction"
+ :action-cancel="$options.modal.cancelAction"
+ @primary="confirmPackageDeletion"
+ @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE)"
+ >
+ <template #modal-title>{{ $options.i18n.deleteModalTitle }}</template>
+ <gl-sprintf :message="$options.i18n.deleteModalContent">
+ <template #version>
+ <strong>{{ packageEntity.version }}</strong>
+ </template>
+
+ <template #name>
+ <strong>{{ packageEntity.name }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+
+ <gl-modal
+ ref="deleteFileModal"
+ modal-id="delete-file-modal"
+ :action-primary="$options.modal.fileDeletePrimaryAction"
+ :action-cancel="$options.modal.cancelAction"
+ @primary="confirmFileDelete"
+ @canceled="track($options.trackingActions.CANCEL_DELETE_PACKAGE_FILE)"
+ >
+ <template #modal-title>{{ $options.i18n.deleteFileModalTitle }}</template>
+ <gl-sprintf v-if="fileToDelete" :message="$options.i18n.deleteFileModalContent">
+ <template #filename>
+ <strong>{{ fileToDelete.file_name }}</strong>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
new file mode 100644
index 00000000000..309b35a8084
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import Translate from '~/vue_shared/translate';
+import PackagesApp from '../components/details/app.vue';
+
+Vue.use(Translate);
+
+export default () => {
+ const el = document.getElementById('js-vue-packages-detail-new');
+ if (!el) {
+ return null;
+ }
+
+ const { canDelete, ...datasetOptions } = el.dataset;
+ return new Vue({
+ el,
+ provide: {
+ canDelete: parseBoolean(canDelete),
+ titleComponent: 'PackageTitle',
+ ...datasetOptions,
+ },
+ render(createElement) {
+ return createElement(PackagesApp);
+ },
+ });
+};
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 01d4861f5c2..ec3be43196c 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -86,7 +86,7 @@ export default {
this.alertMessage = ERROR_UPDATING_SETTINGS;
} else {
this.dismissAlert();
- this.$toast.show(SUCCESS_UPDATING_SETTINGS, { type: 'success' });
+ this.$toast.show(SUCCESS_UPDATING_SETTINGS);
}
})
.catch((e) => {
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/constants.js b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
index a2256c5c371..d29489a0b33 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/constants.js
+++ b/app/assets/javascripts/packages_and_registries/settings/group/constants.js
@@ -3,19 +3,19 @@ import { s__, __ } from '~/locale';
export const PACKAGE_SETTINGS_HEADER = s__('PackageRegistry|Package Registry');
export const PACKAGE_SETTINGS_DESCRIPTION = s__(
- 'PackageRegistry|GitLab Packages allows organizations to utilize GitLab as a private repository for a variety of common package formats. %{linkStart}More Information%{linkEnd}',
+ 'PackageRegistry|Use GitLab as a private registry for common package formats. %{linkStart}Learn more.%{linkEnd}',
);
export const DUPLICATES_TOGGLE_LABEL = s__('PackageRegistry|Allow duplicates');
export const DUPLICATES_ALLOWED_DISABLED = s__(
- 'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Packages with the same name and version are rejected.',
+ 'PackageRegistry|%{boldStart}Do not allow duplicates%{boldEnd} - Reject packages with the same name and version.',
);
export const DUPLICATES_ALLOWED_ENABLED = s__(
- 'PackageRegistry|%{boldStart}Allow duplicates%{boldEnd} - Packages with the same name and version are accepted.',
+ 'PackageRegistry|%{boldStart}Allow duplicates%{boldEnd} - Accept packages with the same name and version.',
);
export const DUPLICATES_SETTING_EXCEPTION_TITLE = __('Exceptions');
export const DUPLICATES_SETTINGS_EXCEPTION_LEGEND = s__(
- 'PackageRegistry|Packages can be published if their name or version matches this regex',
+ 'PackageRegistry|Publish packages if their name or version matches this regex.',
);
export const SUCCESS_UPDATING_SETTINGS = s__('PackageRegistry|Settings saved successfully');
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
index 41be70a3ad5..6030af9d2c3 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/settings_form.vue
@@ -88,8 +88,6 @@ export default {
return {
...this.value,
cadence: this.findDefaultOption('cadence'),
- keepN: this.findDefaultOption('keepN'),
- olderThan: this.findDefaultOption('olderThan'),
};
},
showLoadingIcon() {
@@ -158,14 +156,14 @@ export default {
.then(({ data }) => {
const errorMessage = data?.updateContainerExpirationPolicy?.errors[0];
if (errorMessage) {
- this.$toast.show(errorMessage, { type: 'error' });
+ this.$toast.show(errorMessage);
} else {
- this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE, { type: 'success' });
+ this.$toast.show(UPDATE_SETTINGS_SUCCESS_MESSAGE);
}
})
.catch((error) => {
this.setApiErrors(error);
- this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE, { type: 'error' });
+ this.$toast.show(UPDATE_SETTINGS_ERROR_MESSAGE);
})
.finally(() => {
this.mutationLoading = false;
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/utils.js b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
index 4a2d7c7d466..b577a051862 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/utils.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/utils.js
@@ -11,11 +11,14 @@ export const olderThanTranslationGenerator = (variable) => n__('%d day', '%d day
export const keepNTranslationGenerator = (variable) =>
n__('%d tag per image name', '%d tags per image name', variable);
-export const optionLabelGenerator = (collection, translationFn) =>
- collection.map((option) => ({
+export const optionLabelGenerator = (collection, translationFn) => {
+ const result = collection.map((option) => ({
...option,
label: translationFn(option.variable),
}));
+ result.unshift({ key: null, label: '' });
+ return result;
+};
export const formOptionsGenerator = () => {
return {
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index 3ad9d80b4f2..aa2f539b6e2 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,8 +1,7 @@
import $ from 'jquery';
import 'vendor/jquery.endless-scroll';
import axios from '~/lib/utils/axios_utils';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import { removeParams } from '~/lib/utils/url_utility';
+import { removeParams, getParameterByName } from '~/lib/utils/url_utility';
const ENDLESS_SCROLL_BOTTOM_PX = 400;
const ENDLESS_SCROLL_FIRE_DELAY_MS = 1000;
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index a88d35796f7..ab29f9149f7 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -5,4 +5,4 @@ import AbuseReports from './abuse_reports';
new AbuseReports(); /* eslint-disable-line no-new */
new UsersSelect(); /* eslint-disable-line no-new */
-document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
+initDeprecatedRemoveRowBehavior();
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
index a2fca238613..a5305777dd5 100644
--- a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/index.js
@@ -1,3 +1,3 @@
import setup from '~/admin/application_settings/setup_metrics_and_profiling';
-document.addEventListener('DOMContentLoaded', setup);
+setup();
diff --git a/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
new file mode 100644
index 00000000000..bf27b1a81ff
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/metrics_and_profiling/usage_statistics.js
@@ -0,0 +1,41 @@
+import { __ } from '~/locale';
+
+export const HELPER_TEXT_SERVICE_PING_DISABLED = __(
+ 'To enable Registration Features, make sure "Enable service ping" is checked.',
+);
+
+export const HELPER_TEXT_SERVICE_PING_ENABLED = __(
+ 'You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.',
+);
+
+function setHelperText(usagePingCheckbox) {
+ const helperTextId = document.getElementById('service_ping_features_helper_text');
+
+ const usagePingFeaturesLabel = document.getElementById('service_ping_features_label');
+
+ const usagePingFeaturesCheckbox = document.getElementById(
+ 'application_setting_usage_ping_features_enabled',
+ );
+
+ helperTextId.textContent = usagePingCheckbox.checked
+ ? HELPER_TEXT_SERVICE_PING_ENABLED
+ : HELPER_TEXT_SERVICE_PING_DISABLED;
+
+ usagePingFeaturesLabel.classList.toggle('gl-cursor-not-allowed', !usagePingCheckbox.checked);
+
+ usagePingFeaturesCheckbox.disabled = !usagePingCheckbox.checked;
+
+ if (!usagePingCheckbox.checked) {
+ usagePingFeaturesCheckbox.disabled = true;
+ usagePingFeaturesCheckbox.checked = false;
+ }
+}
+
+export default function initSetHelperText() {
+ const usagePingCheckbox = document.getElementById('application_setting_usage_ping_enabled');
+
+ setHelperText(usagePingCheckbox);
+ usagePingCheckbox.addEventListener('change', () => {
+ setHelperText(usagePingCheckbox);
+ });
+}
diff --git a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
index bc1d4dd6122..08f6633f424 100644
--- a/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
+++ b/app/assets/javascripts/pages/admin/application_settings/payload_previewer.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '../../../flash';
+import createFlash from '~/flash';
import axios from '../../../lib/utils/axios_utils';
import { __ } from '../../../locale';
@@ -38,7 +38,9 @@ export default class PayloadPreviewer {
})
.catch(() => {
this.spinner.classList.remove('d-inline-flex');
- flash(__('Error fetching payload data.'));
+ createFlash({
+ message: __('Error fetching payload data.'),
+ });
});
}
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index 5a16716fe2d..2a7e6a45cdd 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -1,8 +1,9 @@
import $ from 'jquery';
import { debounce } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { textColorForBackground } from '~/lib/utils/color_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
export default () => {
@@ -30,7 +31,11 @@ export default () => {
.then(({ data }) => {
$jsBroadcastMessagePreview.html(data.message);
})
- .catch(() => flash(__('An error occurred while rendering preview broadcast message')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while rendering preview broadcast message'),
+ }),
+ );
}
};
@@ -61,7 +66,7 @@ export default () => {
'input',
debounce(() => {
reloadPreview();
- }, 250),
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
);
const updateColorPreview = () => {
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index b7db6443658..f687423594d 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,7 +1,5 @@
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import initBroadcastMessagesForm from './broadcast_message';
-document.addEventListener('DOMContentLoaded', () => {
- initBroadcastMessagesForm();
- initDeprecatedRemoveRowBehavior();
-});
+initBroadcastMessagesForm();
+initDeprecatedRemoveRowBehavior();
diff --git a/app/assets/javascripts/pages/admin/clusters/destroy/index.js b/app/assets/javascripts/pages/admin/clusters/destroy/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/admin/clusters/destroy/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/destroy/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/clusters/edit/index.js b/app/assets/javascripts/pages/admin/clusters/edit/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/admin/clusters/edit/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/edit/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/admin/clusters/index.js b/app/assets/javascripts/pages/admin/clusters/index.js
index 4d04c37caa7..f398b1cee82 100644
--- a/app/assets/javascripts/pages/admin/clusters/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/index.js
@@ -1,5 +1,3 @@
import initCreateCluster from '~/create_cluster/init_create_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initCreateCluster(document, gon);
-});
+initCreateCluster(document, gon);
diff --git a/app/assets/javascripts/pages/admin/clusters/index/index.js b/app/assets/javascripts/pages/admin/clusters/index/index.js
index a99e0dfa4f0..a1ba920b322 100644
--- a/app/assets/javascripts/pages/admin/clusters/index/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/index/index.js
@@ -1,8 +1,6 @@
import initClustersListApp from '~/clusters_list';
import PersistentUserCallout from '~/persistent_user_callout';
-document.addEventListener('DOMContentLoaded', () => {
- const callout = document.querySelector('.gcp-signup-offer');
- PersistentUserCallout.factory(callout);
- initClustersListApp();
-});
+const callout = document.querySelector('.gcp-signup-offer');
+PersistentUserCallout.factory(callout);
+initClustersListApp();
diff --git a/app/assets/javascripts/pages/admin/clusters/new/index.js b/app/assets/javascripts/pages/admin/clusters/new/index.js
index 876bab0b339..de9ded87ef3 100644
--- a/app/assets/javascripts/pages/admin/clusters/new/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/new/index.js
@@ -1,5 +1,3 @@
import initNewCluster from '~/clusters/new_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initNewCluster();
-});
+initNewCluster();
diff --git a/app/assets/javascripts/pages/admin/clusters/show/index.js b/app/assets/javascripts/pages/admin/clusters/show/index.js
index 9d94973af0d..524b2c6f66a 100644
--- a/app/assets/javascripts/pages/admin/clusters/show/index.js
+++ b/app/assets/javascripts/pages/admin/clusters/show/index.js
@@ -2,8 +2,6 @@ import ClustersBundle from '~/clusters/clusters_bundle';
import initIntegrationForm from '~/clusters/forms/show';
import initClusterHealth from '~/pages/projects/clusters/show/cluster_health';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
- initClusterHealth();
- initIntegrationForm();
-});
+new ClustersBundle(); // eslint-disable-line no-new
+initClusterHealth();
+initIntegrationForm();
diff --git a/app/assets/javascripts/pages/admin/dev_ops_report/index.js b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
index d6fa1be29b0..a94a60af7ff 100644
--- a/app/assets/javascripts/pages/admin/dev_ops_report/index.js
+++ b/app/assets/javascripts/pages/admin/dev_ops_report/index.js
@@ -1,5 +1,5 @@
import initDevOpsScore from '~/analytics/devops_report/devops_score';
-import initDevOpsScoreDisabledUsagePing from '~/analytics/devops_report/devops_score_disabled_usage_ping';
+import initDevOpsScoreDisabledServicePing from '~/analytics/devops_report/devops_score_disabled_service_ping';
-initDevOpsScoreDisabledUsagePing();
+initDevOpsScoreDisabledServicePing();
initDevOpsScore();
diff --git a/app/assets/javascripts/pages/admin/identities/index.js b/app/assets/javascripts/pages/admin/identities/index.js
new file mode 100644
index 00000000000..a9f5f00cb9b
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/identities/index.js
@@ -0,0 +1,6 @@
+import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
+import initConfirmModal from '~/confirm_modal';
+
+initAdminUserActions();
+initDeleteUserModals();
+initConfirmModal();
diff --git a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
index dc1bb88bf4b..8fbc8dc17bc 100644
--- a/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
+++ b/app/assets/javascripts/pages/admin/impersonation_tokens/index.js
@@ -1,3 +1,8 @@
import { initExpiresAtField } from '~/access_tokens';
+import { initAdminUserActions, initDeleteUserModals } from '~/admin/users';
+import initConfirmModal from '~/confirm_modal';
+initAdminUserActions();
+initDeleteUserModals();
initExpiresAtField();
+initConfirmModal();
diff --git a/app/assets/javascripts/pages/admin/integrations/edit/index.js b/app/assets/javascripts/pages/admin/integrations/edit/index.js
index ba4b271f09e..8002fa8bf78 100644
--- a/app/assets/javascripts/pages/admin/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/admin/integrations/edit/index.js
@@ -1,7 +1,7 @@
import IntegrationSettingsForm from '~/integrations/integration_settings_form';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-document.addEventListener('DOMContentLoaded', () => {
+function initIntegrations() {
const prometheusSettingsWrapper = document.querySelector('.js-prometheus-metrics-monitoring');
const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
integrationSettingsForm.init();
@@ -10,4 +10,6 @@ document.addEventListener('DOMContentLoaded', () => {
const prometheusMetrics = new PrometheusMetrics('.js-prometheus-metrics-monitoring');
prometheusMetrics.loadActiveMetrics();
}
-});
+}
+
+initIntegrations();
diff --git a/app/assets/javascripts/pages/admin/jobs/index/index.js b/app/assets/javascripts/pages/admin/jobs/index/index.js
index 46ddb95299d..a4d89889d57 100644
--- a/app/assets/javascripts/pages/admin/jobs/index/index.js
+++ b/app/assets/javascripts/pages/admin/jobs/index/index.js
@@ -5,7 +5,7 @@ import stopJobsModal from './components/stop_jobs_modal.vue';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+function initJobs() {
const buttonId = 'js-stop-jobs-button';
const modalId = 'stop-jobs-modal';
const stopJobsButton = document.getElementById(buttonId);
@@ -31,4 +31,6 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
}
-});
+}
+
+initJobs();
diff --git a/app/assets/javascripts/pages/admin/keys/index.js b/app/assets/javascripts/pages/admin/keys/index.js
index 45b83ffcd67..868c8e33077 100644
--- a/app/assets/javascripts/pages/admin/keys/index.js
+++ b/app/assets/javascripts/pages/admin/keys/index.js
@@ -1,5 +1,3 @@
import initConfirmModal from '~/confirm_modal';
-document.addEventListener('DOMContentLoaded', () => {
- initConfirmModal();
-});
+initConfirmModal();
diff --git a/app/assets/javascripts/pages/admin/labels/index/index.js b/app/assets/javascripts/pages/admin/labels/index/index.js
index 17ee7c03ed6..0ceab3b922f 100644
--- a/app/assets/javascripts/pages/admin/labels/index/index.js
+++ b/app/assets/javascripts/pages/admin/labels/index/index.js
@@ -1,4 +1,4 @@
-document.addEventListener('DOMContentLoaded', () => {
+function initLabels() {
const pagination = document.querySelector('.labels .gl-pagination');
const emptyState = document.querySelector('.labels .nothing-here-block.hidden');
@@ -18,4 +18,6 @@ document.addEventListener('DOMContentLoaded', () => {
document.querySelectorAll('.js-remove-label').forEach((row) => {
row.addEventListener('ajax:success', removeLabelSuccessCallback);
});
-});
+}
+
+initLabels();
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 b92fc8d125d..055d6f40c14 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
@@ -46,7 +46,7 @@ export default {
return sprintf(
s__(`AdminProjects|
You’re about to permanently delete the project %{projectName}, its repository,
- and all related resources including issues, merge requests, etc.. Once you confirm and press
+ and all related resources, including issues and merge requests. Once you confirm and press
%{strong_start}Delete project%{strong_end}, it cannot be undone or recovered.`),
{
projectName: `<strong>${escape(this.projectName)}</strong>`,
diff --git a/app/assets/javascripts/pages/admin/projects/index/index.js b/app/assets/javascripts/pages/admin/projects/index/index.js
index cc9a9b6cc38..c6cf4a46dba 100644
--- a/app/assets/javascripts/pages/admin/projects/index/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index/index.js
@@ -13,9 +13,11 @@ import deleteProjectModal from './components/delete_project_modal.vue';
const deleteModal = new Vue({
el: deleteProjectModalEl,
- data: {
- deleteProjectUrl: '',
- projectName: '',
+ data() {
+ return {
+ deleteProjectUrl: '',
+ projectName: '',
+ };
},
mounted() {
const deleteProjectButtons = document.querySelectorAll('.delete-project-button');
diff --git a/app/assets/javascripts/pages/admin/spam_logs/index.js b/app/assets/javascripts/pages/admin/spam_logs/index.js
index e5ab5d43bbf..ac850a6467b 100644
--- a/app/assets/javascripts/pages/admin/spam_logs/index.js
+++ b/app/assets/javascripts/pages/admin/spam_logs/index.js
@@ -1,3 +1,3 @@
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
-document.addEventListener('DOMContentLoaded', initDeprecatedRemoveRowBehavior);
+initDeprecatedRemoveRowBehavior();
diff --git a/app/assets/javascripts/pages/admin/users/index.js b/app/assets/javascripts/pages/admin/users/index.js
index 9a8b0c9990f..41e99a3baf5 100644
--- a/app/assets/javascripts/pages/admin/users/index.js
+++ b/app/assets/javascripts/pages/admin/users/index.js
@@ -1,64 +1,7 @@
-import Vue from 'vue';
-
-import { initAdminUsersApp } from '~/admin/users';
+import { initAdminUsersApp, initDeleteUserModals, initAdminUserActions } from '~/admin/users';
import initConfirmModal from '~/confirm_modal';
-import csrf from '~/lib/utils/csrf';
-import Translate from '~/vue_shared/translate';
-import ModalManager from './components/user_modal_manager.vue';
-
-const CONFIRM_DELETE_BUTTON_SELECTOR = '.js-delete-user-modal-button';
-const MODAL_TEXTS_CONTAINER_SELECTOR = '#js-modal-texts';
-const MODAL_MANAGER_SELECTOR = '#js-delete-user-modal';
-
-function loadModalsConfigurationFromHtml(modalsElement) {
- const modalsConfiguration = {};
-
- if (!modalsElement) {
- /* eslint-disable-next-line @gitlab/require-i18n-strings */
- throw new Error('Modals content element not found!');
- }
-
- Array.from(modalsElement.children).forEach((node) => {
- const { modal, ...config } = node.dataset;
- modalsConfiguration[modal] = {
- title: node.dataset.title,
- ...config,
- content: node.innerHTML,
- };
- });
-
- return modalsConfiguration;
-}
-
-document.addEventListener('DOMContentLoaded', () => {
- Vue.use(Translate);
-
- initAdminUsersApp();
-
- const modalConfiguration = loadModalsConfigurationFromHtml(
- document.querySelector(MODAL_TEXTS_CONTAINER_SELECTOR),
- );
-
- // eslint-disable-next-line no-new
- new Vue({
- el: MODAL_MANAGER_SELECTOR,
- functional: true,
- methods: {
- show(...args) {
- this.$refs.manager.show(...args);
- },
- },
- render(h) {
- return h(ModalManager, {
- ref: 'manager',
- props: {
- selector: CONFIRM_DELETE_BUTTON_SELECTOR,
- modalConfiguration,
- csrfToken: csrf.token,
- },
- });
- },
- });
- initConfirmModal();
-});
+initAdminUsersApp();
+initAdminUserActions();
+initDeleteUserModals();
+initConfirmModal();
diff --git a/app/assets/javascripts/pages/admin/users/keys/index.js b/app/assets/javascripts/pages/admin/users/keys/index.js
deleted file mode 100644
index 45b83ffcd67..00000000000
--- a/app/assets/javascripts/pages/admin/users/keys/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-import initConfirmModal from '~/confirm_modal';
-
-document.addEventListener('DOMContentLoaded', () => {
- initConfirmModal();
-});
diff --git a/app/assets/javascripts/pages/dashboard/groups/index/index.js b/app/assets/javascripts/pages/dashboard/groups/index/index.js
index b9277106a71..c14848c4798 100644
--- a/app/assets/javascripts/pages/dashboard/groups/index/index.js
+++ b/app/assets/javascripts/pages/dashboard/groups/index/index.js
@@ -1,5 +1,3 @@
import initGroupsList from '~/groups';
-document.addEventListener('DOMContentLoaded', () => {
- initGroupsList();
-});
+initGroupsList();
diff --git a/app/assets/javascripts/pages/dashboard/milestones/show/index.js b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
index 397149aaa9e..1f3e458fe17 100644
--- a/app/assets/javascripts/pages/dashboard/milestones/show/index.js
+++ b/app/assets/javascripts/pages/dashboard/milestones/show/index.js
@@ -2,8 +2,6 @@ import Milestone from '~/milestone';
import Sidebar from '~/right_sidebar';
import MountMilestoneSidebar from '~/sidebar/mount_milestone_sidebar';
-document.addEventListener('DOMContentLoaded', () => {
- new Milestone(); // eslint-disable-line no-new
- new Sidebar(); // eslint-disable-line no-new
- new MountMilestoneSidebar(); // eslint-disable-line no-new
-});
+new Milestone(); // eslint-disable-line no-new
+new Sidebar(); // eslint-disable-line no-new
+new MountMilestoneSidebar(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index 42341436b55..946076cfb29 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import { getGroups } from '~/api/groups_api';
import { getProjects } from '~/api/projects_api';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { isMetaClick } from '~/lib/utils/common_utils';
import { addDelimiter } from '~/lib/utils/text_utility';
@@ -103,7 +103,9 @@ export default class Todos {
})
.catch(() => {
this.updateRowState(target, true);
- return flash(__('Error updating status of to-do item.'));
+ return createFlash({
+ message: __('Error updating status of to-do item.'),
+ });
});
}
@@ -145,7 +147,11 @@ export default class Todos {
this.updateAllState(target, data);
this.updateBadges(data);
})
- .catch(() => flash(__('Error updating status for all to-do items.')));
+ .catch(() =>
+ createFlash({
+ message: __('Error updating status for all to-do items.'),
+ }),
+ );
}
updateAllState(target, data) {
diff --git a/app/assets/javascripts/pages/explore/projects/index.js b/app/assets/javascripts/pages/explore/projects/index.js
index 01001d4f3ff..6c9378b7231 100644
--- a/app/assets/javascripts/pages/explore/projects/index.js
+++ b/app/assets/javascripts/pages/explore/projects/index.js
@@ -1,5 +1,3 @@
import ProjectsList from '~/projects_list';
-document.addEventListener('DOMContentLoaded', () => {
- new ProjectsList(); // eslint-disable-line no-new
-});
+new ProjectsList(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/clusters/destroy/index.js b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/groups/clusters/destroy/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/destroy/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/clusters/edit/index.js b/app/assets/javascripts/pages/groups/clusters/edit/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/groups/clusters/edit/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/edit/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/clusters/index.js b/app/assets/javascripts/pages/groups/clusters/index.js
index d5ce5d076a2..4d48bd4be2b 100644
--- a/app/assets/javascripts/pages/groups/clusters/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/index.js
@@ -1,7 +1,5 @@
import initIntegrationForm from '~/clusters/forms/show/index';
import initCreateCluster from '~/create_cluster/init_create_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initCreateCluster(document, gon);
- initIntegrationForm();
-});
+initCreateCluster(document, gon);
+initIntegrationForm();
diff --git a/app/assets/javascripts/pages/groups/clusters/new/index.js b/app/assets/javascripts/pages/groups/clusters/new/index.js
index 876bab0b339..de9ded87ef3 100644
--- a/app/assets/javascripts/pages/groups/clusters/new/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/new/index.js
@@ -1,5 +1,3 @@
import initNewCluster from '~/clusters/new_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initNewCluster();
-});
+initNewCluster();
diff --git a/app/assets/javascripts/pages/groups/clusters/show/index.js b/app/assets/javascripts/pages/groups/clusters/show/index.js
index ccf631b2c53..5d202a8824f 100644
--- a/app/assets/javascripts/pages/groups/clusters/show/index.js
+++ b/app/assets/javascripts/pages/groups/clusters/show/index.js
@@ -1,7 +1,5 @@
import ClustersBundle from '~/clusters/clusters_bundle';
import initClusterHealth from '~/pages/projects/clusters/show/cluster_health';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
- initClusterHealth();
-});
+new ClustersBundle(); // eslint-disable-line no-new
+initClusterHealth();
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 76db578f6f9..342c054471d 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,5 +1,5 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
+import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { mountIssuablesListApp } from '~/issues_list';
import initManualOrdering from '~/manual_ordering';
import { FILTERED_SEARCH } from '~/pages/constants';
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index 2f6f9bb16e1..02a0a50f984 100644
--- a/app/assets/javascripts/pages/groups/merge_requests/index.js
+++ b/app/assets/javascripts/pages/groups/merge_requests/index.js
@@ -1,6 +1,6 @@
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 issuableInitBulkUpdateSidebar from '~/issuable_init_bulk_update_sidebar';
+import issuableInitBulkUpdateSidebar from '~/issuable_bulk_update_sidebar/issuable_init_bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/pages/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
diff --git a/app/assets/javascripts/pages/groups/milestones/show/index.js b/app/assets/javascripts/pages/groups/milestones/show/index.js
index 2a2cc5faebe..914e2831185 100644
--- a/app/assets/javascripts/pages/groups/milestones/show/index.js
+++ b/app/assets/javascripts/pages/groups/milestones/show/index.js
@@ -1,7 +1,5 @@
import initDeleteMilestoneModal from '~/pages/milestones/shared/delete_milestone_modal_init';
import initMilestonesShow from '~/pages/milestones/shared/init_milestones_show';
-document.addEventListener('DOMContentLoaded', () => {
- initMilestonesShow();
- initDeleteMilestoneModal();
-});
+initMilestonesShow();
+initDeleteMilestoneModal();
diff --git a/app/assets/javascripts/pages/groups/new/group_path_validator.js b/app/assets/javascripts/pages/groups/new/group_path_validator.js
index a0ff98645fb..c58be202043 100644
--- a/app/assets/javascripts/pages/groups/new/group_path_validator.js
+++ b/app/assets/javascripts/pages/groups/new/group_path_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
import fetchGroupPathAvailability from './fetch_group_path_availability';
@@ -12,7 +12,6 @@ const parentIdSelector = 'group_parent_id';
const successMessageSelector = '.validation-success';
const pendingMessageSelector = '.validation-pending';
const unavailableMessageSelector = '.validation-error';
-const suggestionsMessageSelector = '.gl-path-suggestions';
const inputGroupSelector = '.input-group';
export default class GroupPathValidator extends InputValidator {
@@ -57,21 +56,19 @@ export default class GroupPathValidator extends InputValidator {
);
if (data.exists) {
- GroupPathValidator.showSuggestions(inputDomElement, data.suggests);
+ const [suggestedSlug] = data.suggests;
+ const targetDomElement = document.querySelector('.js-autofill-group-path');
+ targetDomElement.value = suggestedSlug;
}
})
- .catch(() => flash(__('An error occurred while validating group path')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while validating group path'),
+ }),
+ );
}
}
- static showSuggestions(inputDomElement, suggestions) {
- const messageElement = inputDomElement.parentElement.parentElement.querySelector(
- suggestionsMessageSelector,
- );
- const textSuggestions = suggestions && suggestions.length > 0 ? suggestions.join(', ') : 'none';
- messageElement.textContent = textSuggestions;
- }
-
static setMessageVisibility(inputDomElement, messageSelector, isVisible = true) {
const messageElement = inputDomElement
.closest(inputGroupSelector)
diff --git a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
index 16f68b94c9a..34f9fe778ea 100644
--- a/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
+++ b/app/assets/javascripts/pages/milestones/shared/components/delete_milestone_modal.vue
@@ -1,6 +1,6 @@
<script>
import { GlSafeHtmlDirective as SafeHtml, GlModal } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
@@ -98,17 +98,17 @@ Once deleted, it cannot be undone or recovered.`),
});
if (error.response && error.response.status === 404) {
- Flash(
- sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), {
+ createFlash({
+ message: sprintf(s__('Milestones|Milestone %{milestoneTitle} was not found'), {
milestoneTitle: this.milestoneTitle,
}),
- );
+ });
} else {
- Flash(
- sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), {
+ createFlash({
+ message: sprintf(s__('Milestones|Failed to delete milestone %{milestoneTitle}'), {
milestoneTitle: this.milestoneTitle,
}),
- );
+ });
}
throw error;
});
diff --git a/app/assets/javascripts/pages/profiles/notifications/show/index.js b/app/assets/javascripts/pages/profiles/notifications/show/index.js
index 51ba6c7a01e..6aa0f260cc0 100644
--- a/app/assets/javascripts/pages/profiles/notifications/show/index.js
+++ b/app/assets/javascripts/pages/profiles/notifications/show/index.js
@@ -1,5 +1,3 @@
import initNotificationsDropdown from '~/notifications';
-document.addEventListener('DOMContentLoaded', () => {
- initNotificationsDropdown();
-});
+initNotificationsDropdown();
diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
index 58ba6a500a3..60680ec7d1d 100644
--- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BuildArtifacts from '~/build_artifacts';
-document.addEventListener('DOMContentLoaded', () => {
- new ShortcutsNavigation(); // eslint-disable-line no-new
- new BuildArtifacts(); // eslint-disable-line no-new
-});
+new ShortcutsNavigation(); // eslint-disable-line no-new
+new BuildArtifacts(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js
index eb5ecc27c43..057ef157374 100644
--- a/app/assets/javascripts/pages/projects/artifacts/file/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js
@@ -1,7 +1,5 @@
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobViewer from '~/blob/viewer/index';
-document.addEventListener('DOMContentLoaded', () => {
- new ShortcutsNavigation(); // eslint-disable-line no-new
- new BlobViewer(); // eslint-disable-line no-new
-});
+new ShortcutsNavigation(); // eslint-disable-line no-new
+new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/clusters/destroy/index.js b/app/assets/javascripts/pages/projects/clusters/destroy/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/projects/clusters/destroy/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/destroy/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/clusters/edit/index.js b/app/assets/javascripts/pages/projects/clusters/edit/index.js
index 8001d2dd1da..487e7a14a16 100644
--- a/app/assets/javascripts/pages/projects/clusters/edit/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/edit/index.js
@@ -1,5 +1,3 @@
import ClustersBundle from '~/clusters/clusters_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
-});
+new ClustersBundle(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/clusters/index.js b/app/assets/javascripts/pages/projects/clusters/index.js
index 4d04c37caa7..f398b1cee82 100644
--- a/app/assets/javascripts/pages/projects/clusters/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/index.js
@@ -1,5 +1,3 @@
import initCreateCluster from '~/create_cluster/init_create_cluster';
-document.addEventListener('DOMContentLoaded', () => {
- initCreateCluster(document, gon);
-});
+initCreateCluster(document, gon);
diff --git a/app/assets/javascripts/pages/projects/clusters/show/index.js b/app/assets/javascripts/pages/projects/clusters/show/index.js
index 1d019285e23..71ab5a0b19c 100644
--- a/app/assets/javascripts/pages/projects/clusters/show/index.js
+++ b/app/assets/javascripts/pages/projects/clusters/show/index.js
@@ -3,9 +3,7 @@ import initIntegrationForm from '~/clusters/forms/show';
import initGkeNamespace from '~/create_cluster/gke_cluster_namespace';
import initClusterHealth from './cluster_health';
-document.addEventListener('DOMContentLoaded', () => {
- new ClustersBundle(); // eslint-disable-line no-new
- initGkeNamespace();
- initClusterHealth();
- initIntegrationForm();
-});
+new ClustersBundle(); // eslint-disable-line no-new
+initGkeNamespace();
+initClusterHealth();
+initIntegrationForm();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index d75c3cc6b8b..e3b30560fef 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -3,7 +3,7 @@ import $ from 'jquery';
import loadAwardsHandler from '~/awards_handler';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
-import flash from '~/flash';
+import createFlash from '~/flash';
import initChangesDropdown from '~/init_changes_dropdown';
import initNotes from '~/init_notes';
import axios from '~/lib/utils/axios_utils';
@@ -39,7 +39,7 @@ if (filesContainer.length) {
new Diff();
})
.catch(() => {
- flash({ message: __('An error occurred while retrieving diff files') });
+ createFlash({ message: __('An error occurred while retrieving diff files') });
});
} else {
new Diff();
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 75c3b6d564c..795ae713c08 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
@@ -39,6 +39,14 @@ const initFormField = ({ value, required = true, skipValidation = false }) => ({
feedback: null,
});
+function sortNamespaces(namespaces) {
+ if (!namespaces || !namespaces?.length) {
+ return namespaces;
+ }
+
+ return namespaces.sort((a, b) => a.full_name.localeCompare(b.full_name));
+}
+
export default {
components: {
GlForm,
@@ -206,7 +214,7 @@ export default {
methods: {
async fetchNamespaces() {
const { data } = await axios.get(this.endpoint);
- this.namespaces = data.namespaces;
+ this.namespaces = sortNamespaces(data.namespaces);
},
isVisibilityLevelDisabled(visibility) {
return !this.allowedVisibilityLevels.includes(visibility);
@@ -301,11 +309,11 @@ export default {
:state="form.fields.namespace.state"
required
>
- <template slot="first">
+ <template #first>
<option :value="null" disabled>{{ s__('ForkProject|Select a namespace') }}</option>
</template>
<option v-for="namespace in namespaces" :key="namespace.id" :value="namespace">
- {{ namespace.name }}
+ {{ namespace.full_name }}
</option>
</gl-form-select>
</gl-form-input-group>
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
index 88f4bba5e2a..d41488acf46 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_groups_list_item.vue
@@ -101,7 +101,7 @@ export default {
v-if="isGroupPendingRemoval"
variant="warning"
class="gl-display-none gl-sm-display-flex gl-mt-3 gl-mr-1"
- >{{ __('pending removal') }}</gl-badge
+ >{{ __('pending deletion') }}</gl-badge
>
<user-access-role-badge v-if="group.permission" class="gl-mt-3">
{{ group.permission }}
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 1eab3becbc3..8ec6e5e66b3 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,9 @@
import ShortcutsNavigation from '../../behaviors/shortcuts/shortcuts_navigation';
+import initTerraformNotification from '../../projects/terraform_notification';
import { initSidebarTracking } from '../shared/nav/sidebar_tracking';
import Project from './project';
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
initSidebarTracking();
+initTerraformNotification();
diff --git a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
index aaa9bb906b2..e708cd32fff 100644
--- a/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
+++ b/app/assets/javascripts/pages/projects/labels/components/promote_label_modal.vue
@@ -85,28 +85,29 @@ export default {
:action-cancel="$options.cancelProps"
@primary="onSubmit"
>
- <div slot="modal-title" class="modal-title-with-label">
- <gl-sprintf
- :message="
- s__(
- 'Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}',
- )
- "
- >
- <template #labelTitle>
- <span
- class="label color-label"
- :style="`background-color: ${labelColor}; color: ${labelTextColor};`"
- >
- {{ labelTitle }}
- </span>
- </template>
- <template #span="{ content }"
- ><span>{{ content }}</span></template
+ <template #modal-title>
+ <div class="modal-title-with-label">
+ <gl-sprintf
+ :message="
+ s__(
+ 'Labels|%{spanStart}Promote label%{spanEnd} %{labelTitle} %{spanStart}to Group Label?%{spanEnd}',
+ )
+ "
>
- </gl-sprintf>
- </div>
-
+ <template #labelTitle>
+ <span
+ class="label color-label"
+ :style="`background-color: ${labelColor}; color: ${labelTextColor};`"
+ >
+ {{ labelTitle }}
+ </span>
+ </template>
+ <template #span="{ content }"
+ ><span>{{ content }}</span></template
+ >
+ </gl-sprintf>
+ </div>
+ </template>
{{ text }}
</gl-modal>
</template>
diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
index 05019915fc9..545a39f4cf1 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
@@ -1,7 +1,5 @@
import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-document.addEventListener('DOMContentLoaded', () => {
- initSidebarBundle();
- initMergeConflicts();
-});
+initSidebarBundle();
+initMergeConflicts();
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 8d152ec4ba6..d61209f904d 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
@@ -15,7 +15,7 @@ const updateCommitList = (url, $loadingIndicator, $commitList, params) => {
.then(({ data }) => {
$loadingIndicator.hide();
$commitList.html(data);
- localTimeAgo($('.js-timeago', $commitList));
+ localTimeAgo($commitList.get(0).querySelectorAll('.js-timeago'));
});
};
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
index 68ab7021cf3..e5f97530c02 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/compare_autocomplete.js
@@ -2,7 +2,7 @@
import $ from 'jquery';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { __ } from '~/locale';
@@ -37,7 +37,11 @@ export default function initCompareAutocomplete(limitTo = null, clickHandler = (
callback(data);
}
})
- .catch(() => flash(__('Error fetching refs')));
+ .catch(() =>
+ createFlash({
+ message: __('Error fetching refs'),
+ }),
+ );
},
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/pages/projects/new/components/app.vue b/app/assets/javascripts/pages/projects/new/components/app.vue
index 60a4fbc3e6b..6e9efc50be8 100644
--- a/app/assets/javascripts/pages/projects/new/components/app.vue
+++ b/app/assets/javascripts/pages/projects/new/components/app.vue
@@ -4,12 +4,10 @@ import blankProjectIllustration from '@gitlab/svgs/dist/illustrations/project-cr
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 { experiment } from '~/experimentation/utils';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
import NewProjectPushTipPopover from './new_project_push_tip_popover.vue';
-const NEW_REPO_EXPERIMENT = 'new_repo';
const CI_CD_PANEL = 'cicd_for_external_repo';
const PANELS = [
{
@@ -79,28 +77,8 @@ export default {
},
computed: {
- decoratedPanels() {
- const PANEL_TITLES = experiment(NEW_REPO_EXPERIMENT, {
- use: () => ({
- blank: s__('ProjectsNew|Create blank project'),
- import: s__('ProjectsNew|Import project'),
- }),
- try: () => ({
- blank: s__('ProjectsNew|Create blank project/repository'),
- import: s__('ProjectsNew|Import project/repository'),
- }),
- });
-
- return PANELS.map(({ key, title, ...el }) => ({
- ...el,
- title: PANEL_TITLES[key] ?? title,
- }));
- },
-
availablePanels() {
- return this.isCiCdAvailable
- ? this.decoratedPanels
- : this.decoratedPanels.filter((p) => p.name !== CI_CD_PANEL);
+ return this.isCiCdAvailable ? PANELS : PANELS.filter((p) => p.name !== CI_CD_PANEL);
},
},
@@ -112,7 +90,6 @@ export default {
}
},
},
- EXPERIMENT: NEW_REPO_EXPERIMENT,
};
</script>
@@ -122,7 +99,6 @@ export default {
:panels="availablePanels"
:jump-to-last-persisted-panel="hasErrors"
:title="s__('ProjectsNew|Create new project')"
- :experiment="$options.EXPERIMENT"
persistence-key="new_project_last_active_tab"
@panel-change="resetProjectErrors"
>
diff --git a/app/assets/javascripts/pages/projects/packages/packages/show/index.js b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
index 1afb900ed88..ee06f247ddc 100644
--- a/app/assets/javascripts/pages/projects/packages/packages/show/index.js
+++ b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
@@ -1,3 +1,11 @@
-import initPackageDetail from '~/packages/details/';
-
-initPackageDetail();
+(async function initPackage() {
+ let app;
+ if (document.getElementById('js-vue-packages-detail-new')) {
+ app = await import(
+ /* webpackChunkName: 'new_package_app' */ `~/packages_and_registries/package_registry/pages/details.js`
+ );
+ } else {
+ app = await import('~/packages/details/');
+ }
+ app.default();
+})();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js
index d65be6bc69e..6dd21380bec 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/create/index.js
@@ -1,3 +1,3 @@
import initForm from '../shared/init_form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
index d65be6bc69e..6dd21380bec 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/edit/index.js
@@ -1,3 +1,3 @@
import initForm from '../shared/init_form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
index d65be6bc69e..6dd21380bec 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/new/index.js
@@ -1,3 +1,3 @@
import initForm from '../shared/init_form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js
index d65be6bc69e..6dd21380bec 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/update/index.js
@@ -1,3 +1,3 @@
import initForm from '../shared/init_form';
-document.addEventListener('DOMContentLoaded', initForm);
+initForm();
diff --git a/app/assets/javascripts/pages/projects/project.js b/app/assets/javascripts/pages/projects/project.js
index 3b24c2c128b..9e93f709937 100644
--- a/app/assets/javascripts/pages/projects/project.js
+++ b/app/assets/javascripts/pages/projects/project.js
@@ -4,7 +4,7 @@ import $ from 'jquery';
import Cookies from 'js-cookie';
import initClonePanel from '~/clone_panel';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { serializeForm } from '~/lib/utils/forms';
import { mergeUrlParams } from '~/lib/utils/url_utility';
@@ -78,7 +78,11 @@ export default class Project {
},
})
.then(({ data }) => callback(data))
- .catch(() => flash(__('An error occurred while getting projects')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while getting projects'),
+ }),
+ );
},
selectable: true,
filterable: true,
diff --git a/app/assets/javascripts/pages/projects/security/configuration/index.js b/app/assets/javascripts/pages/projects/security/configuration/index.js
index 101cb8356b2..8bba3d7af54 100644
--- a/app/assets/javascripts/pages/projects/security/configuration/index.js
+++ b/app/assets/javascripts/pages/projects/security/configuration/index.js
@@ -1,3 +1,3 @@
-import { initStaticSecurityConfiguration } from '~/security_configuration';
+import { initCESecurityConfiguration } from '~/security_configuration';
-initStaticSecurityConfiguration(document.querySelector('#js-security-configuration-static'));
+initCESecurityConfiguration(document.querySelector('#js-security-configuration-static'));
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index db7b3bad6ed..e88dbf20e1b 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -8,6 +8,7 @@ import { initRunnerAwsDeployments } from '~/pages/shared/mount_runner_aws_deploy
import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import initSettingsPanels from '~/settings_panels';
+import { initTokenAccess } from '~/token_access';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
@@ -40,4 +41,5 @@ document.addEventListener('DOMContentLoaded', () => {
initSharedRunnersToggle();
initInstallRunner();
initRunnerAwsDeployments();
+ initTokenAccess();
});
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 11e6b4577e0..6fcaa3ab04b 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
@@ -104,6 +104,11 @@ export default {
required: false,
default: '',
},
+ issuesHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
lfsHelpPath: {
type: String,
required: false,
@@ -438,8 +443,13 @@ export default {
>
<project-setting-row
ref="issues-settings"
+ :help-path="issuesHelpPath"
:label="$options.i18n.issuesLabel"
- :help-text="s__('ProjectSettings|Lightweight issue tracking system.')"
+ :help-text="
+ s__(
+ 'ProjectSettings|Flexible tool to collaboratively develop ideas and plan work in this project.',
+ )
+ "
>
<project-feature-setting
v-model="issuesAccessLevel"
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index 4104025aa59..ae605edeaf0 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -2,8 +2,6 @@ import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
-document.addEventListener('DOMContentLoaded', () => {
- new UsernameValidator(); // eslint-disable-line no-new
- new LengthValidator(); // eslint-disable-line no-new
- new NoEmojiValidator(); // eslint-disable-line no-new
-});
+new UsernameValidator(); // eslint-disable-line no-new
+new LengthValidator(); // eslint-disable-line no-new
+new NoEmojiValidator(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index d39f56cfd03..465aed88c01 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import initVueAlerts from '~/vue_alerts';
import NoEmojiValidator from '../../../emoji/no_emoji_validator';
import LengthValidator from './length_validator';
import OAuthRememberMe from './oauth_remember_me';
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
// Save the URL fragment from the current window location. This will be present if the user was
// redirected to sign-in after attempting to access a protected URL that included a fragment.
preserveUrlFragment(window.location.hash);
+ initVueAlerts();
});
diff --git a/app/assets/javascripts/pages/sessions/new/username_validator.js b/app/assets/javascripts/pages/sessions/new/username_validator.js
index 338fe1b66f2..7ea744a68a6 100644
--- a/app/assets/javascripts/pages/sessions/new/username_validator.js
+++ b/app/assets/javascripts/pages/sessions/new/username_validator.js
@@ -1,6 +1,6 @@
import { debounce } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import InputValidator from '~/validators/input_validator';
@@ -50,7 +50,11 @@ export default class UsernameValidator extends InputValidator {
usernameTaken ? unavailableMessageSelector : successMessageSelector,
);
})
- .catch(() => flash(__('An error occurred while validating username')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while validating username'),
+ }),
+ );
}
}
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
index 26f6d1d683a..e883fecb170 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -212,13 +212,20 @@ export default {
.then(({ data }) => data.body);
},
- handleFormSubmit() {
+ async handleFormSubmit(e) {
+ e.preventDefault();
+
if (this.useContentEditor) {
this.content = this.contentEditor.getSerializedContent();
this.trackFormSubmit();
}
+ // Wait until form field values are refreshed
+ await this.$nextTick();
+
+ e.target.submit();
+
this.isDirty = false;
},
@@ -257,6 +264,7 @@ export default {
this.contentEditor ||
createContentEditor({
renderMarkdown: (markdown) => this.getContentHTML(markdown),
+ uploadsPath: this.pageInfo.uploadsPath,
tiptapOptions: {
onUpdate: () => this.handleContentChange(),
},
@@ -454,7 +462,7 @@ export default {
</markdown-field>
<div v-if="isContentEditorActive">
- <gl-alert class="gl-mb-6" variant="tip" :dismissable="false">
+ <gl-alert class="gl-mb-6" variant="tip" :dismissible="false">
<gl-sprintf :message="$options.i18n.contentEditor.feedbackTip">
<template
#link="// eslint-disable-next-line vue/no-template-shadow
@@ -468,7 +476,11 @@ export default {
>
</gl-sprintf>
</gl-alert>
- <gl-loading-icon v-if="isContentEditorLoading" class="bordered-box gl-w-full gl-py-6" />
+ <gl-loading-icon
+ v-if="isContentEditorLoading"
+ size="sm"
+ class="bordered-box gl-w-full gl-py-6"
+ />
<content-editor v-else :content-editor="contentEditor" />
<input id="wiki_content" v-model.trim="content" type="hidden" name="wiki[content]" />
</div>
diff --git a/app/assets/javascripts/pages/shared/wikis/index.js b/app/assets/javascripts/pages/shared/wikis/index.js
index c04cd0b3fa4..42aefe81325 100644
--- a/app/assets/javascripts/pages/shared/wikis/index.js
+++ b/app/assets/javascripts/pages/shared/wikis/index.js
@@ -27,8 +27,10 @@ const createModalVueApp = () => {
// eslint-disable-next-line no-new
new Vue({
el: deleteWikiModalWrapperEl,
- data: {
- deleteWikiUrl: '',
+ data() {
+ return {
+ deleteWikiUrl: '',
+ };
},
render(createElement) {
return createElement(deleteWikiModal, {
diff --git a/app/assets/javascripts/pages/users/activity_calendar.js b/app/assets/javascripts/pages/users/activity_calendar.js
index 03dba699461..0fab4678bc3 100644
--- a/app/assets/javascripts/pages/users/activity_calendar.js
+++ b/app/assets/javascripts/pages/users/activity_calendar.js
@@ -2,7 +2,7 @@ import { select } from 'd3-selection';
import dateFormat from 'dateformat';
import $ from 'jquery';
import { last } from 'lodash';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { getDayName, getDayDifference } from '~/lib/utils/datetime_utility';
import { n__, s__, __ } from '~/locale';
@@ -295,7 +295,11 @@ export default class ActivityCalendar {
responseType: 'text',
})
.then(({ data }) => $(this.activitiesContainer).html(data))
- .catch(() => flash(__('An error occurred while retrieving calendar activity')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while retrieving calendar activity'),
+ }),
+ );
} else {
this.currentSelectedDate = '';
$(this.activitiesContainer).html('');
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index f9d70845560..90eafa85886 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -166,7 +166,7 @@ export default class UserTabs {
const tabSelector = `div#${action}`;
this.$parentEl.find(tabSelector).html(data.html);
this.loaded[action] = true;
- localTimeAgo($('.js-timeago', tabSelector));
+ localTimeAgo(document.querySelectorAll(`${tabSelector} .js-timeago`));
this.toggleLoading(false);
})
@@ -209,7 +209,7 @@ export default class UserTabs {
container,
url: $(`${container} .overview-content-list`).data('href'),
...options,
- postRenderCallback: () => localTimeAgo($('.js-timeago', container)),
+ postRenderCallback: () => localTimeAgo(document.querySelectorAll(`${container} .js-timeago`)),
});
}
diff --git a/app/assets/javascripts/performance/constants.js b/app/assets/javascripts/performance/constants.js
index 1db80057d0c..b9a9ef215af 100644
--- a/app/assets/javascripts/performance/constants.js
+++ b/app/assets/javascripts/performance/constants.js
@@ -83,7 +83,9 @@ export const PIPELINES_DETAIL_LINKS_JOB_RATIO = 'pipeline_graph_links_per_job_ra
// Marks
export const REPO_BLOB_LOAD_VIEWER_START = 'blobviewer-load-viewer-start';
+export const REPO_BLOB_SWITCH_TO_VIEWER_START = 'blobviewer-switch-to-viewerr-start';
export const REPO_BLOB_LOAD_VIEWER_FINISH = 'blobviewer-load-viewer-finish';
// Measures
-export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the content';
+export const REPO_BLOB_LOAD_VIEWER = 'Repository File Viewer: loading the viewer';
+export const REPO_BLOB_SWITCH_VIEWER = 'Repository File Viewer: switching the viewer';
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 04efc459a21..f163a7c3a8e 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -27,9 +27,7 @@ export default {
title: {
type: String,
required: false,
- default() {
- return this.metric;
- },
+ default: null,
},
header: {
type: String,
@@ -101,6 +99,9 @@ export default {
return '';
},
+ actualTitle() {
+ return this.title ?? this.metric;
+ },
},
methods: {
toggleBacktrace(toggledIndex) {
@@ -214,7 +215,7 @@ export default {
<div></div>
</template>
</gl-modal>
- {{ title }}
+ {{ actualTitle }}
<request-warning :html-id="htmlId" :warnings="warnings" />
</div>
</template>
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index e845c8b9df4..bc83844b8b9 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as Flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -62,7 +62,11 @@ export default class PersistentUserCallout {
}
})
.catch(() => {
- Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
+ createFlash({
+ message: __(
+ 'An error occurred while dismissing the alert. Refresh the page and try again.',
+ ),
+ });
});
}
@@ -79,11 +83,11 @@ export default class PersistentUserCallout {
window.location.assign(href);
})
.catch(() => {
- Flash(
- __(
+ createFlash({
+ message: __(
'An error occurred while acknowledging the notification. Refresh the page and try again.',
),
- );
+ });
});
}
diff --git a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
index f6e88738002..f1fe8cf10fd 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -103,6 +103,7 @@ export default {
v-model="targetBranch"
class="gl-font-monospace!"
required
+ data-qa-selector="target_branch_field"
/>
<gl-form-checkbox
v-if="!isCurrentBranchTarget"
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
index 455990f2791..853e839a7ab 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_config_merged_preview.vue
@@ -2,14 +2,14 @@
import { GlIcon } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { s__ } from '~/locale';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
i18n: {
viewOnlyMessage: s__('Pipelines|Merged YAML is view only'),
},
components: {
- EditorLite,
+ SourceEditor,
GlIcon,
},
inject: ['ciConfigPath'],
@@ -41,7 +41,7 @@ export default {
{{ $options.i18n.viewOnlyMessage }}
</div>
<div class="gl-mt-3 gl-border-solid gl-border-gray-100 gl-border-1">
- <editor-lite
+ <source-editor
ref="editor"
:value="mergedYaml"
:file-name="ciConfigPath"
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
new file mode 100644
index 00000000000..b4e9ab81d38
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/components/editor/ci_editor_header.vue
@@ -0,0 +1,38 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import Tracking from '~/tracking';
+import { pipelineEditorTrackingOptions, TEMPLATE_REPOSITORY_URL } from '../../constants';
+
+export default {
+ i18n: {
+ browseTemplates: __('Browse templates'),
+ },
+ TEMPLATE_REPOSITORY_URL,
+ components: {
+ GlButton,
+ },
+ mixins: [Tracking.mixin()],
+ methods: {
+ trackTemplateBrowsing() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+
+ this.track(actions.browse_templates, { label });
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-bg-gray-10 gl-p-3 gl-border-solid gl-border-gray-100 gl-border-1">
+ <gl-button
+ :href="$options.TEMPLATE_REPOSITORY_URL"
+ size="small"
+ icon="external-link"
+ target="_blank"
+ @click="trackTemplateBrowsing"
+ >
+ {{ $options.i18n.browseTemplates }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
index d373f74a5c4..77ede396496 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -1,13 +1,13 @@
<script>
import { EDITOR_READY_EVENT } from '~/editor/constants';
-import { CiSchemaExtension } from '~/editor/extensions/editor_ci_schema_ext';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import { CiSchemaExtension } from '~/editor/extensions/source_editor_ci_schema_ext';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import getCommitSha from '../../graphql/queries/client/commit_sha.graphql';
export default {
components: {
- EditorLite,
+ SourceEditor,
},
mixins: [glFeatureFlagMixin()],
inject: ['ciConfigPath', 'projectPath', 'projectNamespace', 'defaultBranch'],
@@ -43,8 +43,8 @@ export default {
};
</script>
<template>
- <div class="gl-border-solid gl-border-gray-100 gl-border-1">
- <editor-lite
+ <div class="gl-border-solid gl-border-gray-100 gl-border-1 gl-border-t-none!">
+ <source-editor
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
diff --git a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
index 05b87abecd5..ee6d4ff7c4d 100644
--- a/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -158,6 +158,12 @@ export default {
const updatedPath = setUrlParams({ branch_name: newBranch });
historyPushState(updatedPath);
+ this.$emit('updateCommitSha', { newBranch });
+
+ // refetching the content will cause a lot of components to re-render,
+ // including the text editor which uses the commit sha to register the CI schema
+ // so we need to make sure the commit sha is updated first
+ await this.$nextTick();
this.$emit('refetchContent');
},
async setSearchTerm(newSearchTerm) {
@@ -205,6 +211,7 @@ export default {
:header-text="$options.i18n.dropdownHeader"
:text="currentBranch"
icon="branch"
+ data-qa-selector="branch_selector_button"
>
<gl-search-box-by-type :debounce="$options.inputDebounce" @input="setSearchTerm" />
<gl-dropdown-section-header>
@@ -222,6 +229,7 @@ export default {
:key="branch"
:is-checked="currentBranch === branch"
:is-check-item="true"
+ data-qa-selector="menu_branch_button"
@click="selectBranch(branch)"
>
{{ branch }}
diff --git a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
index 368a026bdaa..6af3361e7e6 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -66,6 +66,7 @@ export default {
},
data() {
return {
+ commitSha: '',
hasError: false,
};
},
diff --git a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
index d1534655a00..8bffd893473 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -87,7 +87,7 @@ export default {
<template>
<div>
<template v-if="isLoading">
- <gl-loading-icon inline />
+ <gl-loading-icon size="sm" inline />
{{ $options.i18n.loading }}
</template>
diff --git a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
index c3dcc00af6e..e463fcf379d 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -16,6 +16,7 @@ import {
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import CiEditorHeader from './editor/ci_editor_header.vue';
import TextEditor from './editor/text_editor.vue';
import CiLint from './lint/ci_lint.vue';
import EditorTab from './ui/editor_tab.vue';
@@ -49,6 +50,7 @@ export default {
},
components: {
CiConfigMergedPreview,
+ CiEditorHeader,
CiLint,
EditorTab,
GlAlert,
@@ -107,6 +109,7 @@ export default {
data-testid="editor-tab"
@click="setCurrentTab($options.tabConstants.CREATE_TAB)"
>
+ <ci-editor-header />
<text-editor :value="ciFileContent" v-on="$listeners" />
</editor-tab>
<editor-tab
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index 1467abd7289..d05b06d16db 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -33,3 +33,13 @@ export const BRANCH_PAGINATION_LIMIT = 20;
export const BRANCH_SEARCH_DEBOUNCE = '500';
export const STARTER_TEMPLATE_NAME = 'Getting-Started';
+
+export const pipelineEditorTrackingOptions = {
+ label: 'pipeline_editor',
+ actions: {
+ browse_templates: 'browse_templates',
+ },
+};
+
+export const TEMPLATE_REPOSITORY_URL =
+ 'https://gitlab.com/gitlab-org/gitlab-foss/tree/master/lib/gitlab/ci/templates';
diff --git a/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
new file mode 100644
index 00000000000..dce17cad808
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/mutations/update_commit_sha.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateCommitSha($commitSha: String) {
+ updateCommitSha(commitSha: $commitSha) @client
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql
index 9f1b5b13088..5500244b430 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/blob_content.graphql
@@ -1,5 +1,11 @@
-query getBlobContent($projectPath: ID!, $path: String, $ref: String!) {
- blobContent(projectPath: $projectPath, path: $path, ref: $ref) @client {
- rawData
+query getBlobContent($projectPath: ID!, $path: String!, $ref: String) {
+ project(fullPath: $projectPath) {
+ repository {
+ blobs(paths: [$path], ref: $ref) {
+ nodes {
+ rawBlob
+ }
+ }
+ }
}
}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
index 30c18a96536..df7de6a1f54 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/ci_config.graphql
@@ -1,7 +1,7 @@
#import "~/pipelines/graphql/fragments/pipeline_stages_connection.fragment.graphql"
-query getCiConfigData($projectPath: ID!, $content: String!) {
- ciConfig(projectPath: $projectPath, content: $content) {
+query getCiConfigData($projectPath: ID!, $sha: String, $content: String!) {
+ ciConfig(projectPath: $projectPath, sha: $sha, content: $content) {
errors
mergedYaml
status
diff --git a/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
new file mode 100644
index 00000000000..219c23bb22b
--- /dev/null
+++ b/app/assets/javascripts/pipeline_editor/graphql/queries/latest_commit_sha.query.graphql
@@ -0,0 +1,12 @@
+query getLatestCommitSha($projectPath: ID!, $ref: String) {
+ project(fullPath: $projectPath) {
+ pipelines(ref: $ref) {
+ nodes {
+ id
+ sha
+ path
+ commitPath
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
index 8cead7f3315..2bec2006e95 100644
--- a/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
+++ b/app/assets/javascripts/pipeline_editor/graphql/resolvers.js
@@ -1,20 +1,10 @@
import produce from 'immer';
-import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import getCommitShaQuery from './queries/client/commit_sha.graphql';
import getCurrentBranchQuery from './queries/client/current_branch.graphql';
import getLastCommitBranchQuery from './queries/client/last_commit_branch.query.graphql';
export const resolvers = {
- Query: {
- blobContent(_, { projectPath, path, ref }) {
- return {
- __typename: 'BlobContent',
- rawData: Api.getRawFile(projectPath, path, { ref }).then(({ data }) => {
- return data;
- }),
- };
- },
- },
Mutation: {
lintCI: (_, { endpoint, content, dry_run }) => {
return axios.post(endpoint, { content, dry_run }).then(({ data }) => ({
@@ -42,7 +32,15 @@ export const resolvers = {
__typename: 'CiLintContent',
}));
},
- updateCurrentBranch: (_, { currentBranch = undefined }, { cache }) => {
+ updateCommitSha: (_, { commitSha }, { cache }) => {
+ cache.writeQuery({
+ query: getCommitShaQuery,
+ data: produce(cache.readQuery({ query: getCommitShaQuery }), (draftData) => {
+ draftData.commitSha = commitSha;
+ }),
+ });
+ },
+ updateCurrentBranch: (_, { currentBranch }, { cache }) => {
cache.writeQuery({
query: getCurrentBranchQuery,
data: produce(cache.readQuery({ query: getCurrentBranchQuery }), (draftData) => {
@@ -50,7 +48,7 @@ export const resolvers = {
}),
});
},
- updateLastCommitBranch: (_, { lastCommitBranch = undefined }, { cache }) => {
+ updateLastCommitBranch: (_, { lastCommitBranch }, { cache }) => {
cache.writeQuery({
query: getLastCommitBranchQuery,
data: produce(cache.readQuery({ query: getLastCommitBranchQuery }), (draftData) => {
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index c24e6523352..0e8a6805a59 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import { fetchPolicies } from '~/lib/graphql';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
import { unwrapStagesWithNeeds } from '~/pipelines/components/unwrapping_utils';
@@ -16,12 +16,15 @@ import {
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
+import updateCommitShaMutation from './graphql/mutations/update_commit_sha.mutation.graphql';
import getBlobContent from './graphql/queries/blob_content.graphql';
import getCiConfigData from './graphql/queries/ci_config.graphql';
import getAppStatus from './graphql/queries/client/app_status.graphql';
+import getCommitSha from './graphql/queries/client/commit_sha.graphql';
import getCurrentBranch from './graphql/queries/client/current_branch.graphql';
import getIsNewCiConfigFile from './graphql/queries/client/is_new_ci_config_file.graphql';
import getTemplate from './graphql/queries/get_starter_template.query.graphql';
+import getLatestCommitShaQuery from './graphql/queries/latest_commit_sha.query.graphql';
import PipelineEditorHome from './pipeline_editor_home.vue';
export default {
@@ -42,6 +45,7 @@ export default {
},
data() {
return {
+ starterTemplateName: STARTER_TEMPLATE_NAME,
ciConfigData: {},
failureType: null,
failureReasons: [],
@@ -76,22 +80,40 @@ export default {
};
},
update(data) {
- return data?.blobContent?.rawData;
+ return data?.project?.repository?.blobs?.nodes[0]?.rawBlob;
},
result({ data }) {
- const fileContent = data?.blobContent?.rawData ?? '';
+ const nodes = data?.project?.repository?.blobs?.nodes;
+ if (!nodes) {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
+ } else {
+ const rawBlob = nodes[0]?.rawBlob;
+ const fileContent = rawBlob ?? '';
- this.lastCommittedContent = fileContent;
- this.currentCiFileContent = fileContent;
+ this.lastCommittedContent = fileContent;
+ this.currentCiFileContent = fileContent;
- // make sure to reset the start screen flag during a refetch
- // e.g. when switching branches
- if (fileContent.length) {
- this.showStartScreen = false;
+ // If rawBlob is defined and returns a string, it means that there is
+ // a CI config file with empty content. If `rawBlob` is not defined
+ // at all, it means there was no file found.
+ const hasCIFile = rawBlob === '' || fileContent.length > 0;
+
+ if (!fileContent.length) {
+ this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
+ }
+
+ if (!hasCIFile) {
+ this.showStartScreen = true;
+ } else if (fileContent.length) {
+ // If the file content is > 0, then we make sure to reset the
+ // start screen flag during a refetch
+ // e.g. when switching branches
+ this.showStartScreen = false;
+ }
}
},
- error(error) {
- this.handleBlobContentError(error);
+ error() {
+ this.reportFailure(LOAD_FAILURE_UNKNOWN);
},
watchLoading(isLoading) {
if (isLoading) {
@@ -107,6 +129,7 @@ export default {
variables() {
return {
projectPath: this.projectFullPath,
+ sha: this.commitSha,
content: this.currentCiFileContent,
};
},
@@ -132,6 +155,9 @@ export default {
appStatus: {
query: getAppStatus,
},
+ commitSha: {
+ query: getCommitSha,
+ },
currentBranch: {
query: getCurrentBranch,
},
@@ -143,7 +169,7 @@ export default {
variables() {
return {
projectPath: this.projectFullPath,
- templateName: STARTER_TEMPLATE_NAME,
+ templateName: this.starterTemplateName,
};
},
skip({ isNewCiConfigFile }) {
@@ -186,23 +212,10 @@ export default {
}
},
},
+ mounted() {
+ this.loadTemplateFromURL();
+ },
methods: {
- handleBlobContentError(error = {}) {
- const { networkError } = error;
-
- const { response } = networkError;
- // 404 for missing CI file
- // 400 for blank projects with no repository
- if (
- response?.status === httpStatusCodes.NOT_FOUND ||
- response?.status === httpStatusCodes.BAD_REQUEST
- ) {
- this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
- this.showStartScreen = true;
- } else {
- this.reportFailure(LOAD_FAILURE_UNKNOWN);
- }
- },
hideFailure() {
this.showFailure = false;
},
@@ -244,6 +257,38 @@ export default {
updateCiConfig(ciFileContent) {
this.currentCiFileContent = ciFileContent;
},
+ async updateCommitSha({ newBranch }) {
+ let fetchResults;
+
+ try {
+ fetchResults = await this.$apollo.query({
+ query: getLatestCommitShaQuery,
+ variables: {
+ projectPath: this.projectFullPath,
+ ref: newBranch,
+ },
+ });
+ } catch {
+ this.showFetchError();
+ return;
+ }
+
+ if (fetchResults.errors?.length > 0) {
+ this.showFetchError();
+ return;
+ }
+
+ const pipelineNodes = fetchResults?.data?.project?.pipelines?.nodes ?? [];
+ if (pipelineNodes.length === 0) {
+ return;
+ }
+
+ const commitSha = pipelineNodes[0].sha;
+ this.$apollo.mutate({
+ mutation: updateCommitShaMutation,
+ variables: { commitSha },
+ });
+ },
updateOnCommit({ type }) {
this.reportSuccess(type);
@@ -257,6 +302,14 @@ export default {
// if the user has made changes to the file that are unsaved.
this.lastCommittedContent = this.currentCiFileContent;
},
+ loadTemplateFromURL() {
+ const templateName = queryToObject(window.location.search)?.template;
+
+ if (templateName) {
+ this.starterTemplateName = templateName;
+ this.setNewEmptyCiConfigFile();
+ }
+ },
},
};
</script>
@@ -288,6 +341,7 @@ export default {
@showError="showErrorAlert"
@refetchContent="refetchContent"
@updateCiConfig="updateCiConfig"
+ @updateCommitSha="updateCommitSha"
/>
<confirm-unsaved-changes-dialog :has-unsaved-changes="hasUnsavedChanges" />
</div>
diff --git a/app/assets/javascripts/pipeline_new/constants.js b/app/assets/javascripts/pipeline_new/constants.js
index 91a064a0fb8..a6c9f3cb746 100644
--- a/app/assets/javascripts/pipeline_new/constants.js
+++ b/app/assets/javascripts/pipeline_new/constants.js
@@ -1,6 +1,8 @@
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
export const VARIABLE_TYPE = 'env_var';
export const FILE_TYPE = 'file';
-export const DEBOUNCE_REFS_SEARCH_MS = 250;
+export const DEBOUNCE_REFS_SEARCH_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const CONFIG_VARIABLES_TIMEOUT = 5000;
export const BRANCH_REF_TYPE = 'branch';
export const TAG_REF_TYPE = 'tag';
diff --git a/app/assets/javascripts/pipelines/components/graph/graph_component.vue b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
index 71ec81b8969..ea45b5e3ec7 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component.vue
@@ -101,9 +101,6 @@ export default {
showJobLinks() {
return !this.isStageView && this.showLinks;
},
- shouldShowStageName() {
- return !this.isStageView;
- },
// The show downstream check prevents showing redundant linked columns
showDownstreamPipelines() {
return (
@@ -165,8 +162,10 @@ export default {
<div class="js-pipeline-graph">
<div
ref="mainPipelineContainer"
- class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap gl-border-t-solid gl-border-t-1 gl-border-gray-100"
- :class="{ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto': !isLinkedPipeline }"
+ class="gl-display-flex gl-position-relative gl-bg-gray-10 gl-white-space-nowrap"
+ :class="{
+ 'gl-pipeline-min-h gl-py-5 gl-overflow-auto gl-border-t-solid gl-border-t-1 gl-border-gray-100': !isLinkedPipeline,
+ }"
>
<linked-graph-wrapper>
<template #upstream>
@@ -202,11 +201,12 @@ export default {
:groups="column.groups"
:action="column.status.action"
:highlighted-jobs="highlightedJobs"
- :show-stage-name="shouldShowStageName"
+ :is-stage-view="isStageView"
:job-hovered="hoveredJobName"
:source-job-hovered="hoveredSourceJobName"
:pipeline-expanded="pipelineExpanded"
:pipeline-id="pipeline.id"
+ :user-permissions="pipeline.userPermissions"
@refreshPipelineGraph="$emit('refreshPipelineGraph')"
@jobHover="setJob"
@updateMeasurements="getMeasurements"
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 fb45738f8d1..a948a57c144 100644
--- a/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
+++ b/app/assets/javascripts/pipelines/components/graph/graph_component_wrapper.vue
@@ -105,7 +105,7 @@ export default {
return this.pipeline;
}
- return unwrapPipelineData(this.pipelineProjectPath, data);
+ return unwrapPipelineData(this.pipelineProjectPath, JSON.parse(JSON.stringify(data)));
},
error(err) {
this.reportFailure({ type: LOAD_FAILURE, skipSentry: true });
@@ -114,7 +114,7 @@ export default {
this.$options.name,
`| type: ${LOAD_FAILURE} , info: ${serializeLoadErrors(err)}`,
{
- projectPath: this.projectPath,
+ 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/linked_pipeline.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
index b3c5af5418f..dd8a354511a 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue
@@ -161,7 +161,7 @@ export default {
:size="24"
css-classes="gl-top-0 gl-pr-2"
/>
- <div v-else class="gl-pr-2"><gl-loading-icon inline /></div>
+ <div v-else class="gl-pr-2"><gl-loading-icon size="sm" inline /></div>
<div class="gl-display-flex gl-flex-direction-column gl-w-13">
<span class="gl-text-truncate" data-testid="downstream-title">
{{ downstreamTitle }}
diff --git a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
index 45113ecff41..52ee40bd982 100644
--- a/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
+++ b/app/assets/javascripts/pipelines/components/graph/linked_pipelines_column.vue
@@ -118,7 +118,7 @@ export default {
return this.currentPipeline;
}
- return unwrapPipelineData(projectPath, data);
+ return unwrapPipelineData(projectPath, JSON.parse(JSON.stringify(data)));
},
result() {
this.loadingPipelineId = null;
diff --git a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
index 81d59f1ef65..d34ae8036ed 100644
--- a/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
+++ b/app/assets/javascripts/pipelines/components/graph/stage_column_component.vue
@@ -40,6 +40,11 @@ export default {
required: false,
default: () => [],
},
+ isStageView: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
jobHovered: {
type: String,
required: false,
@@ -50,16 +55,15 @@ export default {
required: false,
default: () => ({}),
},
- showStageName: {
- type: Boolean,
- required: false,
- default: false,
- },
sourceJobHovered: {
type: String,
required: false,
default: '',
},
+ userPermissions: {
+ type: Object,
+ required: true,
+ },
},
titleClasses: [
'gl-font-weight-bold',
@@ -69,20 +73,11 @@ export default {
'gl-pl-3',
],
computed: {
- /*
- currentGroups and filteredGroups are part of
- a test to hunt down a bug
- (see: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57142).
-
- They should be removed when the bug is rectified.
- */
- currentGroups() {
- return this.glFeatures.pipelineFilterJobs ? this.filteredGroups : this.groups;
+ canUpdatePipeline() {
+ return this.userPermissions.updatePipeline;
},
- filteredGroups() {
- return this.groups.map((group) => {
- return { ...group, jobs: group.jobs.filter(Boolean) };
- });
+ columnSpacingClass() {
+ return this.isStageView ? 'gl-px-6' : 'gl-px-9';
},
formattedTitle() {
return capitalize(escape(this.name));
@@ -90,6 +85,9 @@ export default {
hasAction() {
return !isEmpty(this.action);
},
+ showStageName() {
+ return !this.isStageView;
+ },
},
errorCaptured(err, _vm, info) {
reportToSentry('stage_column_component', `error: ${err}, info: ${info}`);
@@ -123,7 +121,7 @@ export default {
};
</script>
<template>
- <main-graph-wrapper class="gl-px-6" data-testid="stage-column">
+ <main-graph-wrapper :class="columnSpacingClass" data-testid="stage-column">
<template #stages>
<div
data-testid="stage-column-title"
@@ -132,7 +130,7 @@ export default {
>
<div>{{ formattedTitle }}</div>
<action-component
- v-if="hasAction"
+ v-if="hasAction && canUpdatePipeline"
:action-icon="action.icon"
:tooltip-text="action.title"
:link="action.path"
@@ -143,7 +141,7 @@ export default {
</template>
<template #jobs>
<div
- v-for="group in currentGroups"
+ v-for="group in groups"
:id="groupId(group)"
:key="getGroupId(group)"
data-testid="stage-column-group"
diff --git a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
index 7c62acbe8de..83f2466f0bf 100644
--- a/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
+++ b/app/assets/javascripts/pipelines/components/graph_shared/drawing_utils.js
@@ -75,11 +75,11 @@ export const generateLinksData = ({ links }, containerID, modifier = '') => {
// until we can safely draw the bezier to look nice.
// The adjustment number here is a magic number to make things
// look nice and should change if the padding changes. This goes well
- // with gl-px-6. gl-px-8 is more like 100.
- const straightLineDestinationX = targetNodeX - 60;
+ // with gl-px-9 which we translate with 100px here.
+ const straightLineDestinationX = targetNodeX - 100;
const controlPointX = straightLineDestinationX + (targetNodeX - straightLineDestinationX) / 2;
- if (straightLineDestinationX > 0) {
+ if (straightLineDestinationX > firstPointCoordinateX) {
path.lineTo(straightLineDestinationX, sourceNodeY);
}
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 d19215e7895..efad43ddd4f 100644
--- a/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
+++ b/app/assets/javascripts/pipelines/components/jobs_shared/action_component.vue
@@ -99,7 +99,7 @@ export default {
class="js-ci-action gl-ci-action-icon-container ci-action-icon-container ci-action-icon-wrapper gl-display-flex gl-align-items-center gl-justify-content-center"
@click.stop="onClickAction"
>
- <gl-loading-icon v-if="isLoading" class="js-action-icon-loading" />
+ <gl-loading-icon v-if="isLoading" size="sm" class="js-action-icon-loading" />
<gl-icon v-else :name="actionIcon" class="gl-mr-0!" :aria-label="actionIcon" />
</gl-button>
</template>
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index f1d9ced807b..b36c9c0d049 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -1,4 +1,4 @@
-import { isEqual, memoize, uniqWith } from 'lodash';
+import { memoize } from 'lodash';
import { createSankey } from './dag/drawing_utils';
/*
@@ -113,11 +113,24 @@ export const filterByAncestors = (links, nodeDict) =>
return !allAncestors.includes(source);
});
+/*
+ A peformant alternative to lodash's isEqual. Because findIndex always finds
+ the first instance of a match, if the found index is not the first, we know
+ it is in fact a duplicate.
+*/
+const deduplicate = (item, itemIndex, arr) => {
+ const foundIdx = arr.findIndex((test) => {
+ return test.source === item.source && test.target === item.target;
+ });
+
+ return foundIdx === itemIndex;
+};
+
export const parseData = (nodes) => {
const nodeDict = createNodeDict(nodes);
const allLinks = makeLinksFromNodes(nodes, nodeDict);
const filteredLinks = filterByAncestors(allLinks, nodeDict);
- const links = uniqWith(filteredLinks, isEqual);
+ const links = filteredLinks.filter(deduplicate);
return { nodes, links };
};
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
index 01baf0a42d5..836333c8bde 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/job_pill.vue
@@ -14,7 +14,7 @@ export default {
type: Number,
required: true,
},
- isHighlighted: {
+ isHovered: {
type: Boolean,
required: false,
default: false,
@@ -42,7 +42,7 @@ export default {
jobPillClasses() {
return [
{ 'gl-opacity-3': this.isFadedOut },
- this.isHighlighted ? 'gl-shadow-blue-200-x0-y0-b4-s2' : 'gl-inset-border-2-green-400',
+ { 'gl-bg-gray-50 gl-inset-border-1-gray-200': this.isHovered },
];
},
},
@@ -57,15 +57,17 @@ export default {
};
</script>
<template>
- <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
- <div
- :id="id"
- class="gl-w-15 gl-bg-white gl-text-center gl-text-truncate gl-rounded-pill gl-mb-3 gl-px-5 gl-py-2 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
- :class="jobPillClasses"
- @mouseover="onMouseEnter"
- @mouseleave="onMouseLeave"
- >
- {{ jobName }}
- </div>
- </tooltip-on-truncate>
+ <div class="gl-w-full">
+ <tooltip-on-truncate :title="jobName" truncate-target="child" placement="top">
+ <div
+ :id="id"
+ class="gl-bg-white gl-inset-border-1-gray-100 gl-text-center gl-text-truncate gl-rounded-6 gl-mb-3 gl-px-5 gl-py-3 gl-relative gl-z-index-1 gl-transition-duration-slow gl-transition-timing-function-ease"
+ :class="jobPillClasses"
+ @mouseover="onMouseEnter"
+ @mouseleave="onMouseLeave"
+ >
+ {{ jobName }}
+ </div>
+ </tooltip-on-truncate>
+ </div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
index 3ba0d7d0120..78771b6a072 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -4,14 +4,14 @@ import { __ } from '~/locale';
import { DRAW_FAILURE, DEFAULT } from '../../constants';
import LinksLayer from '../graph_shared/links_layer.vue';
import JobPill from './job_pill.vue';
-import StagePill from './stage_pill.vue';
+import StageName from './stage_name.vue';
export default {
components: {
GlAlert,
JobPill,
LinksLayer,
- StagePill,
+ StageName,
},
CONTAINER_REF: 'PIPELINE_GRAPH_CONTAINER_REF',
BASE_CONTAINER_ID: 'pipeline-graph-container',
@@ -21,6 +21,11 @@ export default {
[DRAW_FAILURE]: __('Could not draw the lines for job relationships'),
[DEFAULT]: __('An unknown error occurred.'),
},
+ // The combination of gl-w-full gl-min-w-full and gl-max-w-15 is necessary.
+ // The max width and the width make sure the ellipsis to work and the min width
+ // is for when there is less text than the stage column width (which the width 100% does not fix)
+ jobWrapperClasses:
+ 'gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8 gl-min-w-full gl-max-w-15',
props: {
pipelineData: {
required: true,
@@ -85,23 +90,8 @@ export default {
height: this.$refs[this.$options.CONTAINER_REF].scrollHeight,
};
},
- getStageBackgroundClasses(index) {
- const { length } = this.pipelineStages;
- // It's possible for a graph to have only one stage, in which
- // case we concatenate both the left and right rounding classes
- if (length === 1) {
- return 'gl-rounded-bottom-left-6 gl-rounded-top-left-6 gl-rounded-bottom-right-6 gl-rounded-top-right-6';
- }
-
- if (index === 0) {
- return 'gl-rounded-bottom-left-6 gl-rounded-top-left-6';
- }
-
- if (index === length - 1) {
- return 'gl-rounded-bottom-right-6 gl-rounded-top-right-6';
- }
-
- return '';
+ isFadedOut(jobName) {
+ return this.highlightedJobs.length > 1 && !this.isJobHighlighted(jobName);
},
isJobHighlighted(jobName) {
return this.highlightedJobs.includes(jobName);
@@ -137,7 +127,12 @@ export default {
>
{{ failure.text }}
</gl-alert>
- <div :id="containerId" :ref="$options.CONTAINER_REF" data-testid="graph-container">
+ <div
+ :id="containerId"
+ :ref="$options.CONTAINER_REF"
+ class="gl-bg-gray-10 gl-overflow-auto"
+ data-testid="graph-container"
+ >
<links-layer
:pipeline-data="pipelineStages"
:pipeline-id="$options.PIPELINE_ID"
@@ -152,23 +147,17 @@ export default {
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
- <div
- class="gl-display-flex gl-align-items-center gl-bg-white gl-w-full gl-px-8 gl-py-4 gl-mb-5"
- :class="getStageBackgroundClasses(index)"
- data-testid="stage-background"
- >
- <stage-pill :stage-name="stage.name" :is-empty="stage.groups.length === 0" />
+ <div class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5">
+ <stage-name :stage-name="stage.name" />
</div>
- <div
- class="gl-display-flex gl-flex-direction-column gl-align-items-center gl-w-full gl-px-8"
- >
+ <div :class="$options.jobWrapperClasses">
<job-pill
v-for="group in stage.groups"
:key="group.name"
:job-name="group.name"
:pipeline-id="$options.PIPELINE_ID"
- :is-highlighted="hasHighlightedJob && isJobHighlighted(group.name)"
- :is-faded-out="hasHighlightedJob && !isJobHighlighted(group.name)"
+ :is-hovered="highlightedJob === group.name"
+ :is-faded-out="isFadedOut(group.name)"
@on-mouse-enter="setHoveredJob"
@on-mouse-leave="removeHoveredJob"
/>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
index df48426f24e..367a18af248 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/stage_pill.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/stage_name.vue
@@ -1,4 +1,5 @@
<script>
+import { capitalize, escape } from 'lodash';
import tooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
export default {
@@ -10,26 +11,18 @@ export default {
type: String,
required: true,
},
- isEmpty: {
- type: Boolean,
- required: false,
- default: false,
- },
},
computed: {
- emptyClass() {
- return this.isEmpty ? 'gl-bg-gray-200' : 'gl-bg-gray-600';
+ formattedTitle() {
+ return capitalize(escape(this.stageName));
},
},
};
</script>
<template>
<tooltip-on-truncate :title="stageName" truncate-target="child" placement="top">
- <div
- class="gl-px-5 gl-py-2 gl-text-white gl-text-center gl-text-truncate gl-rounded-pill gl-w-20"
- :class="emptyClass"
- >
- {{ stageName }}
+ <div class="gl-py-2 gl-text-truncate gl-font-weight-bold gl-w-20">
+ {{ formattedTitle }}
</div>
</tooltip-on-truncate>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
index 104a3caab4c..1ce6654e0e9 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/empty_state.vue
@@ -16,7 +16,6 @@ export default {
consuming tasks, so you can spend more time creating.`),
aboutRunnersBtnText: s__('Pipelines|Learn about Runners'),
installRunnersBtnText: s__('Pipelines|Install GitLab Runners'),
- getStartedBtnText: s__('Pipelines|Get started with CI/CD'),
codeQualityTitle: s__('Pipelines|Improve code quality with GitLab CI/CD'),
codeQualityDescription: s__(`Pipelines|To keep your codebase simple,
readable, and accessible to contributors, use GitLab CI/CD
@@ -55,9 +54,6 @@ export default {
ciHelpPagePath() {
return helpPagePath('ci/quick_start/index.md');
},
- isPipelineEmptyStateTemplatesExperimentActive() {
- return this.canSetCi && Boolean(getExperimentData('pipeline_empty_state_templates'));
- },
isCodeQualityExperimentActive() {
return this.canSetCi && Boolean(getExperimentData('code_quality_walkthrough'));
},
@@ -81,37 +77,8 @@ export default {
</script>
<template>
<div>
- <gitlab-experiment
- v-if="isPipelineEmptyStateTemplatesExperimentActive"
- name="pipeline_empty_state_templates"
- >
- <template #control>
- <gl-empty-state
- :title="$options.i18n.title"
- :svg-path="emptyStateSvgPath"
- :description="$options.i18n.description"
- :primary-button-text="$options.i18n.getStartedBtnText"
- :primary-button-link="ciHelpPagePath"
- />
- </template>
- <template #candidate>
- <pipelines-ci-templates />
- </template>
- </gitlab-experiment>
- <gitlab-experiment v-else-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
- <template #control>
- <gl-empty-state
- :title="$options.i18n.title"
- :svg-path="emptyStateSvgPath"
- :description="$options.i18n.description"
- >
- <template #actions>
- <gl-button :href="ciHelpPagePath" variant="confirm" @click="trackClick()">
- {{ $options.i18n.getStartedBtnText }}
- </gl-button>
- </template>
- </gl-empty-state>
- </template>
+ <gitlab-experiment v-if="isCodeQualityExperimentActive" name="code_quality_walkthrough">
+ <template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.codeQualityTitle"
@@ -127,23 +94,7 @@ export default {
</template>
</gitlab-experiment>
<gitlab-experiment v-else-if="isCiRunnerTemplatesExperimentActive" name="ci_runner_templates">
- <template #control>
- <gl-empty-state
- :title="$options.i18n.title"
- :svg-path="emptyStateSvgPath"
- :description="$options.i18n.description"
- >
- <template #actions>
- <gl-button
- :href="ciHelpPagePath"
- variant="confirm"
- @click="trackCiRunnerTemplatesClick('get_started_button_clicked')"
- >
- {{ $options.i18n.getStartedBtnText }}
- </gl-button>
- </template>
- </gl-empty-state>
- </template>
+ <template #control><pipelines-ci-templates /></template>
<template #candidate>
<gl-empty-state
:title="$options.i18n.title"
@@ -169,14 +120,7 @@ export default {
</gl-empty-state>
</template>
</gitlab-experiment>
- <gl-empty-state
- v-else-if="canSetCi"
- :title="$options.i18n.title"
- :svg-path="emptyStateSvgPath"
- :description="$options.i18n.description"
- :primary-button-text="$options.i18n.getStartedBtnText"
- :primary-button-link="ciHelpPagePath"
- />
+ <pipelines-ci-templates v-else-if="canSetCi" />
<gl-empty-state
v-else
title=""
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
index d7bd2d731b1..5e18f636b52 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_multi_actions.vue
@@ -97,7 +97,7 @@ export default {
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<gl-dropdown-item
v-for="(artifact, i) in artifacts"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
index bf992b84387..7552ddb61dc 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import eventHub from '../../event_hub';
@@ -83,7 +83,9 @@ export default {
this.$refs.dropdown.hide();
this.isLoading = false;
- Flash(__('Something went wrong on our end.'));
+ createFlash({
+ message: __('Something went wrong on our end.'),
+ });
});
},
isDropdownOpen() {
@@ -118,7 +120,7 @@ export default {
<gl-icon :name="borderlessIcon" />
</span>
</template>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<ul
v-else
class="js-builds-dropdown-list scrollable-menu"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
index 52c8ef2cf26..fc8f31c5b7e 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipeline_url.vue
@@ -60,19 +60,20 @@ export default {
data-testid="pipeline-url-link"
data-qa-selector="pipeline_url_link"
>
- <span class="pipeline-id">#{{ pipeline.id }}</span>
+ #{{ pipeline.id }}
</gl-link>
<div class="label-container">
- <gl-link v-if="isScheduled" :href="pipelineScheduleUrl" target="__blank">
- <gl-badge
- v-gl-tooltip
- :title="__('This pipeline was triggered by a schedule.')"
- variant="info"
- size="sm"
- data-testid="pipeline-url-scheduled"
- >{{ __('Scheduled') }}</gl-badge
- >
- </gl-link>
+ <gl-badge
+ v-if="isScheduled"
+ v-gl-tooltip
+ :href="pipelineScheduleUrl"
+ target="__blank"
+ :title="__('This pipeline was triggered by a schedule.')"
+ variant="info"
+ size="sm"
+ data-testid="pipeline-url-scheduled"
+ >{{ __('Scheduled') }}</gl-badge
+ >
<gl-badge
v-if="pipeline.flags.latest"
v-gl-tooltip
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
index 8bb2657c161..e3373178239 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines.vue
@@ -2,7 +2,7 @@
import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { isEqual } from 'lodash';
import createFlash from '~/flash';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
index 147fff52101..36629d9f1f1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_artifacts.vue
@@ -96,7 +96,7 @@ export default {
{{ $options.i18n.artifactsFetchErrorMessage }}
</gl-alert>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<gl-alert v-else-if="!hasArtifacts" variant="info" :dismissible="false">
{{ $options.i18n.noArtifacts }}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
index c2ec8c57fd7..c6c81d5253b 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/pipelines_ci_templates.vue
@@ -1,17 +1,19 @@
<script>
-import { GlButton, GlCard, GlSprintf } from '@gitlab/ui';
-import ExperimentTracking from '~/experimentation/experiment_tracking';
+import { GlAvatar, GlButton, GlCard, GlSprintf } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
-import { HELLO_WORLD_TEMPLATE_KEY } from '../../constants';
+import { STARTER_TEMPLATE_NAME } from '~/pipeline_editor/constants';
+import Tracking from '~/tracking';
export default {
components: {
+ GlAvatar,
GlButton,
GlCard,
GlSprintf,
},
- HELLO_WORLD_TEMPLATE_KEY,
+ mixins: [Tracking.mixin()],
+ STARTER_TEMPLATE_NAME,
i18n: {
cta: s__('Pipelines|Use template'),
testTemplates: {
@@ -19,10 +21,10 @@ export default {
subtitle: s__(
'Pipelines|Use a sample %{codeStart}.gitlab-ci.yml%{codeEnd} template file to explore how CI/CD works.',
),
- helloWorld: {
- title: s__('Pipelines|“Hello world” with GitLab CI/CD'),
+ gettingStarted: {
+ title: s__('Pipelines|Get started with GitLab CI/CD'),
description: s__(
- 'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a simple pipeline that runs a “Hello world” script.',
+ 'Pipelines|Get familiar with GitLab CI/CD syntax by starting with a basic 3 stage CI/CD pipeline.',
),
},
},
@@ -34,31 +36,30 @@ export default {
description: s__('Pipelines|CI/CD template to test and deploy your %{name} project.'),
},
},
- inject: ['addCiYmlPath', 'suggestedCiTemplates'],
+ inject: ['pipelineEditorPath', 'suggestedCiTemplates'],
data() {
const templates = this.suggestedCiTemplates.map(({ name, logo }) => {
return {
name,
logo,
- link: mergeUrlParams({ template: name }, this.addCiYmlPath),
+ link: mergeUrlParams({ template: name }, this.pipelineEditorPath),
description: sprintf(this.$options.i18n.templates.description, { name }),
};
});
return {
templates,
- helloWorldTemplateUrl: mergeUrlParams(
- { template: HELLO_WORLD_TEMPLATE_KEY },
- this.addCiYmlPath,
+ gettingStartedTemplateUrl: mergeUrlParams(
+ { template: STARTER_TEMPLATE_NAME },
+ this.pipelineEditorPath,
),
};
},
methods: {
trackEvent(template) {
- const tracking = new ExperimentTracking('pipeline_empty_state_templates', {
+ this.track('template_clicked', {
label: template,
});
- tracking.event('template_clicked');
},
},
};
@@ -81,18 +82,18 @@ export default {
<div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
<div class="gl-mb-3">
<strong class="gl-text-gray-800 gl-mb-2">{{
- $options.i18n.testTemplates.helloWorld.title
+ $options.i18n.testTemplates.gettingStarted.title
}}</strong>
</div>
- <p class="gl-font-sm">{{ $options.i18n.testTemplates.helloWorld.description }}</p>
+ <p class="gl-font-sm">{{ $options.i18n.testTemplates.gettingStarted.description }}</p>
</div>
<gl-button
category="primary"
variant="confirm"
- :href="helloWorldTemplateUrl"
+ :href="gettingStartedTemplateUrl"
data-testid="test-template-link"
- @click="trackEvent($options.HELLO_WORLD_TEMPLATE_KEY)"
+ @click="trackEvent($options.STARTER_TEMPLATE_NAME)"
>
{{ $options.i18n.cta }}
</gl-button>
@@ -109,11 +110,12 @@ export default {
class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-border-b-solid gl-border-b-1 gl-border-b-gray-100 gl-pb-3 gl-pt-3"
>
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
- <img
- width="64"
- height="64"
+ <gl-avatar
:src="template.logo"
- class="gl-mr-6"
+ :size="64"
+ class="gl-mr-6 gl-bg-white dark-mode-override"
+ shape="rect"
+ :alt="template.name"
data-testid="template-logo"
/>
<div class="gl-flex-direction-row">
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
index 15ff7da35e1..5409e68cdc4 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue
@@ -60,7 +60,7 @@ export default {
@input="searchBranches"
>
<template #suggestions>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="(branch, index) in branches"
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
index af62c492748..afcdd63b664 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue
@@ -55,7 +55,7 @@ export default {
<template>
<gl-filtered-search-token v-bind="{ ...$props, ...$attrs }" v-on="$listeners" @input="searchTags">
<template #suggestions>
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion v-for="(tag, index) in tags" :key="index" :value="tag">
{{ tag }}
diff --git a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
index bc661f37493..33115d72b9c 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue
@@ -98,7 +98,7 @@ export default {
}}</gl-filtered-search-suggestion>
<gl-dropdown-divider />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="user in users"
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index 01705e7726f..21b114825a6 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -35,6 +35,3 @@ export const POST_FAILURE = 'post_failure';
export const UNSUPPORTED_DATA = 'unsupported_data';
export const CHILD_VIEW = 'child';
-
-// The key of the template is the same as the filename
-export const HELLO_WORLD_TEMPLATE_KEY = 'Hello-World';
diff --git a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
index 9f15b6c4ae3..5c34f4e4f7e 100644
--- a/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
+++ b/app/assets/javascripts/pipelines/mixins/graph_pipeline_bundle_mixin.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
export default {
@@ -13,7 +13,9 @@ export default {
})
.catch(() => {
this.mediator.store.toggleLoading(pipeline);
- flash(__('An error occurred while fetching the pipeline.'));
+ createFlash({
+ message: __('An error occurred while fetching the pipeline.'),
+ });
});
},
/**
@@ -53,9 +55,11 @@ export default {
requestRefreshPipelineGraph() {
// When an action is clicked
// (whether in the dropdown or in the main nodes, we refresh the big graph)
- this.mediator
- .refreshPipeline()
- .catch(() => flash(__('An error occurred while making the request.')));
+ this.mediator.refreshPipeline().catch(() =>
+ createFlash({
+ message: __('An error occurred while making the request.'),
+ }),
+ );
},
},
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 9ab4753fec8..e8d5ed175ba 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { parseBoolean } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import Translate from '~/vue_shared/translate';
@@ -96,14 +96,18 @@ export default async function initPipelineDetailsBundle() {
try {
createPipelineHeaderApp(SELECTORS.PIPELINE_HEADER, apolloProvider, dataset.graphqlResourceEtag);
} catch {
- Flash(__('An error occurred while loading a section of this page.'));
+ createFlash({
+ message: __('An error occurred while loading a section of this page.'),
+ });
}
if (canShowNewPipelineDetails) {
try {
createPipelinesDetailApp(SELECTORS.PIPELINE_GRAPH, apolloProvider, dataset);
} catch {
- Flash(__('An error occurred while loading the pipeline.'));
+ createFlash({
+ message: __('An error occurred while loading the pipeline.'),
+ });
}
} else {
const { default: PipelinesMediator } = await import(
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediator.js b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
index 09637c25654..72c4fedc64c 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediator.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediator.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import { deprecatedCreateFlash as Flash } from '../flash';
+import createFlash from '~/flash';
import Poll from '../lib/utils/poll';
import { __ } from '../locale';
import PipelineService from './services/pipeline_service';
@@ -47,7 +47,9 @@ export default class pipelinesMediator {
errorCallback() {
this.state.isLoading = false;
- Flash(__('An error occurred while fetching the pipeline.'));
+ createFlash({
+ message: __('An error occurred while fetching the pipeline.'),
+ });
}
refreshPipeline() {
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/pipelines/pipeline_shared_client.js
index c3be487caae..7a922acd0b3 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/pipelines/pipeline_shared_client.js
@@ -5,6 +5,7 @@ export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
+ assumeImmutableResults: true,
useGet: true,
},
),
diff --git a/app/assets/javascripts/pipelines/pipelines_index.js b/app/assets/javascripts/pipelines/pipelines_index.js
index 925a96ea1aa..c4c2b5f2927 100644
--- a/app/assets/javascripts/pipelines/pipelines_index.js
+++ b/app/assets/javascripts/pipelines/pipelines_index.js
@@ -29,7 +29,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
errorStateSvgPath,
noPipelinesSvgPath,
newPipelinePath,
- addCiYmlPath,
+ pipelineEditorPath,
suggestedCiTemplates,
canCreatePipeline,
hasGitlabCi,
@@ -44,7 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
return new Vue({
el,
provide: {
- addCiYmlPath,
+ pipelineEditorPath,
artifactsEndpoint,
artifactsEndpointPlaceholder,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 07d8f3cc5f1..a0129dd536b 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -131,7 +131,8 @@ export default {
<div class="col-lg-8">
<div class="form-group">
<gl-button
- variant="success"
+ category="primary"
+ variant="confirm"
name="commit"
type="submit"
:disabled="!isSubmitEnabled"
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index dad2c18fb18..c49ade2bbb8 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { parseBoolean } from '~/lib/utils/common_utils';
import { Rails } from '~/lib/utils/rails_ujs';
import TimezoneDropdown, {
formatTimezone,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
-import { deprecatedCreateFlash as flash } from '../flash';
export default class Profile {
constructor({ form } = {}) {
@@ -83,14 +83,21 @@ export default class Profile {
this.updateHeaderAvatar();
}
- flash(data.message, 'notice');
+ createFlash({
+ message: data.message,
+ type: 'notice',
+ });
})
.then(() => {
window.scrollTo(0, 0);
// Enable submit button after requests ends
self.form.find(':input[disabled]').enable();
})
- .catch((error) => flash(error.message));
+ .catch((error) =>
+ createFlash({
+ message: error.message,
+ }),
+ );
}
updateHeaderAvatar() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index f44661cb139..d295c06928f 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -2,7 +2,7 @@
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import $ from 'jquery';
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
import { spriteIcon } from '~/lib/utils/common_utils';
@@ -88,7 +88,11 @@ export default class ProjectFindFile {
this.findFile();
this.element.find('.files-slider tr.tree-item').eq(0).addClass('selected').focus();
})
- .catch(() => flash(__('An error occurred while loading filenames')));
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while loading filenames'),
+ }),
+ );
}
// render result
diff --git a/app/assets/javascripts/project_label_subscription.js b/app/assets/javascripts/project_label_subscription.js
index e6dd4145cb8..f7804c2faa4 100644
--- a/app/assets/javascripts/project_label_subscription.js
+++ b/app/assets/javascripts/project_label_subscription.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { fixTitle } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
@@ -60,7 +60,11 @@ export default class ProjectLabelSubscription {
return button;
});
})
- .catch(() => flash(__('There was an error subscribing to this label.')));
+ .catch(() =>
+ createFlash({
+ message: __('There was an error subscribing to this label.'),
+ }),
+ );
}
static setNewTitle($button, originalTitle, newStatus) {
diff --git a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
index cc5bc703994..52da8aaba4d 100644
--- a/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
+++ b/app/assets/javascripts/projects/commit/components/branches_dropdown.vue
@@ -99,7 +99,7 @@ export default {
{{ branch }}
</gl-dropdown-item>
<gl-dropdown-text v-show="isFetching" data-testid="dropdown-text-loading-icon">
- <gl-loading-icon class="gl-mx-auto" />
+ <gl-loading-icon size="sm" class="gl-mx-auto" />
</gl-dropdown-text>
<gl-dropdown-text
v-if="!filteredResults.length && !isFetching"
diff --git a/app/assets/javascripts/projects/commits/components/author_select.vue b/app/assets/javascripts/projects/commits/components/author_select.vue
index 1566232751d..c8a0a3417f3 100644
--- a/app/assets/javascripts/projects/commits/components/author_select.vue
+++ b/app/assets/javascripts/projects/commits/components/author_select.vue
@@ -9,8 +9,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import { mapState, mapActions } from 'vuex';
-import { urlParamsToObject } from '~/lib/utils/common_utils';
-import { redirectTo } from '~/lib/utils/url_utility';
+import { redirectTo, queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
const tooltipMessage = __('Searching by both author and message is currently not supported.');
@@ -52,7 +51,7 @@ export default {
},
mounted() {
this.fetchAuthors();
- const params = urlParamsToObject(window.location.search);
+ const params = queryToObject(window.location.search);
const { search: searchParam, author: authorParam } = params;
const commitsSearchInput = this.projectCommitsEl.querySelector('#commits-search');
diff --git a/app/assets/javascripts/projects/components/project_delete_button.vue b/app/assets/javascripts/projects/components/project_delete_button.vue
index 81d23a563e2..06711e4025a 100644
--- a/app/assets/javascripts/projects/components/project_delete_button.vue
+++ b/app/assets/javascripts/projects/components/project_delete_button.vue
@@ -24,9 +24,6 @@ export default {
alertBody: __(
'Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc.',
),
- modalBody: __(
- "This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.",
- ),
},
};
</script>
@@ -46,7 +43,6 @@ export default {
</template>
</gl-sprintf>
</gl-alert>
- <p>{{ $options.strings.modalBody }}</p>
</template>
</shared-delete-button>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index 1c4413bef71..0b0560f63c1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -225,11 +225,21 @@ export default {
{
name: 'success',
data: this.mergeLabelsAndValues(labels, success),
+ areaStyle: {
+ color: this.$options.successColor,
+ },
+ lineStyle: {
+ color: this.$options.successColor,
+ },
+ itemStyle: {
+ color: this.$options.successColor,
+ },
},
],
};
},
},
+ successColor: '#608b2f',
chartContainerHeight: CHART_CONTAINER_HEIGHT,
timesChartOptions: {
height: INNER_CHART_HEIGHT,
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 04ea6f760f6..ee02f446795 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -74,6 +74,7 @@ const deriveProjectPathFromUrl = ($projectImportUrl) => {
const bindEvents = () => {
const $newProjectForm = $('#new_project');
const $projectImportUrl = $('#project_import_url');
+ const $projectImportUrlWarning = $('.js-import-url-warning');
const $projectPath = $('.tab-pane.active #project_path');
const $useTemplateBtn = $('.template-button > input');
const $projectFieldsForm = $('.project-fields-form');
@@ -134,7 +135,25 @@ const bindEvents = () => {
$projectPath.val($projectPath.val().trim());
});
- $projectImportUrl.keyup(() => deriveProjectPathFromUrl($projectImportUrl));
+ function updateUrlPathWarningVisibility() {
+ const url = $projectImportUrl.val();
+ const URL_PATTERN = /(?:git|https?):\/\/.*\/.*\.git$/;
+ const isUrlValid = URL_PATTERN.test(url);
+ $projectImportUrlWarning.toggleClass('hide', isUrlValid);
+ }
+
+ let isProjectImportUrlDirty = false;
+ $projectImportUrl.on('blur', () => {
+ isProjectImportUrlDirty = true;
+ updateUrlPathWarningVisibility();
+ });
+ $projectImportUrl.on('keyup', () => {
+ deriveProjectPathFromUrl($projectImportUrl);
+ // defer error message till first input blur
+ if (isProjectImportUrlDirty) {
+ updateUrlPathWarningVisibility();
+ }
+ });
$('.js-import-git-toggle-button').on('click', () => {
const $projectMirror = $('#project_mirror');
diff --git a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
index 0786a74f6b1..e4edb950a1e 100644
--- a/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
+++ b/app/assets/javascripts/projects/settings/components/shared_runners_toggle.vue
@@ -1,15 +1,23 @@
<script>
import { GlAlert, GlToggle, GlTooltip } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
const DEFAULT_ERROR_MESSAGE = __('An error occurred while updating the configuration.');
+const REQUIRES_VALIDATION_TEXT = s__(
+ `Billings|Shared runners cannot be enabled until a valid credit card is on file.`,
+);
export default {
+ i18n: {
+ REQUIRES_VALIDATION_TEXT,
+ },
components: {
GlAlert,
GlToggle,
GlTooltip,
+ CcValidationRequiredAlert: () =>
+ import('ee_component/billings/components/cc_validation_required_alert.vue'),
},
props: {
isDisabledAndUnoverridable: {
@@ -20,6 +28,10 @@ export default {
type: Boolean,
required: true,
},
+ isCreditCardValidationRequired: {
+ type: Boolean,
+ required: false,
+ },
updatePath: {
type: String,
required: true,
@@ -28,14 +40,24 @@ export default {
data() {
return {
isLoading: false,
- isSharedRunnerEnabled: false,
+ isSharedRunnerEnabled: this.isEnabled,
errorMessage: null,
+ successfulValidation: false,
};
},
- created() {
- this.isSharedRunnerEnabled = this.isEnabled;
+ computed: {
+ showCreditCardValidation() {
+ return (
+ this.isCreditCardValidationRequired &&
+ !this.isSharedRunnerEnabled &&
+ !this.successfulValidation
+ );
+ },
},
methods: {
+ creditCardValidated() {
+ this.successfulValidation = true;
+ },
toggleSharedRunners() {
this.isLoading = true;
this.errorMessage = null;
@@ -61,16 +83,25 @@ export default {
<gl-alert v-if="errorMessage" class="gl-mb-3" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
- <div ref="sharedRunnersToggle">
- <gl-toggle
- :disabled="isDisabledAndUnoverridable"
- :is-loading="isLoading"
- :label="__('Enable shared runners for this project')"
- :value="isSharedRunnerEnabled"
- data-testid="toggle-shared-runners"
- @change="toggleSharedRunners"
- />
- </div>
+
+ <cc-validation-required-alert
+ v-if="showCreditCardValidation"
+ class="gl-pb-5"
+ :custom-message="$options.i18n.REQUIRES_VALIDATION_TEXT"
+ @verifiedCreditCard="creditCardValidated"
+ />
+
+ <gl-toggle
+ v-else
+ ref="sharedRunnersToggle"
+ :disabled="isDisabledAndUnoverridable"
+ :is-loading="isLoading"
+ :label="__('Enable shared runners for this project')"
+ :value="isSharedRunnerEnabled"
+ data-testid="toggle-shared-runners"
+ @change="toggleSharedRunners"
+ />
+
<gl-tooltip v-if="isDisabledAndUnoverridable" :target="() => $refs.sharedRunnersToggle">
{{ __('Shared runners are disabled on group level') }}
</gl-tooltip>
diff --git a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
index eaeb5848b68..5ca864a412b 100644
--- a/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
+++ b/app/assets/javascripts/projects/settings/mount_shared_runners_toggle.js
@@ -4,7 +4,12 @@ import SharedRunnersToggle from '~/projects/settings/components/shared_runners_t
export default (containerId = 'toggle-shared-runners-form') => {
const containerEl = document.getElementById(containerId);
- const { isDisabledAndUnoverridable, isEnabled, updatePath } = containerEl.dataset;
+ const {
+ isDisabledAndUnoverridable,
+ isEnabled,
+ updatePath,
+ isCreditCardValidationRequired,
+ } = containerEl.dataset;
return new Vue({
el: containerEl,
@@ -13,6 +18,7 @@ export default (containerId = 'toggle-shared-runners-form') => {
props: {
isDisabledAndUnoverridable: parseBoolean(isDisabledAndUnoverridable),
isEnabled: parseBoolean(isEnabled),
+ isCreditCardValidationRequired: parseBoolean(isCreditCardValidationRequired),
updatePath,
},
});
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 fb00f58abae..4c083ed5496 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,5 @@
<script>
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSafeHtmlDirective } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import { __, sprintf } from '~/locale';
import ServiceDeskSetting from './service_desk_setting.vue';
@@ -9,6 +9,9 @@ export default {
GlAlert,
ServiceDeskSetting,
},
+ directives: {
+ SafeHtml: GlSafeHtmlDirective,
+ },
inject: {
initialIsEnabled: {
default: false,
@@ -121,7 +124,7 @@ export default {
<template>
<div>
<gl-alert v-if="isAlertShowing" class="mb-3" :variant="alertVariant" @dismiss="onDismiss">
- {{ alertMessage }}
+ <span v-safe-html="alertMessage"></span>
</gl-alert>
<service-desk-setting
:is-enabled="isEnabled"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 3294a37c26a..34d53e2de0c 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -144,7 +144,7 @@ export default {
</span>
</template>
<template v-else>
- <gl-loading-icon :inline="true" />
+ <gl-loading-icon size="sm" :inline="true" />
<span class="sr-only">{{ __('Fetching incoming email') }}</span>
</template>
diff --git a/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
new file mode 100644
index 00000000000..0b398eddc9c
--- /dev/null
+++ b/app/assets/javascripts/projects/terraform_notification/components/terraform_notification.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlBanner } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { parseBoolean, setCookie, getCookie } from '~/lib/utils/common_utils';
+import { s__ } from '~/locale';
+
+export default {
+ name: 'TerraformNotification',
+ i18n: {
+ title: s__('TerraformBanner|Using Terraform? Try the GitLab Managed Terraform State'),
+ description: s__(
+ 'TerraformBanner|The GitLab managed Terraform state backend can store your Terraform state easily and securely, and spares you from setting up additional remote resources. Its features include: versioning, encryption of the state file both in transit and at rest, locking, and remote Terraform plan/apply execution.',
+ ),
+ buttonText: s__("TerraformBanner|Learn more about GitLab's Backend State"),
+ },
+ components: {
+ GlBanner,
+ },
+ props: {
+ projectId: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isVisible: true,
+ };
+ },
+ computed: {
+ bannerDissmisedKey() {
+ return `terraform_notification_dismissed_for_project_${this.projectId}`;
+ },
+ docsUrl() {
+ return helpPagePath('user/infrastructure/terraform_state');
+ },
+ },
+ created() {
+ if (parseBoolean(getCookie(this.bannerDissmisedKey))) {
+ this.isVisible = false;
+ }
+ },
+ methods: {
+ handleClose() {
+ setCookie(this.bannerDissmisedKey, true);
+ this.isVisible = false;
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="isVisible">
+ <div class="gl-py-5">
+ <gl-banner
+ :title="$options.i18n.title"
+ :button-text="$options.i18n.buttonText"
+ :button-link="docsUrl"
+ variant="introduction"
+ @close="handleClose"
+ >
+ <p>{{ $options.i18n.description }}</p>
+ </gl-banner>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/terraform_notification/index.js b/app/assets/javascripts/projects/terraform_notification/index.js
new file mode 100644
index 00000000000..eb04f109a8e
--- /dev/null
+++ b/app/assets/javascripts/projects/terraform_notification/index.js
@@ -0,0 +1,18 @@
+import Vue from 'vue';
+import TerraformNotification from './components/terraform_notification.vue';
+
+export default () => {
+ const el = document.querySelector('.js-terraform-notification');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectId } = el.dataset;
+
+ return new Vue({
+ el,
+ render: (createElement) =>
+ createElement(TerraformNotification, { props: { projectId: Number(projectId) } }),
+ });
+};
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
index f3d12e0dd00..f6f409873c8 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status_component.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import Visibility from 'visibilityjs';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import Poll from '~/lib/utils/poll';
import { __, s__, sprintf } from '~/locale';
import ciIcon from '~/vue_shared/components/ci_icon.vue';
@@ -57,7 +57,9 @@ export default {
group: 'notfound',
};
this.isLoading = false;
- Flash(s__('Something went wrong on our end'));
+ createFlash({
+ message: s__('Something went wrong on our end'),
+ });
},
initPolling() {
this.poll = new Poll({
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 726ddba1014..d0d2c1400a7 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import CreateItemDropdown from '~/create_item_dropdown';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
@@ -135,6 +135,10 @@ export default class ProtectedBranchCreate {
.then(() => {
window.location.reload();
})
- .catch(() => Flash(__('Failed to protect the branch')));
+ .catch(() =>
+ createFlash({
+ message: __('Failed to protect the branch'),
+ }),
+ );
}
}
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.js b/app/assets/javascripts/protected_tags/protected_tag_edit.js
index ae7855d4638..1fe9a753e1e 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '../flash';
+import createFlash from '~/flash';
import axios from '../lib/utils/axios_utils';
import { FAILED_TO_UPDATE_TAG_MESSAGE } from './constants';
import ProtectedTagAccessDropdown from './protected_tag_access_dropdown';
@@ -49,7 +49,9 @@ export default class ProtectedTagEdit {
this.$allowedToCreateDropdownButton.enable();
window.scrollTo({ top: 0, behavior: 'smooth' });
- flash(FAILED_TO_UPDATE_TAG_MESSAGE);
+ createFlash({
+ message: FAILED_TO_UPDATE_TAG_MESSAGE,
+ });
});
}
}
diff --git a/app/assets/javascripts/ref/constants.js b/app/assets/javascripts/ref/constants.js
index 44d0f50b832..1cef986a83d 100644
--- a/app/assets/javascripts/ref/constants.js
+++ b/app/assets/javascripts/ref/constants.js
@@ -1,3 +1,4 @@
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { __ } from '~/locale';
export const REF_TYPE_BRANCHES = 'REF_TYPE_BRANCHES';
@@ -7,7 +8,7 @@ export const ALL_REF_TYPES = Object.freeze([REF_TYPE_BRANCHES, REF_TYPE_TAGS, RE
export const X_TOTAL_HEADER = 'x-total';
-export const SEARCH_DEBOUNCE_MS = 250;
+export const SEARCH_DEBOUNCE_MS = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export const DEFAULT_I18N = Object.freeze({
dropdownHeader: __('Select Git revision'),
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
new file mode 100644
index 00000000000..8d9e221af4c
--- /dev/null
+++ b/app/assets/javascripts/registry/explorer/components/list_page/cleanup_status.vue
@@ -0,0 +1,71 @@
+<script>
+import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import {
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ CLEANUP_STATUS_SCHEDULED,
+ CLEANUP_STATUS_ONGOING,
+ CLEANUP_STATUS_UNFINISHED,
+ UNFINISHED_STATUS,
+ UNSCHEDULED_STATUS,
+ SCHEDULED_STATUS,
+ ONGOING_STATUS,
+} from '../../constants/index';
+
+export default {
+ name: 'CleanupStatus',
+ components: {
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ validator(value) {
+ return [UNFINISHED_STATUS, UNSCHEDULED_STATUS, SCHEDULED_STATUS, ONGOING_STATUS].includes(
+ value,
+ );
+ },
+ },
+ },
+ i18n: {
+ CLEANUP_STATUS_SCHEDULED,
+ CLEANUP_STATUS_ONGOING,
+ CLEANUP_STATUS_UNFINISHED,
+ ASYNC_DELETE_IMAGE_ERROR_MESSAGE,
+ },
+ computed: {
+ showStatus() {
+ return this.status !== UNSCHEDULED_STATUS;
+ },
+ failedDelete() {
+ return this.status === UNFINISHED_STATUS;
+ },
+ statusText() {
+ return this.$options.i18n[`CLEANUP_STATUS_${this.status}`];
+ },
+ expireIconClass() {
+ return this.failedDelete ? 'gl-text-orange-500' : '';
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="showStatus" class="gl-display-inline-flex gl-align-items-center">
+ <gl-icon name="expire" data-testid="main-icon" :class="expireIconClass" />
+ <span class="gl-mx-2">
+ {{ statusText }}
+ </span>
+ <gl-icon
+ v-if="failedDelete"
+ v-gl-tooltip="{ title: $options.i18n.ASYNC_DELETE_IMAGE_ERROR_MESSAGE }"
+ :size="14"
+ class="gl-text-black-normal"
+ data-testid="extra-info"
+ name="information"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
index 930ad01c758..c1ec523574a 100644
--- a/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
+++ b/app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue
@@ -16,6 +16,7 @@ import {
ROOT_IMAGE_TEXT,
} from '../../constants/index';
import DeleteButton from '../delete_button.vue';
+import CleanupStatus from './cleanup_status.vue';
export default {
name: 'ImageListRow',
@@ -26,6 +27,7 @@ export default {
GlIcon,
ListItem,
GlSkeletonLoader,
+ CleanupStatus,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -112,27 +114,24 @@ export default {
:title="item.location"
category="tertiary"
/>
- <gl-icon
- v-if="warningIconText"
- v-gl-tooltip="{ title: warningIconText }"
- data-testid="warning-icon"
- name="warning"
- class="gl-text-orange-500"
- />
</template>
<template #left-secondary>
- <span
- v-if="!metadataLoading"
- class="gl-display-flex gl-align-items-center"
- data-testid="tags-count"
- >
- <gl-icon name="tag" class="gl-mr-2" />
- <gl-sprintf :message="tagsCountText">
- <template #count>
- {{ item.tagsCount }}
- </template>
- </gl-sprintf>
- </span>
+ <template v-if="!metadataLoading">
+ <span class="gl-display-flex gl-align-items-center" data-testid="tags-count">
+ <gl-icon name="tag" class="gl-mr-2" />
+ <gl-sprintf :message="tagsCountText">
+ <template #count>
+ {{ item.tagsCount }}
+ </template>
+ </gl-sprintf>
+ </span>
+
+ <cleanup-status
+ v-if="item.expirationPolicyCleanupStatus"
+ class="ml-2"
+ :status="item.expirationPolicyCleanupStatus"
+ />
+ </template>
<div v-else class="gl-w-full">
<gl-skeleton-loader :width="900" :height="16" preserve-aspect-ratio="xMinYMax meet">
diff --git a/app/assets/javascripts/registry/explorer/constants/details.js b/app/assets/javascripts/registry/explorer/constants/details.js
index 5dcc042a9c4..9b4c06349e2 100644
--- a/app/assets/javascripts/registry/explorer/constants/details.js
+++ b/app/assets/javascripts/registry/explorer/constants/details.js
@@ -89,6 +89,10 @@ export const CLEANUP_DISABLED_TOOLTIP = s__(
'ContainerRegistry|Cleanup is disabled for this project',
);
+export const CLEANUP_STATUS_SCHEDULED = s__('ContainerRegistry|Cleanup will run soon');
+export const CLEANUP_STATUS_ONGOING = s__('ContainerRegistry|Cleanup is ongoing');
+export const CLEANUP_STATUS_UNFINISHED = s__('ContainerRegistry|Cleanup timed out');
+
export const DETAILS_DELETE_IMAGE_ERROR_MESSAGE = s__(
'ContainerRegistry|Something went wrong while scheduling the image for deletion.',
);
diff --git a/app/assets/javascripts/related_issues/components/related_issues_list.vue b/app/assets/javascripts/related_issues/components/related_issues_list.vue
index 825a4a02b71..8f486fb1b07 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
import Sortable from 'sortablejs';
-import sortableConfig from 'ee_else_ce/sortable/sortable_config';
+import sortableConfig from '~/sortable/sortable_config';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
export default {
@@ -102,7 +102,12 @@ export default {
class="related-issues-loading-icon"
data-qa-selector="related_issues_loading_placeholder"
>
- <gl-loading-icon ref="loadingIcon" label="Fetching linked issues" class="gl-mt-2" />
+ <gl-loading-icon
+ ref="loadingIcon"
+ size="sm"
+ label="Fetching linked issues"
+ class="gl-mt-2"
+ />
</div>
<ul ref="list" :class="{ 'content-list': !canReorder }" class="related-items-list">
<li
diff --git a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
index ccb92d2aedc..6fb1d1ed365 100644
--- a/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
+++ b/app/assets/javascripts/related_merge_requests/components/related_merge_requests.vue
@@ -94,7 +94,7 @@ export default {
</div>
<div>
<div v-if="isFetchingMergeRequests" class="qa-related-merge-requests-loading-icon">
- <gl-loading-icon label="Fetching related merge requests" class="py-2" />
+ <gl-loading-icon size="sm" label="Fetching related merge requests" class="py-2" />
</div>
<ul v-else class="content-list related-items-list">
<li v-for="mr in mergeRequests" :key="mr.id" class="list-item pt-0 pb-0">
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 3774f97a060..39140216bc5 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -1,8 +1,7 @@
<script>
import { GlButton, GlFormInput, GlFormGroup, GlSprintf } from '@gitlab/ui';
import { mapState, mapActions, mapGetters } from 'vuex';
-import { getParameterByName } from '~/lib/utils/common_utils';
-import { isSameOriginUrl } from '~/lib/utils/url_utility';
+import { isSameOriginUrl, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index 31d335fa15d..c2c91f406a1 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,7 +1,7 @@
<script>
import { GlEmptyState, GlLink, GlButton } from '@gitlab/ui';
import { mapState, mapActions } from 'vuex';
-import { getParameterByName } from '~/lib/utils/common_utils';
+import { getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
diff --git a/app/assets/javascripts/releases/components/app_index_apollo_client.vue b/app/assets/javascripts/releases/components/app_index_apollo_client.vue
index ea0aa409577..f49c44a399f 100644
--- a/app/assets/javascripts/releases/components/app_index_apollo_client.vue
+++ b/app/assets/javascripts/releases/components/app_index_apollo_client.vue
@@ -1,12 +1,12 @@
<script>
import { GlButton } from '@gitlab/ui';
+import allReleasesQuery from 'shared_queries/releases/all_releases.query.graphql';
import createFlash from '~/flash';
-import { historyPushState, getParameterByName } from '~/lib/utils/common_utils';
+import { historyPushState } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
-import { setUrlParams } from '~/lib/utils/url_utility';
+import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
-import allReleasesQuery from '~/releases/graphql/queries/all_releases.query.graphql';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
diff --git a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
index 3a742db7d9e..3a927dfc756 100644
--- a/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
+++ b/app/assets/javascripts/releases/graphql/fragments/release.fragment.graphql
@@ -1,4 +1,5 @@
fragment Release on Release {
+ __typename
name
tagName
tagPath
@@ -7,15 +8,20 @@ fragment Release on Release {
createdAt
upcomingRelease
assets {
+ __typename
count
sources {
+ __typename
nodes {
+ __typename
format
url
}
}
links {
+ __typename
nodes {
+ __typename
id
name
url
@@ -26,13 +32,16 @@ fragment Release on Release {
}
}
evidences {
+ __typename
nodes {
+ __typename
filepath
collectedAt
sha
}
}
links {
+ __typename
editUrl
selfUrl
openedIssuesUrl
@@ -42,22 +51,27 @@ fragment Release on Release {
closedMergeRequestsUrl
}
commit {
+ __typename
sha
webUrl
title
}
author {
+ __typename
webUrl
avatarUrl
username
}
milestones {
+ __typename
nodes {
+ __typename
id
title
description
webPath
stats {
+ __typename
totalIssuesCount
closedIssuesCount
}
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 47c5afefd78..75a73acb9ae 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
@@ -9,6 +9,7 @@ fragment ReleaseForEditing on Release {
name
url
linkType
+ directAssetPath
}
}
}
diff --git a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
index 10e4d883e62..f2d89dbe682 100644
--- a/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
+++ b/app/assets/javascripts/releases/graphql/queries/all_releases.query.graphql
@@ -1,5 +1,11 @@
#import "../fragments/release.fragment.graphql"
+# This query is identical to
+# `app/graphql/queries/releases/all_releases.query.graphql`.
+# These two queries should be kept in sync.
+# When the `releases_index_apollo_client` feature flag is
+# removed, this query should be removed entirely.
+
query allReleases(
$fullPath: ID!
$first: Int
@@ -9,11 +15,14 @@ query allReleases(
$sort: ReleaseSort
) {
project(fullPath: $fullPath) {
+ __typename
releases(first: $first, last: $last, before: $before, after: $after, sort: $sort) {
+ __typename
nodes {
...Release
}
pageInfo {
+ __typename
startCursor
hasPreviousPage
hasNextPage
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 5955ec3352e..576f099248e 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -165,6 +165,7 @@ const createReleaseLink = async ({ state, link }) => {
name: link.name,
url: link.url,
linkType: link.linkType.toUpperCase(),
+ directAssetPath: link.directAssetPath,
},
},
});
diff --git a/app/assets/javascripts/reports/components/issue_body.js b/app/assets/javascripts/reports/components/issue_body.js
index 56f46a3938e..6014d9d6ad8 100644
--- a/app/assets/javascripts/reports/components/issue_body.js
+++ b/app/assets/javascripts/reports/components/issue_body.js
@@ -1,3 +1,4 @@
+import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
import AccessibilityIssueBody from '../accessibility_report/components/accessibility_issue_body.vue';
import CodequalityIssueBody from '../codequality_report/components/codequality_issue_body.vue';
import TestIssueBody from '../grouped_test_report/components/test_issue_body.vue';
@@ -13,3 +14,11 @@ export const componentNames = {
CodequalityIssueBody: CodequalityIssueBody.name,
TestIssueBody: TestIssueBody.name,
};
+
+export const iconComponents = {
+ IssueStatusIcon,
+};
+
+export const iconComponentNames = {
+ IssueStatusIcon: IssueStatusIcon.name,
+};
diff --git a/app/assets/javascripts/reports/components/report_item.vue b/app/assets/javascripts/reports/components/report_item.vue
index df20d5c19ba..8871da8fbd7 100644
--- a/app/assets/javascripts/reports/components/report_item.vue
+++ b/app/assets/javascripts/reports/components/report_item.vue
@@ -1,12 +1,16 @@
<script>
-import { components, componentNames } from 'ee_else_ce/reports/components/issue_body';
-import IssueStatusIcon from '~/reports/components/issue_status_icon.vue';
+import {
+ components,
+ componentNames,
+ iconComponents,
+ iconComponentNames,
+} from 'ee_else_ce/reports/components/issue_body';
export default {
name: 'ReportItem',
components: {
- IssueStatusIcon,
...components,
+ ...iconComponents,
},
props: {
issue: {
@@ -19,6 +23,12 @@ export default {
default: '',
validator: (value) => value === '' || Object.values(componentNames).includes(value),
},
+ iconComponent: {
+ type: String,
+ required: false,
+ default: iconComponentNames.IssueStatusIcon,
+ validator: (value) => Object.values(iconComponentNames).includes(value),
+ },
// failed || success
status: {
type: String,
@@ -48,11 +58,12 @@ export default {
class="report-block-list-issue align-items-center"
data-qa-selector="report_item_row"
>
- <issue-status-icon
+ <component
+ :is="iconComponent"
v-if="showReportSectionStatusIcon"
:status="status"
:status-icon-size="statusIconSize"
- class="gl-mr-3"
+ class="gl-mr-2"
/>
<component :is="component" v-if="component" :issue="issue" :status="status" :is-new="isNew" />
diff --git a/app/assets/javascripts/reports/constants.js b/app/assets/javascripts/reports/constants.js
index acd90ebf1b1..7f7ea2adc0e 100644
--- a/app/assets/javascripts/reports/constants.js
+++ b/app/assets/javascripts/reports/constants.js
@@ -16,6 +16,7 @@ export const STATUS_NEUTRAL = 'neutral';
export const ICON_WARNING = 'warning';
export const ICON_SUCCESS = 'success';
export const ICON_NOTFOUND = 'notfound';
+export const ICON_PENDING = 'pending';
export const status = {
LOADING,
diff --git a/app/assets/javascripts/repository/components/blob_replace.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 91d7811eb6d..273825b996a 100644
--- a/app/assets/javascripts/repository/components/blob_replace.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -1,18 +1,22 @@
<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
import getRefMixin from '../mixins/get_ref';
+import DeleteBlobModal from './delete_blob_modal.vue';
import UploadBlobModal from './upload_blob_modal.vue';
export default {
i18n: {
replace: __('Replace'),
replacePrimaryBtnText: __('Replace file'),
+ delete: __('Delete'),
},
components: {
+ GlButtonGroup,
GlButton,
UploadBlobModal,
+ DeleteBlobModal,
},
directives: {
GlModal: GlModalDirective,
@@ -39,31 +43,50 @@ export default {
type: String,
required: true,
},
+ deletePath: {
+ type: String,
+ required: true,
+ },
canPushCode: {
type: Boolean,
required: true,
},
+ emptyRepo: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
replaceModalId() {
return uniqueId('replace-modal');
},
- title() {
+ replaceModalTitle() {
return sprintf(__('Replace %{name}'), { name: this.name });
},
+ deleteModalId() {
+ return uniqueId('delete-modal');
+ },
+ deleteModalTitle() {
+ return sprintf(__('Delete %{name}'), { name: this.name });
+ },
},
};
</script>
<template>
<div class="gl-mr-3">
- <gl-button v-gl-modal="replaceModalId">
- {{ $options.i18n.replace }}
- </gl-button>
+ <gl-button-group>
+ <gl-button v-gl-modal="replaceModalId">
+ {{ $options.i18n.replace }}
+ </gl-button>
+ <gl-button v-gl-modal="deleteModalId">
+ {{ $options.i18n.delete }}
+ </gl-button>
+ </gl-button-group>
<upload-blob-modal
:modal-id="replaceModalId"
- :modal-title="title"
- :commit-message="title"
+ :modal-title="replaceModalTitle"
+ :commit-message="replaceModalTitle"
:target-branch="targetBranch || ref"
:original-branch="originalBranch || ref"
:can-push-code="canPushCode"
@@ -71,5 +94,15 @@ export default {
:replace-path="replacePath"
:primary-btn-text="$options.i18n.replacePrimaryBtnText"
/>
+ <delete-blob-modal
+ :modal-id="deleteModalId"
+ :modal-title="deleteModalTitle"
+ :delete-path="deletePath"
+ :commit-message="deleteModalTitle"
+ :target-branch="targetBranch || ref"
+ :original-branch="originalBranch || ref"
+ :can-push-code="canPushCode"
+ :empty-repo="emptyRepo"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 7fbf331d585..09ac60c94c7 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -5,16 +5,19 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import blobInfoQuery from '../queries/blob_info.query.graphql';
-import BlobHeaderEdit from './blob_header_edit.vue';
-import BlobReplace from './blob_replace.vue';
+import BlobButtonGroup from './blob_button_group.vue';
+import BlobEdit from './blob_edit.vue';
+import { loadViewer, viewerProps } from './blob_viewers';
export default {
components: {
BlobHeader,
- BlobHeaderEdit,
- BlobReplace,
+ BlobEdit,
+ BlobButtonGroup,
BlobContent,
GlLoadingIcon,
},
@@ -31,9 +34,12 @@ export default {
this.switchViewer(
this.hasRichViewer && !window.location.hash ? RICH_BLOB_VIEWER : SIMPLE_BLOB_VIEWER,
);
+ if (this.hasRichViewer && !this.blobViewer) {
+ this.loadLegacyViewer();
+ }
},
error() {
- createFlash({ message: __('An error occurred while loading the file. Please try again.') });
+ this.displayError();
},
},
},
@@ -54,9 +60,16 @@ export default {
},
data() {
return {
+ legacyRichViewer: null,
+ isBinary: false,
+ isLoadingLegacyViewer: false,
activeViewerType: SIMPLE_BLOB_VIEWER,
project: {
+ userPermissions: {
+ pushCode: false,
+ },
repository: {
+ empty: true,
blobs: {
nodes: [
{
@@ -77,10 +90,10 @@ export default {
canLock: false,
isLocked: false,
lockLink: '',
- canModifyBlob: true,
forkPath: '',
simpleViewer: {},
richViewer: null,
+ webPath: '',
},
],
},
@@ -90,10 +103,10 @@ export default {
},
computed: {
isLoggedIn() {
- return Boolean(gon.current_user_id);
+ return isLoggedIn();
},
isLoading() {
- return this.$apollo.queries.project.loading;
+ return this.$apollo.queries.project.loading || this.isLoadingLegacyViewer;
},
blobInfo() {
const nodes = this.project?.repository?.blobs?.nodes;
@@ -110,8 +123,30 @@ export default {
hasRenderError() {
return Boolean(this.viewer.renderError);
},
+ blobViewer() {
+ const { fileType } = this.viewer;
+ return loadViewer(fileType);
+ },
+ viewerProps() {
+ const { fileType } = this.viewer;
+ return viewerProps(fileType, this.blobInfo);
+ },
},
methods: {
+ loadLegacyViewer() {
+ this.isLoadingLegacyViewer = true;
+ axios
+ .get(`${this.blobInfo.webPath}?format=json&viewer=rich`)
+ .then(({ data: { html, binary } }) => {
+ this.legacyRichViewer = html;
+ this.isBinary = binary;
+ this.isLoadingLegacyViewer = false;
+ })
+ .catch(() => this.displayError());
+ },
+ displayError() {
+ createFlash({ message: __('An error occurred while loading the file. Please try again.') });
+ },
switchViewer(newViewer) {
this.activeViewerType = newViewer || SIMPLE_BLOB_VIEWER;
},
@@ -121,36 +156,42 @@ export default {
<template>
<div>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<div v-if="blobInfo && !isLoading" class="file-holder">
<blob-header
:blob="blobInfo"
- :hide-viewer-switcher="!hasRichViewer"
+ :hide-viewer-switcher="!hasRichViewer || isBinary"
:active-viewer-type="viewer.type"
:has-render-error="hasRenderError"
@viewer-changed="switchViewer"
>
<template #actions>
- <blob-header-edit
+ <blob-edit
+ v-if="!isBinary"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
/>
- <blob-replace
+ <blob-button-group
v-if="isLoggedIn"
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
- :can-push-code="blobInfo.canModifyBlob"
+ :delete-path="blobInfo.webPath"
+ :can-push-code="project.userPermissions.pushCode"
+ :empty-repo="project.repository.empty"
/>
</template>
</blob-header>
<blob-content
+ v-if="!blobViewer"
+ :rich-viewer="legacyRichViewer"
:blob="blobInfo"
:content="blobInfo.rawTextBlob"
:is-raw-content="true"
:active-viewer="viewer"
:loading="false"
/>
+ <component :is="blobViewer" v-else v-bind="viewerProps" class="blob-viewer" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/repository/components/blob_header_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue
index 3d97ebe89e4..3d97ebe89e4 100644
--- a/app/assets/javascripts/repository/components/blob_header_edit.vue
+++ b/app/assets/javascripts/repository/components/blob_edit.vue
diff --git a/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue
new file mode 100644
index 00000000000..48fa33eb558
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/download_viewer.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlIcon, GlLink } from '@gitlab/ui';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ fileName: {
+ type: String,
+ required: true,
+ },
+ filePath: {
+ type: String,
+ required: true,
+ },
+ fileSize: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ computed: {
+ downloadFileSize() {
+ return numberToHumanSize(this.fileSize);
+ },
+ downloadText() {
+ if (this.fileSize > 0) {
+ return sprintf(__('Download (%{fileSizeReadable})'), {
+ fileSizeReadable: this.downloadFileSize,
+ });
+ }
+ return __('Download');
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-text-center gl-py-13 gl-bg-gray-50">
+ <gl-link :href="filePath" rel="nofollow" :download="fileName" target="_blank">
+ <div>
+ <gl-icon :size="16" name="download" class="gl-text-gray-900" />
+ </div>
+ <h4>{{ downloadText }}</h4>
+ </gl-link>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue
new file mode 100644
index 00000000000..53210cbcc93
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/empty_viewer.vue
@@ -0,0 +1,3 @@
+<template>
+ <div class="nothing-here-block">{{ __('Empty file') }}</div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
new file mode 100644
index 00000000000..4e16b16041f
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -0,0 +1,27 @@
+export const loadViewer = (type) => {
+ switch (type) {
+ case 'empty':
+ return () => import(/* webpackChunkName: 'blob_empty_viewer' */ './empty_viewer.vue');
+ case 'text':
+ return () => import(/* webpackChunkName: 'blob_text_viewer' */ './text_viewer.vue');
+ case 'download':
+ return () => import(/* webpackChunkName: 'blob_download_viewer' */ './download_viewer.vue');
+ default:
+ return null;
+ }
+};
+
+export const viewerProps = (type, blob) => {
+ return {
+ text: {
+ content: blob.rawTextBlob,
+ fileName: blob.name,
+ readOnly: true,
+ },
+ download: {
+ fileName: blob.name,
+ filePath: blob.rawPath,
+ fileSize: blob.rawSize,
+ },
+ }[type];
+};
diff --git a/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue b/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue
new file mode 100644
index 00000000000..57fc979a56e
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_viewers/text_viewer.vue
@@ -0,0 +1,25 @@
+<script>
+export default {
+ components: {
+ SourceEditor: () =>
+ import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
+ },
+ props: {
+ content: {
+ type: String,
+ required: true,
+ },
+ fileName: {
+ type: String,
+ required: true,
+ },
+ readOnly: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <source-editor :value="content" :file-name="fileName" :editor-options="{ readOnly }" />
+</template>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
new file mode 100644
index 00000000000..6599d99d7bd
--- /dev/null
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -0,0 +1,151 @@
+<script>
+import { GlModal, GlFormGroup, GlFormInput, GlFormTextarea, GlToggle } from '@gitlab/ui';
+import csrf from '~/lib/utils/csrf';
+import { __ } from '~/locale';
+import {
+ SECONDARY_OPTIONS_TEXT,
+ COMMIT_LABEL,
+ TARGET_BRANCH_LABEL,
+ TOGGLE_CREATE_MR_LABEL,
+} from '../constants';
+
+export default {
+ csrf,
+ components: {
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlToggle,
+ },
+ i18n: {
+ PRIMARY_OPTIONS_TEXT: __('Delete file'),
+ SECONDARY_OPTIONS_TEXT,
+ COMMIT_LABEL,
+ TARGET_BRANCH_LABEL,
+ TOGGLE_CREATE_MR_LABEL,
+ },
+ props: {
+ modalId: {
+ type: String,
+ required: true,
+ },
+ modalTitle: {
+ type: String,
+ required: true,
+ },
+ deletePath: {
+ type: String,
+ required: true,
+ },
+ commitMessage: {
+ type: String,
+ required: true,
+ },
+ targetBranch: {
+ type: String,
+ required: true,
+ },
+ originalBranch: {
+ type: String,
+ required: true,
+ },
+ canPushCode: {
+ type: Boolean,
+ required: true,
+ },
+ emptyRepo: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ commit: this.commitMessage,
+ target: this.targetBranch,
+ createNewMr: true,
+ error: '',
+ };
+ },
+ computed: {
+ primaryOptions() {
+ return {
+ text: this.$options.i18n.PRIMARY_OPTIONS_TEXT,
+ attributes: [
+ {
+ variant: 'danger',
+ loading: this.loading,
+ disabled: !this.formCompleted || this.loading,
+ },
+ ],
+ };
+ },
+ cancelOptions() {
+ return {
+ text: this.$options.i18n.SECONDARY_OPTIONS_TEXT,
+ attributes: [
+ {
+ disabled: this.loading,
+ },
+ ],
+ };
+ },
+ showCreateNewMrToggle() {
+ return this.canPushCode && this.target !== this.originalBranch;
+ },
+ formCompleted() {
+ return this.commit && this.target;
+ },
+ },
+ methods: {
+ submitForm(e) {
+ e.preventDefault(); // Prevent modal from closing
+ this.loading = true;
+ this.$refs.form.submit();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-modal
+ :modal-id="modalId"
+ :title="modalTitle"
+ :action-primary="primaryOptions"
+ :action-cancel="cancelOptions"
+ @primary="submitForm"
+ >
+ <form ref="form" :action="deletePath" method="post">
+ <input type="hidden" name="_method" value="delete" />
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ <template v-if="emptyRepo">
+ <!-- Once "empty_repo_upload_experiment" is made available, will need to add class 'js-branch-name'
+ Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335721 -->
+ <input type="hidden" name="branch_name" :value="originalBranch" />
+ </template>
+ <template v-else>
+ <input type="hidden" name="original_branch" :value="originalBranch" />
+ <!-- Once "push to branch" permission is made available, will need to add to conditional
+ Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 -->
+ <input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" />
+ <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message">
+ <gl-form-textarea v-model="commit" name="commit_message" :disabled="loading" />
+ </gl-form-group>
+ <gl-form-group
+ v-if="canPushCode"
+ :label="$options.i18n.TARGET_BRANCH_LABEL"
+ label-for="branch_name"
+ >
+ <gl-form-input v-model="target" :disabled="loading" name="branch_name" />
+ </gl-form-group>
+ <gl-toggle
+ v-if="showCreateNewMrToggle"
+ v-model="createNewMr"
+ :disabled="loading"
+ :label="$options.i18n.TOGGLE_CREATE_MR_LABEL"
+ />
+ </template>
+ </form>
+ </gl-modal>
+</template>
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index ca5711de49c..69eefc807d7 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -70,7 +70,7 @@ export default {
);
},
showParentRow() {
- return !this.isLoading && ['', '/'].indexOf(this.path) === -1;
+ return ['', '/'].indexOf(this.path) === -1;
},
},
methods: {
diff --git a/app/assets/javascripts/repository/components/table/row.vue b/app/assets/javascripts/repository/components/table/row.vue
index 62f863db871..82c18d13a6a 100644
--- a/app/assets/javascripts/repository/components/table/row.vue
+++ b/app/assets/javascripts/repository/components/table/row.vue
@@ -186,6 +186,8 @@ export default {
:is="linkComponent"
ref="link"
v-gl-hover-load="handlePreload"
+ v-gl-tooltip:tooltip-container
+ :title="fullPath"
:to="routerLinkTo"
:href="url"
:class="{
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index 794a8a85cc5..c861fb8dd06 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -1,8 +1,9 @@
<script>
import filesQuery from 'shared_queries/repository/files.query.graphql';
import createFlash from '~/flash';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __ } from '../../locale';
-import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT } from '../constants';
+import { TREE_PAGE_SIZE, TREE_INITIAL_FETCH_COUNT, TREE_PAGE_LIMIT } from '../constants';
import getRefMixin from '../mixins/get_ref';
import projectPathQuery from '../queries/project_path.query.graphql';
import { readmeFile } from '../utils/readme';
@@ -14,7 +15,7 @@ export default {
FileTable,
FilePreview,
},
- mixins: [getRefMixin],
+ mixins: [getRefMixin, glFeatureFlagMixin()],
apollo: {
projectPath: {
query: projectPathQuery,
@@ -36,6 +37,7 @@ export default {
return {
projectPath: '',
nextPageCursor: '',
+ pagesLoaded: 1,
entries: {
trees: [],
submodules: [],
@@ -44,16 +46,28 @@ export default {
isLoadingFiles: false,
isOverLimit: false,
clickedShowMore: false,
- pageSize: TREE_PAGE_SIZE,
fetchCounter: 0,
};
},
computed: {
+ pageSize() {
+ // we want to exponentially increase the page size to reduce the load on the frontend
+ const exponentialSize = (TREE_PAGE_SIZE / TREE_INITIAL_FETCH_COUNT) * (this.fetchCounter + 1);
+ return exponentialSize < TREE_PAGE_SIZE && this.glFeatures.increasePageSizeExponentially
+ ? exponentialSize
+ : TREE_PAGE_SIZE;
+ },
+ totalEntries() {
+ return Object.values(this.entries).flat().length;
+ },
readme() {
return readmeFile(this.entries.blobs);
},
+ pageLimitReached() {
+ return this.totalEntries / this.pagesLoaded >= TREE_PAGE_LIMIT;
+ },
hasShowMore() {
- return !this.clickedShowMore && this.fetchCounter === TREE_INITIAL_FETCH_COUNT;
+ return !this.clickedShowMore && this.pageLimitReached;
},
},
@@ -104,7 +118,7 @@ export default {
if (pageInfo?.hasNextPage) {
this.nextPageCursor = pageInfo.endCursor;
this.fetchCounter += 1;
- if (this.fetchCounter < TREE_INITIAL_FETCH_COUNT || this.clickedShowMore) {
+ if (!this.pageLimitReached || this.clickedShowMore) {
this.fetchFiles();
this.clickedShowMore = false;
}
@@ -127,6 +141,7 @@ export default {
},
handleShowMore() {
this.clickedShowMore = true;
+ this.pagesLoaded += 1;
this.fetchFiles();
},
},
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 7f065dbdf6d..df5a5ea6163 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -17,13 +17,15 @@ import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { trackFileUploadEvent } from '~/projects/upload_file_experiment_tracking';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import {
+ SECONDARY_OPTIONS_TEXT,
+ COMMIT_LABEL,
+ TARGET_BRANCH_LABEL,
+ TOGGLE_CREATE_MR_LABEL,
+} from '../constants';
const PRIMARY_OPTIONS_TEXT = __('Upload file');
-const SECONDARY_OPTIONS_TEXT = __('Cancel');
const MODAL_TITLE = __('Upload New File');
-const COMMIT_LABEL = __('Commit message');
-const TARGET_BRANCH_LABEL = __('Target branch');
-const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
const REMOVE_FILE_TEXT = __('Remove file');
const NEW_BRANCH_IN_FORK = __(
'A new branch will be created in your fork and a new merge request will be started.',
@@ -170,7 +172,7 @@ export default {
})
.catch(() => {
this.loading = false;
- createFlash(ERROR_MESSAGE);
+ createFlash({ message: ERROR_MESSAGE });
});
},
formData() {
diff --git a/app/assets/javascripts/repository/constants.js b/app/assets/javascripts/repository/constants.js
index 62d5d3db445..2d2faa8d9f3 100644
--- a/app/assets/javascripts/repository/constants.js
+++ b/app/assets/javascripts/repository/constants.js
@@ -1,4 +1,10 @@
-const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
+import { __ } from '~/locale';
+export const TREE_PAGE_LIMIT = 1000; // the maximum amount of items per page
export const TREE_PAGE_SIZE = 100; // the amount of items to be fetched per (batch) request
export const TREE_INITIAL_FETCH_COUNT = TREE_PAGE_LIMIT / TREE_PAGE_SIZE; // the amount of (batch) requests to make
+
+export const SECONDARY_OPTIONS_TEXT = __('Cancel');
+export const COMMIT_LABEL = __('Commit message');
+export const TARGET_BRANCH_LABEL = __('Target branch');
+export const TOGGLE_CREATE_MR_LABEL = __('Start a new merge request with these changes');
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index bfd9447d260..a8f263941e2 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -1,6 +1,10 @@
query getBlobInfo($projectPath: ID!, $filePath: String!) {
project(fullPath: $projectPath) {
+ userPermissions {
+ pushCode
+ }
repository {
+ empty
blobs(paths: [$filePath]) {
nodes {
webPath
@@ -15,7 +19,6 @@ query getBlobInfo($projectPath: ID!, $filePath: String!) {
storedExternally
rawPath
replacePath
- canModifyBlob
simpleViewer {
fileType
tooLarge
diff --git a/app/assets/javascripts/repository/queries/commit.fragment.graphql b/app/assets/javascripts/repository/queries/commit.fragment.graphql
index be6897b9a16..b046fc1f730 100644
--- a/app/assets/javascripts/repository/queries/commit.fragment.graphql
+++ b/app/assets/javascripts/repository/queries/commit.fragment.graphql
@@ -5,5 +5,6 @@ fragment TreeEntryCommit on LogTreeCommit {
committedDate
commitPath
fileName
+ filePath
type
}
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 6cdd89ad431..36f5e6f4ce1 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -2,8 +2,8 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
-import { fixTitle, hide } from '~/tooltips';
-import { deprecatedCreateFlash as flash } from './flash';
+import { hide } from '~/tooltips';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { sprintf, s__, __ } from './locale';
@@ -98,45 +98,15 @@ Sidebar.prototype.toggleTodo = function (e) {
this.todoUpdateDone(data);
})
.catch(() =>
- flash(
- sprintf(__('There was an error %{message} todo.'), {
+ createFlash({
+ message: sprintf(__('There was an error %{message} todo.'), {
message:
ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'),
}),
- ),
+ }),
);
};
-Sidebar.prototype.todoUpdateDone = function (data) {
- const deletePath = data.delete_path ? data.delete_path : null;
- const attrPrefix = deletePath ? 'mark' : 'todo';
- const $todoBtns = $('.js-issuable-todo');
-
- $(document).trigger('todo:toggle', data.count);
-
- $todoBtns.each((i, el) => {
- const $el = $(el);
- const $elText = $el.find('.js-issuable-todo-inner');
-
- $el
- .removeClass('is-loading')
- .enable()
- .attr('aria-label', $el.data(`${attrPrefix}Text`))
- .attr('title', $el.data(`${attrPrefix}Text`))
- .data('deletePath', deletePath);
-
- if ($el.hasClass('has-tooltip')) {
- fixTitle(el);
- }
-
- if (typeof $el.data('isCollapsed') !== 'undefined') {
- $elText.html($el.data(`${attrPrefix}Icon`));
- } else {
- $elText.text($el.data(`${attrPrefix}Text`));
- }
- });
-};
-
Sidebar.prototype.sidebarCollapseClicked = function (e) {
if ($(e.currentTarget).hasClass('dont-change-state')) {
return;
diff --git a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
index 7f9f796bdee..863f0ab995f 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -1,9 +1,11 @@
<script>
import { GlButton, GlButtonGroup, GlTooltipDirective } from '@gitlab/ui';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
-import deleteRunnerMutation from '~/runner/graphql/delete_runner.mutation.graphql';
+import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerUpdateMutation from '~/runner/graphql/runner_update.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
const i18n = {
I18N_EDIT: __('Edit'),
@@ -14,6 +16,7 @@ const i18n = {
};
export default {
+ name: 'RunnerActionsCell',
components: {
GlButton,
GlButtonGroup,
@@ -86,7 +89,7 @@ export default {
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
+ throw new Error(errors.join(' '));
}
} catch (e) {
this.onError(e);
@@ -109,7 +112,7 @@ export default {
runnerDelete: { errors },
},
} = await this.$apollo.mutate({
- mutation: deleteRunnerMutation,
+ mutation: runnerDeleteMutation,
variables: {
input: {
id: this.runner.id,
@@ -119,7 +122,7 @@ export default {
refetchQueries: ['getRunners'],
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
+ throw new Error(errors.join(' '));
}
} catch (e) {
this.onError(e);
@@ -129,9 +132,13 @@ export default {
},
onError(error) {
- // TODO Render errors when "delete" action is done
- // `active` toggle would not fail due to user input.
- throw error;
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
+ },
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
i18n,
diff --git a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
index b3ebdfd82e3..f186a8daf72 100644
--- a/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_type_cell.vue
@@ -32,11 +32,11 @@ export default {
<runner-type-badge :type="runnerType" size="sm" />
<gl-badge v-if="locked" variant="warning" size="sm">
- {{ __('locked') }}
+ {{ s__('Runners|locked') }}
</gl-badge>
<gl-badge v-if="paused" variant="danger" size="sm">
- {{ __('paused') }}
+ {{ s__('Runners|paused') }}
</gl-badge>
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/helpers/masked_value.vue b/app/assets/javascripts/runner/components/helpers/masked_value.vue
new file mode 100644
index 00000000000..feccb37de81
--- /dev/null
+++ b/app/assets/javascripts/runner/components/helpers/masked_value.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isMasked: true,
+ };
+ },
+ computed: {
+ label() {
+ if (this.isMasked) {
+ return __('Click to reveal');
+ }
+ return __('Click to hide');
+ },
+ icon() {
+ if (this.isMasked) {
+ return 'eye';
+ }
+ return 'eye-slash';
+ },
+ displayedValue() {
+ if (this.isMasked && this.value?.length) {
+ return '*'.repeat(this.value.length);
+ }
+ return this.value;
+ },
+ },
+ methods: {
+ toggleMasked() {
+ this.isMasked = !this.isMasked;
+ },
+ },
+};
+</script>
+<template>
+ <span
+ >{{ displayedValue }}
+ <gl-button
+ :aria-label="label"
+ :icon="icon"
+ class="gl-text-body!"
+ data-testid="toggle-masked"
+ variant="link"
+ @click="toggleMasked"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
index bec33ce2f44..e14b3b17fa8 100644
--- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
+++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue
@@ -1,9 +1,9 @@
<script>
-import { GlFilteredSearchToken } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
-import { __, s__ } from '~/locale';
+import { formatNumber, sprintf, __, s__ } from '~/locale';
import { OPERATOR_IS_ONLY } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
import {
STATUS_ACTIVE,
STATUS_PAUSED,
@@ -19,50 +19,9 @@ import {
CONTACTED_ASC,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
} from '../constants';
-
-const searchTokens = [
- {
- icon: 'status',
- title: __('Status'),
- type: PARAM_KEY_STATUS,
- token: GlFilteredSearchToken,
- // TODO Get more than one value when GraphQL API supports OR for "status"
- unique: true,
- options: [
- { value: STATUS_ACTIVE, title: s__('Runners|Active') },
- { value: STATUS_PAUSED, title: s__('Runners|Paused') },
- { value: STATUS_ONLINE, title: s__('Runners|Online') },
- { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
-
- // Added extra quotes in this title to avoid splitting this value:
- // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
- { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
- ],
- // TODO In principle we could support more complex search rules,
- // this can be added to a separate issue.
- operators: OPERATOR_IS_ONLY,
- },
-
- {
- icon: 'file-tree',
- title: __('Type'),
- type: PARAM_KEY_RUNNER_TYPE,
- token: GlFilteredSearchToken,
- // TODO Get more than one value when GraphQL API supports OR for "status"
- unique: true,
- options: [
- { value: INSTANCE_TYPE, title: s__('Runners|shared') },
- { value: GROUP_TYPE, title: s__('Runners|group') },
- { value: PROJECT_TYPE, title: s__('Runners|specific') },
- ],
- // TODO We should support more complex search rules,
- // search for multiple states (OR) or have NOT operators
- operators: OPERATOR_IS_ONLY,
- },
-
- // TODO Support tags
-];
+import TagToken from './search_tokens/tag_token.vue';
const sortOptions = [
{
@@ -95,6 +54,14 @@ export default {
return Array.isArray(val?.filters) && typeof val?.sort === 'string';
},
},
+ namespace: {
+ type: String,
+ required: true,
+ },
+ activeRunnersCount: {
+ type: Number,
+ required: true,
+ },
},
data() {
// filtered_search_bar_root.vue may mutate the inital
@@ -106,6 +73,62 @@ export default {
initialSortBy: sort,
};
},
+ computed: {
+ searchTokens() {
+ return [
+ {
+ icon: 'status',
+ title: __('Status'),
+ type: PARAM_KEY_STATUS,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: STATUS_ACTIVE, title: s__('Runners|Active') },
+ { value: STATUS_PAUSED, title: s__('Runners|Paused') },
+ { value: STATUS_ONLINE, title: s__('Runners|Online') },
+ { value: STATUS_OFFLINE, title: s__('Runners|Offline') },
+
+ // Added extra quotes in this title to avoid splitting this value:
+ // see: https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1438
+ { value: STATUS_NOT_CONNECTED, title: `"${s__('Runners|Not connected')}"` },
+ ],
+ // TODO In principle we could support more complex search rules,
+ // this can be added to a separate issue.
+ operators: OPERATOR_IS_ONLY,
+ },
+
+ {
+ icon: 'file-tree',
+ title: __('Type'),
+ type: PARAM_KEY_RUNNER_TYPE,
+ token: BaseToken,
+ unique: true,
+ options: [
+ { value: INSTANCE_TYPE, title: s__('Runners|instance') },
+ { value: GROUP_TYPE, title: s__('Runners|group') },
+ { value: PROJECT_TYPE, title: s__('Runners|project') },
+ ],
+ // TODO We should support more complex search rules,
+ // search for multiple states (OR) or have NOT operators
+ operators: OPERATOR_IS_ONLY,
+ },
+
+ {
+ icon: 'tag',
+ title: s__('Runners|Tags'),
+ type: PARAM_KEY_TAG,
+ token: TagToken,
+ recentTokenValuesStorageKey: `${this.namespace}-recent-tags`,
+ operators: OPERATOR_IS_ONLY,
+ },
+ ];
+ },
+ activeRunnersMessage() {
+ return sprintf(__('Runners currently online: %{active_runners_count}'), {
+ active_runners_count: formatNumber(this.activeRunnersCount),
+ });
+ },
+ },
methods: {
onFilter(filters) {
const { sort } = this.value;
@@ -127,19 +150,23 @@ export default {
},
},
sortOptions,
- searchTokens,
};
</script>
<template>
- <filtered-search
- v-bind="$attrs"
- recent-searches-storage-key="runners-search"
- :sort-options="$options.sortOptions"
- :initial-filter-value="initialFilterValue"
- :initial-sort-by="initialSortBy"
- :tokens="$options.searchTokens"
- :search-input-placeholder="__('Search or filter results...')"
- @onFilter="onFilter"
- @onSort="onSort"
- />
+ <div>
+ <filtered-search
+ v-bind="$attrs"
+ :namespace="namespace"
+ recent-searches-storage-key="runners-search"
+ :sort-options="$options.sortOptions"
+ :initial-filter-value="initialFilterValue"
+ :initial-sort-by="initialSortBy"
+ :tokens="searchTokens"
+ :search-input-placeholder="__('Search or filter results...')"
+ data-testid="runners-filtered-search"
+ @onFilter="onFilter"
+ @onSort="onSort"
+ />
+ <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
+ </div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_list.vue b/app/assets/javascripts/runner/components/runner_list.vue
index 41adbbb55f6..69a1f106ca8 100644
--- a/app/assets/javascripts/runner/components/runner_list.vue
+++ b/app/assets/javascripts/runner/components/runner_list.vue
@@ -1,8 +1,9 @@
<script>
import { GlTable, GlTooltipDirective, GlSkeletonLoader } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { formatNumber, sprintf, __, s__ } from '~/locale';
+import { formatNumber, __, s__ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { RUNNER_JOB_COUNT_LIMIT } from '../constants';
import RunnerActionsCell from './cells/runner_actions_cell.vue';
import RunnerNameCell from './cells/runner_name_cell.vue';
import RunnerTypeCell from './cells/runner_type_cell.vue';
@@ -51,19 +52,20 @@ export default {
type: Array,
required: true,
},
- activeRunnersCount: {
- type: Number,
- required: true,
- },
- },
- computed: {
- activeRunnersMessage() {
- return sprintf(__('Runners currently online: %{active_runners_count}'), {
- active_runners_count: formatNumber(this.activeRunnersCount),
- });
- },
},
methods: {
+ formatProjectCount(projectCount) {
+ if (projectCount === null) {
+ return __('n/a');
+ }
+ return formatNumber(projectCount);
+ },
+ formatJobCount(jobCount) {
+ if (jobCount > RUNNER_JOB_COUNT_LIMIT) {
+ return `${formatNumber(RUNNER_JOB_COUNT_LIMIT)}+`;
+ }
+ return formatNumber(jobCount);
+ },
runnerTrAttr(runner) {
if (runner) {
return {
@@ -88,12 +90,12 @@ export default {
</script>
<template>
<div>
- <div class="gl-text-right" data-testid="active-runners-message">{{ activeRunnersMessage }}</div>
<gl-table
:busy="loading"
:items="runners"
:fields="$options.fields"
:tbody-tr-attr="runnerTrAttr"
+ data-testid="runner-list"
stacked="md"
fixed
>
@@ -117,12 +119,12 @@ export default {
{{ ipAddress }}
</template>
- <template #cell(projectCount)>
- <!-- TODO add projects count -->
+ <template #cell(projectCount)="{ item: { projectCount } }">
+ {{ formatProjectCount(projectCount) }}
</template>
- <template #cell(jobCount)>
- <!-- TODO add jobs count -->
+ <template #cell(jobCount)="{ item: { jobCount } }">
+ {{ formatJobCount(jobCount) }}
</template>
<template #cell(tagList)="{ item: { tagList } }">
diff --git a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
index 426d377c92b..475d362bb52 100644
--- a/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
+++ b/app/assets/javascripts/runner/components/runner_manual_setup_help.vue
@@ -1,6 +1,7 @@
<script>
import { GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
import { s__ } from '~/locale';
+import MaskedValue from '~/runner/components/helpers/masked_value.vue';
import RunnerRegistrationTokenReset from '~/runner/components/runner_registration_token_reset.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
@@ -11,6 +12,7 @@ export default {
GlLink,
GlSprintf,
ClipboardButton,
+ MaskedValue,
RunnerInstructions,
RunnerRegistrationTokenReset,
},
@@ -92,7 +94,9 @@ export default {
{{ __('And this registration token:') }}
<br />
- <code data-testid="registration-token">{{ currentRegistrationToken }}</code>
+ <code data-testid="registration-token"
+ ><masked-value :value="currentRegistrationToken"
+ /></code>
<clipboard-button :title="__('Copy token')" :text="currentRegistrationToken" />
</li>
</ol>
diff --git a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
index b03574264d9..2335faa4f85 100644
--- a/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
+++ b/app/assets/javascripts/runner/components/runner_registration_token_reset.vue
@@ -3,9 +3,11 @@ import { GlButton } from '@gitlab/ui';
import createFlash, { FLASH_TYPES } from '~/flash';
import { __, s__ } from '~/locale';
import runnersRegistrationTokenResetMutation from '~/runner/graphql/runners_registration_token_reset.mutation.graphql';
+import { captureException } from '~/runner/sentry_utils';
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
export default {
+ name: 'RunnerRegistrationTokenReset',
components: {
GlButton,
},
@@ -52,8 +54,7 @@ export default {
},
});
if (errors && errors.length) {
- this.onError(new Error(errors[0]));
- return;
+ throw new Error(errors.join(' '));
}
this.onSuccess(token);
} catch (e) {
@@ -65,6 +66,8 @@ export default {
onError(error) {
const { message } = error;
createFlash({ message });
+
+ this.reportToSentry(error);
},
onSuccess(token) {
createFlash({
@@ -73,6 +76,9 @@ export default {
});
this.$emit('tokenReset', token);
},
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
};
</script>
diff --git a/app/assets/javascripts/runner/components/runner_tag.vue b/app/assets/javascripts/runner/components/runner_tag.vue
new file mode 100644
index 00000000000..06562e618a8
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_tag.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { RUNNER_TAG_BADGE_VARIANT } from '../constants';
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ tag: {
+ type: String,
+ required: true,
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+ RUNNER_TAG_BADGE_VARIANT,
+};
+</script>
+<template>
+ <gl-badge :size="size" :variant="$options.RUNNER_TAG_BADGE_VARIANT">
+ {{ tag }}
+ </gl-badge>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_tags.vue b/app/assets/javascripts/runner/components/runner_tags.vue
index 4ba07e00c96..aec0d8e2c66 100644
--- a/app/assets/javascripts/runner/components/runner_tags.vue
+++ b/app/assets/javascripts/runner/components/runner_tags.vue
@@ -1,9 +1,9 @@
<script>
-import { GlBadge } from '@gitlab/ui';
+import RunnerTag from './runner_tag.vue';
export default {
components: {
- GlBadge,
+ RunnerTag,
},
props: {
tagList: {
@@ -16,18 +16,11 @@ export default {
required: false,
default: 'md',
},
- variant: {
- type: String,
- required: false,
- default: 'info',
- },
},
};
</script>
<template>
<div>
- <gl-badge v-for="tag in tagList" :key="tag" :size="size" :variant="variant">
- {{ tag }}
- </gl-badge>
+ <runner-tag v-for="tag in tagList" :key="tag" :tag="tag" :size="size" />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_type_help.vue b/app/assets/javascripts/runner/components/runner_type_help.vue
index 927deb290a4..70456b3ab65 100644
--- a/app/assets/javascripts/runner/components/runner_type_help.vue
+++ b/app/assets/javascripts/runner/components/runner_type_help.vue
@@ -44,13 +44,13 @@ export default {
</li>
<li>
<gl-badge variant="warning" size="sm">
- {{ __('locked') }}
+ {{ s__('Runners|locked') }}
</gl-badge>
- {{ __('Cannot be assigned to other projects.') }}
</li>
<li>
<gl-badge variant="danger" size="sm">
- {{ __('paused') }}
+ {{ s__('Runners|paused') }}
</gl-badge>
- {{ __('Not available to run jobs.') }}
</li>
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 0c1b83b6830..85d14547efd 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -7,42 +7,26 @@ import {
GlFormInputGroup,
GlTooltipDirective,
} from '@gitlab/ui';
+import {
+ modelToUpdateMutationVariables,
+ runnerToModel,
+} from 'ee_else_ce/runner/runner_details/runner_update_form_utils';
import createFlash, { FLASH_TYPES } from '~/flash';
import { __ } from '~/locale';
+import { captureException } from '~/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
import runnerUpdateMutation from '../graphql/runner_update.mutation.graphql';
-const runnerToModel = (runner) => {
- const {
- id,
- description,
- maximumTimeout,
- accessLevel,
- active,
- locked,
- runUntagged,
- tagList = [],
- } = runner || {};
-
- return {
- id,
- description,
- maximumTimeout,
- accessLevel,
- active,
- locked,
- runUntagged,
- tagList: tagList.join(', '),
- };
-};
-
export default {
+ name: 'RunnerUpdateForm',
components: {
GlButton,
GlForm,
GlFormCheckbox,
GlFormGroup,
GlFormInputGroup,
+ RunnerUpdateCostFactorFields: () =>
+ import('ee_component/runner/components/runner_update_cost_factor_fields.vue'),
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -67,18 +51,6 @@ export default {
readonlyIpAddress() {
return this.runner?.ipAddress;
},
- updateMutationInput() {
- const { maximumTimeout, tagList } = this.model;
-
- return {
- ...this.model,
- maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null,
- tagList: tagList
- .split(',')
- .map((tag) => tag.trim())
- .filter((tag) => Boolean(tag)),
- };
- },
},
watch: {
runner(newVal, oldVal) {
@@ -98,31 +70,32 @@ export default {
},
} = await this.$apollo.mutate({
mutation: runnerUpdateMutation,
- variables: {
- input: this.updateMutationInput,
- },
+ variables: modelToUpdateMutationVariables(this.model),
});
if (errors?.length) {
- this.onError(new Error(errors[0]));
+ // Validation errors need not be thrown
+ createFlash({ message: errors[0] });
return;
}
this.onSuccess();
- } catch (e) {
- this.onError(e);
+ } catch (error) {
+ const { message } = error;
+ createFlash({ message });
+
+ this.reportToSentry(error);
} finally {
this.saving = false;
}
},
- onError(error) {
- const { message } = error;
- createFlash({ message });
- },
onSuccess() {
createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS });
this.model = runnerToModel(this.runner);
},
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
+ },
},
ACCESS_LEVEL_NOT_PROTECTED,
ACCESS_LEVEL_REF_PROTECTED,
@@ -213,6 +186,8 @@ export default {
<gl-form-input-group v-model="model.tagList" />
</gl-form-group>
+ <runner-update-cost-factor-fields v-model="model" />
+
<div class="form-actions">
<gl-button
type="submit"
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
new file mode 100644
index 00000000000..0c69072f06a
--- /dev/null
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -0,0 +1,91 @@
+<script>
+import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { s__ } from '~/locale';
+
+import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
+import { RUNNER_TAG_BG_CLASS } from '../../constants';
+
+export const TAG_SUGGESTIONS_PATH = '/admin/runners/tag_list.json';
+
+export default {
+ components: {
+ BaseToken,
+ GlFilteredSearchSuggestion,
+ GlToken,
+ },
+ props: {
+ config: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tags: [],
+ loading: false,
+ };
+ },
+ methods: {
+ fnCurrentTokenValue(data) {
+ // By default, values are transformed with `toLowerCase`
+ // however, runner tags are case sensitive.
+ return data;
+ },
+ getTagsOptions(search) {
+ // TODO This should be implemented via a GraphQL API
+ // The API should
+ // 1) scope to the rights of the user
+ // 2) stay up to date to the removal of old tags
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333796
+ return axios
+ .get(TAG_SUGGESTIONS_PATH, {
+ params: {
+ search,
+ },
+ })
+ .then(({ data }) => {
+ return data.map(({ id, name }) => ({ id, value: name, text: name }));
+ });
+ },
+ async fetchTags(searchTerm) {
+ this.loading = true;
+ try {
+ this.tags = await this.getTagsOptions(searchTerm);
+ } catch {
+ createFlash({
+ message: s__('Runners|Something went wrong while fetching the tags suggestions'),
+ });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+ RUNNER_TAG_BG_CLASS,
+};
+</script>
+
+<template>
+ <base-token
+ v-bind="$attrs"
+ :config="config"
+ :suggestions-loading="loading"
+ :suggestions="tags"
+ :fn-current-token-value="fnCurrentTokenValue"
+ :recent-suggestions-storage-key="config.recentTokenValuesStorageKey"
+ @fetch-suggestions="fetchTags"
+ v-on="$listeners"
+ >
+ <template #view-token="{ viewTokenProps: { listeners, inputValue, activeTokenValue } }">
+ <gl-token variant="search-value" :class="$options.RUNNER_TAG_BG_CLASS" v-on="listeners">
+ {{ activeTokenValue ? activeTokenValue.text : inputValue }}
+ </gl-token>
+ </template>
+ <template #suggestions-list="{ suggestions }">
+ <gl-filtered-search-suggestion v-for="tag in suggestions" :key="tag.id" :value="tag.value">
+ {{ tag.text }}
+ </gl-filtered-search-suggestion>
+ </template>
+ </base-token>
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index a57d18ba745..2822882e0cc 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -1,18 +1,23 @@
import { s__ } from '~/locale';
export const RUNNER_PAGE_SIZE = 20;
+export const RUNNER_JOB_COUNT_LIMIT = 1000;
+export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.');
export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}');
-export const RUNNER_ENTITY_TYPE = 'Ci::Runner';
+export const RUNNER_TAG_BADGE_VARIANT = 'info';
+export const RUNNER_TAG_BG_CLASS = 'gl-bg-blue-100';
// Filtered search parameter names
// - Used for URL params names
// - GlFilteredSearch tokens type
-export const PARAM_KEY_SEARCH = 'search';
export const PARAM_KEY_STATUS = 'status';
export const PARAM_KEY_RUNNER_TYPE = 'runner_type';
+export const PARAM_KEY_TAG = 'tag';
+export const PARAM_KEY_SEARCH = 'search';
+
export const PARAM_KEY_SORT = 'sort';
export const PARAM_KEY_PAGE = 'page';
export const PARAM_KEY_AFTER = 'after';
diff --git a/app/assets/javascripts/runner/graphql/get_runner.query.graphql b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
index 84e0d6cc95c..c294cb9bf22 100644
--- a/app/assets/javascripts/runner/graphql/get_runner.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runner.query.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/runner_details.fragment.graphql"
+#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
query getRunner($id: CiRunnerID!) {
runner(id: $id) {
diff --git a/app/assets/javascripts/runner/graphql/get_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
index 45df9c625a6..9f837197558 100644
--- a/app/assets/javascripts/runner/graphql/get_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
@@ -6,9 +6,10 @@ query getRunners(
$after: String
$first: Int
$last: Int
- $search: String
$status: CiRunnerStatus
$type: CiRunnerType
+ $tagList: [String!]
+ $search: String
$sort: CiRunnerSort
) {
runners(
@@ -16,9 +17,10 @@ query getRunners(
after: $after
first: $first
last: $last
- search: $search
status: $status
type: $type
+ tagList: $tagList
+ search: $search
sort: $sort
) {
nodes {
diff --git a/app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql
index d580ea2785e..d580ea2785e 100644
--- a/app/assets/javascripts/runner/graphql/delete_runner.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_delete.mutation.graphql
diff --git a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
index 6d7dc1e2798..2449ee0fc0f 100644
--- a/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_details.fragment.graphql
@@ -1,12 +1,5 @@
+#import "./runner_details_shared.fragment.graphql"
+
fragment RunnerDetails on CiRunner {
- id
- runnerType
- active
- accessLevel
- runUntagged
- locked
- ipAddress
- description
- maximumTimeout
- tagList
+ ...RunnerDetailsShared
}
diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
new file mode 100644
index 00000000000..8c50cba7de3
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
@@ -0,0 +1,12 @@
+fragment RunnerDetailsShared on CiRunner {
+ id
+ runnerType
+ active
+ accessLevel
+ runUntagged
+ locked
+ ipAddress
+ description
+ maximumTimeout
+ tagList
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 0835e3c7c09..68d6f02f799 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -10,4 +10,6 @@ fragment RunnerNode on CiRunner {
locked
tagList
contactedAt
+ jobCount
+ projectCount
}
diff --git a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
index d50c1880d77..dcc7fdf24f1 100644
--- a/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_update.mutation.graphql
@@ -1,4 +1,4 @@
-#import "~/runner/graphql/runner_details.fragment.graphql"
+#import "ee_else_ce/runner/graphql/runner_details.fragment.graphql"
mutation runnerUpdate($input: RunnerUpdateInput!) {
runnerUpdate(input: $input) {
diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
index 5d5fa81b851..6557a7834e7 100644
--- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue
+++ b/app/assets/javascripts/runner/runner_details/runner_details_app.vue
@@ -1,20 +1,22 @@
<script>
+import createFlash from '~/flash';
+import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { sprintf } from '~/locale';
import RunnerTypeAlert from '../components/runner_type_alert.vue';
import RunnerTypeBadge from '../components/runner_type_badge.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
-import { I18N_DETAILS_TITLE, RUNNER_ENTITY_TYPE } from '../constants';
+import { I18N_DETAILS_TITLE, I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
+import { captureException } from '../sentry_utils';
export default {
+ name: 'RunnerDetailsApp',
components: {
RunnerTypeAlert,
RunnerTypeBadge,
RunnerUpdateForm,
},
- i18n: {
- I18N_DETAILS_TITLE,
- },
props: {
runnerId: {
type: String,
@@ -31,9 +33,27 @@ export default {
query: getRunnerQuery,
variables() {
return {
- id: convertToGraphQLId(RUNNER_ENTITY_TYPE, this.runnerId),
+ id: convertToGraphQLId(TYPE_CI_RUNNER, this.runnerId),
};
},
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
+ },
+ },
+ },
+ computed: {
+ pageTitle() {
+ return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId });
+ },
+ },
+ errorCaptured(error) {
+ this.reportToSentry(error);
+ },
+ methods: {
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
};
@@ -41,9 +61,7 @@ export default {
<template>
<div>
<h2 class="page-title">
- {{ sprintf($options.i18n.I18N_DETAILS_TITLE, { runner_id: runnerId }) }}
-
- <runner-type-badge v-if="runner" :type="runner.runnerType" />
+ {{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" />
</h2>
<runner-type-alert v-if="runner" :type="runner.runnerType" />
diff --git a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js
new file mode 100644
index 00000000000..3b519fa7d71
--- /dev/null
+++ b/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js
@@ -0,0 +1,38 @@
+export const runnerToModel = (runner) => {
+ const {
+ id,
+ description,
+ maximumTimeout,
+ accessLevel,
+ active,
+ locked,
+ runUntagged,
+ tagList = [],
+ } = runner || {};
+
+ return {
+ id,
+ description,
+ maximumTimeout,
+ accessLevel,
+ active,
+ locked,
+ runUntagged,
+ tagList: tagList.join(', '),
+ };
+};
+
+export const modelToUpdateMutationVariables = (model) => {
+ const { maximumTimeout, tagList } = model;
+
+ return {
+ input: {
+ ...model,
+ maximumTimeout: maximumTimeout !== '' ? maximumTimeout : null,
+ tagList: tagList
+ ?.split(',')
+ .map((tag) => tag.trim())
+ .filter((tag) => Boolean(tag)),
+ },
+ };
+};
diff --git a/app/assets/javascripts/runner/runner_list/index.js b/app/assets/javascripts/runner/runner_list/index.js
index 5eba14a7948..16616f00d1e 100644
--- a/app/assets/javascripts/runner/runner_list/index.js
+++ b/app/assets/javascripts/runner/runner_list/index.js
@@ -12,7 +12,8 @@ export const initRunnerList = (selector = '#js-runner-list') => {
return null;
}
- // TODO `activeRunnersCount` should be implemented using a GraphQL API.
+ // TODO `activeRunnersCount` should be implemented using a GraphQL API
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
const { activeRunnersCount, registrationToken, runnerInstallHelpPage } = el.dataset;
const apolloProvider = new VueApollo({
diff --git a/app/assets/javascripts/runner/runner_list/runner_list_app.vue b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
index 7f3a980ccca..8d39243d609 100644
--- a/app/assets/javascripts/runner/runner_list/runner_list_app.vue
+++ b/app/assets/javascripts/runner/runner_list/runner_list_app.vue
@@ -1,5 +1,5 @@
<script>
-import * as Sentry from '@sentry/browser';
+import createFlash from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
@@ -7,8 +7,9 @@ import RunnerList from '../components/runner_list.vue';
import RunnerManualSetupHelp from '../components/runner_manual_setup_help.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeHelp from '../components/runner_type_help.vue';
-import { INSTANCE_TYPE } from '../constants';
+import { INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
+import { captureException } from '../sentry_utils';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
@@ -16,6 +17,7 @@ import {
} from './runner_search_utils';
export default {
+ name: 'RunnerListApp',
components: {
RunnerFilteredSearchBar,
RunnerList,
@@ -59,8 +61,10 @@ export default {
pageInfo: runners?.pageInfo || {},
};
},
- error(err) {
- this.captureException(err);
+ error(error) {
+ createFlash({ message: I18N_FETCH_ERROR });
+
+ this.reportToSentry(error);
},
},
},
@@ -87,15 +91,12 @@ export default {
},
},
},
- errorCaptured(err) {
- this.captureException(err);
+ errorCaptured(error) {
+ this.reportToSentry(error);
},
methods: {
- captureException(err) {
- Sentry.withScope((scope) => {
- scope.setTag('component', 'runner_list_app');
- Sentry.captureException(err);
- });
+ reportToSentry(error) {
+ captureException({ error, component: this.$options.name });
},
},
INSTANCE_TYPE,
@@ -115,17 +116,17 @@ export default {
</div>
</div>
- <runner-filtered-search-bar v-model="search" namespace="admin_runners" />
+ <runner-filtered-search-bar
+ v-model="search"
+ namespace="admin_runners"
+ :active-runners-count="activeRunnersCount"
+ />
<div v-if="noRunnersFound" class="gl-text-center gl-p-5">
{{ __('No runners found') }}
</div>
<template v-else>
- <runner-list
- :runners="runners.items"
- :loading="runnersLoading"
- :active-runners-count="activeRunnersCount"
- />
+ <runner-list :runners="runners.items" :loading="runnersLoading" />
<runner-pagination v-model="search.pagination" :page-info="runners.pageInfo" />
</template>
</div>
diff --git a/app/assets/javascripts/runner/runner_list/runner_search_utils.js b/app/assets/javascripts/runner/runner_list/runner_search_utils.js
index e45972b81db..9a0dc9c3a32 100644
--- a/app/assets/javascripts/runner/runner_list/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_list/runner_search_utils.js
@@ -6,9 +6,10 @@ import {
prepareTokens,
} from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
import {
- PARAM_KEY_SEARCH,
PARAM_KEY_STATUS,
PARAM_KEY_RUNNER_TYPE,
+ PARAM_KEY_TAG,
+ PARAM_KEY_SEARCH,
PARAM_KEY_SORT,
PARAM_KEY_PAGE,
PARAM_KEY_AFTER,
@@ -40,7 +41,7 @@ export const fromUrlQueryToSearch = (query = window.location.search) => {
return {
filters: prepareTokens(
urlQueryToFilter(query, {
- filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE],
+ filterNamesAllowList: [PARAM_KEY_STATUS, PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG],
filteredSearchTermKey: PARAM_KEY_SEARCH,
legacySpacesDecode: false,
}),
@@ -56,15 +57,19 @@ export const fromSearchToUrl = (
) => {
const filterParams = {
// Defaults
- [PARAM_KEY_SEARCH]: null,
[PARAM_KEY_STATUS]: [],
[PARAM_KEY_RUNNER_TYPE]: [],
+ [PARAM_KEY_TAG]: [],
// Current filters
...filterToQueryObject(processFilters(filters), {
filteredSearchTermKey: PARAM_KEY_SEARCH,
}),
};
+ if (!filterParams[PARAM_KEY_SEARCH]) {
+ filterParams[PARAM_KEY_SEARCH] = null;
+ }
+
const isDefaultSort = sort !== DEFAULT_SORT;
const isFirstPage = pagination?.page === 1;
const otherParams = {
@@ -87,12 +92,12 @@ export const fromSearchToVariables = ({ filters = [], sort = null, pagination =
variables.search = queryObj[PARAM_KEY_SEARCH];
- // TODO Get more than one value when GraphQL API supports OR for "status"
+ // TODO Get more than one value when GraphQL API supports OR for "status" or "runner_type"
[variables.status] = queryObj[PARAM_KEY_STATUS] || [];
-
- // TODO Get more than one value when GraphQL API supports OR for "runner type"
[variables.type] = queryObj[PARAM_KEY_RUNNER_TYPE] || [];
+ variables.tagList = queryObj[PARAM_KEY_TAG];
+
if (sort) {
variables.sort = sort;
}
diff --git a/app/assets/javascripts/runner/sentry_utils.js b/app/assets/javascripts/runner/sentry_utils.js
new file mode 100644
index 00000000000..29de1f9adae
--- /dev/null
+++ b/app/assets/javascripts/runner/sentry_utils.js
@@ -0,0 +1,20 @@
+import * as Sentry from '@sentry/browser';
+
+const COMPONENT_TAG = 'vue_component';
+
+/**
+ * Captures an error in a Vue component and sends it
+ * to Sentry
+ *
+ * @param {Object} options
+ * @param {Error} options.error - Exception or error
+ * @param {String} options.component - Component name in CamelCase format
+ */
+export const captureException = ({ error, component }) => {
+ Sentry.withScope((scope) => {
+ if (component) {
+ scope.setTag(COMPONENT_TAG, component);
+ }
+ Sentry.captureException(error);
+ });
+};
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 0c3f273fec7..b53557c0ec5 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -2,11 +2,13 @@ import Api from '~/api';
import createFlash from '~/flash';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
import * as types from './mutation_types';
+import { loadDataFromLS, setFrequentItemToLS, mergeById } from './utils';
export const fetchGroups = ({ commit }, search) => {
commit(types.REQUEST_GROUPS);
- Api.groups(search)
+ Api.groups(search, { order_by: 'similarity' })
.then((data) => {
commit(types.RECEIVE_GROUPS_SUCCESS, data);
})
@@ -30,7 +32,12 @@ export const fetchProjects = ({ commit, state }, search) => {
if (groupId) {
// TODO (https://gitlab.com/gitlab-org/gitlab/-/issues/323331): For errors `createFlash` is called twice; in `callback` and in `Api.groupProjects`
- Api.groupProjects(groupId, search, {}, callback);
+ Api.groupProjects(
+ groupId,
+ search,
+ { order_by: 'similarity', with_shared: false, include_subgroups: true },
+ callback,
+ );
} else {
// The .catch() is due to the API method not handling a rejection properly
Api.projects(search, { order_by: 'id' }, callback).catch(() => {
@@ -39,6 +46,40 @@ export const fetchProjects = ({ commit, state }, search) => {
}
};
+export const loadFrequentGroups = async ({ commit }) => {
+ const data = loadDataFromLS(GROUPS_LOCAL_STORAGE_KEY);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data });
+
+ const promises = data.map((d) => Api.group(d.id));
+ try {
+ const inflatedData = mergeById(await Promise.all(promises), data);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: GROUPS_LOCAL_STORAGE_KEY, data: inflatedData });
+ } catch {
+ createFlash({ message: __('There was a problem fetching recent groups.') });
+ }
+};
+
+export const loadFrequentProjects = async ({ commit }) => {
+ const data = loadDataFromLS(PROJECTS_LOCAL_STORAGE_KEY);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data });
+
+ const promises = data.map((d) => Api.project(d.id).then((res) => res.data));
+ try {
+ const inflatedData = mergeById(await Promise.all(promises), data);
+ commit(types.LOAD_FREQUENT_ITEMS, { key: PROJECTS_LOCAL_STORAGE_KEY, data: inflatedData });
+ } catch {
+ createFlash({ message: __('There was a problem fetching recent projects.') });
+ }
+};
+
+export const setFrequentGroup = ({ state }, item) => {
+ setFrequentItemToLS(GROUPS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+};
+
+export const setFrequentProject = ({ state }, item) => {
+ setFrequentItemToLS(PROJECTS_LOCAL_STORAGE_KEY, state.frequentItems, item);
+};
+
export const setQuery = ({ commit }, { key, value }) => {
commit(types.SET_QUERY, { key, value });
};
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
new file mode 100644
index 00000000000..3abf7cac6ba
--- /dev/null
+++ b/app/assets/javascripts/search/store/constants.js
@@ -0,0 +1,7 @@
+export const MAX_FREQUENT_ITEMS = 5;
+
+export const MAX_FREQUENCY = 5;
+
+export const GROUPS_LOCAL_STORAGE_KEY = 'global-search-frequent-groups';
+
+export const PROJECTS_LOCAL_STORAGE_KEY = 'global-search-frequent-projects';
diff --git a/app/assets/javascripts/search/store/getters.js b/app/assets/javascripts/search/store/getters.js
new file mode 100644
index 00000000000..650af5fa55a
--- /dev/null
+++ b/app/assets/javascripts/search/store/getters.js
@@ -0,0 +1,9 @@
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
+
+export const frequentGroups = (state) => {
+ return state.frequentItems[GROUPS_LOCAL_STORAGE_KEY];
+};
+
+export const frequentProjects = (state) => {
+ return state.frequentItems[PROJECTS_LOCAL_STORAGE_KEY];
+};
diff --git a/app/assets/javascripts/search/store/index.js b/app/assets/javascripts/search/store/index.js
index 1923c8b96ab..4fa88822722 100644
--- a/app/assets/javascripts/search/store/index.js
+++ b/app/assets/javascripts/search/store/index.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
+import * as getters from './getters';
import mutations from './mutations';
import createState from './state';
@@ -8,6 +9,7 @@ Vue.use(Vuex);
export const getStoreConfig = ({ query }) => ({
actions,
+ getters,
mutations,
state: createState({ query }),
});
diff --git a/app/assets/javascripts/search/store/mutation_types.js b/app/assets/javascripts/search/store/mutation_types.js
index a6430b53c4f..5c1c29dc738 100644
--- a/app/assets/javascripts/search/store/mutation_types.js
+++ b/app/assets/javascripts/search/store/mutation_types.js
@@ -7,3 +7,5 @@ export const RECEIVE_PROJECTS_SUCCESS = 'RECEIVE_PROJECTS_SUCCESS';
export const RECEIVE_PROJECTS_ERROR = 'RECEIVE_PROJECTS_ERROR';
export const SET_QUERY = 'SET_QUERY';
+
+export const LOAD_FREQUENT_ITEMS = 'LOAD_FREQUENT_ITEMS';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index 91d7cf66c8f..63156a89738 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -26,4 +26,7 @@ export default {
[types.SET_QUERY](state, { key, value }) {
state.query[key] = value;
},
+ [types.LOAD_FREQUENT_ITEMS](state, { key, data }) {
+ state.frequentItems[key] = data;
+ },
};
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index 9a0d61d0b93..5b1429ccc97 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,8 +1,14 @@
+import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
+
const createState = ({ query }) => ({
query,
groups: [],
fetchingGroups: false,
projects: [],
fetchingProjects: false,
+ frequentItems: {
+ [GROUPS_LOCAL_STORAGE_KEY]: [],
+ [PROJECTS_LOCAL_STORAGE_KEY]: [],
+ },
});
export default createState;
diff --git a/app/assets/javascripts/search/store/utils.js b/app/assets/javascripts/search/store/utils.js
new file mode 100644
index 00000000000..60c09221ca9
--- /dev/null
+++ b/app/assets/javascripts/search/store/utils.js
@@ -0,0 +1,80 @@
+import AccessorUtilities from '../../lib/utils/accessor';
+import { MAX_FREQUENT_ITEMS, MAX_FREQUENCY } from './constants';
+
+function extractKeys(object, keyList) {
+ return Object.fromEntries(keyList.map((key) => [key, object[key]]));
+}
+
+export const loadDataFromLS = (key) => {
+ if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ return [];
+ }
+
+ try {
+ return JSON.parse(localStorage.getItem(key)) || [];
+ } catch {
+ // The LS got in a bad state, let's wipe it
+ localStorage.removeItem(key);
+ return [];
+ }
+};
+
+export const setFrequentItemToLS = (key, data, itemData) => {
+ if (!AccessorUtilities.isLocalStorageAccessSafe()) {
+ return;
+ }
+
+ const keyList = [
+ 'id',
+ 'avatar_url',
+ 'name',
+ 'full_name',
+ 'name_with_namespace',
+ 'frequency',
+ 'lastUsed',
+ ];
+
+ try {
+ const frequentItems = data[key].map((obj) => extractKeys(obj, keyList));
+ const item = extractKeys(itemData, keyList);
+ const existingItemIndex = frequentItems.findIndex((i) => i.id === item.id);
+
+ if (existingItemIndex >= 0) {
+ // Up the frequency (Max 5)
+ const currentFrequency = frequentItems[existingItemIndex].frequency;
+ frequentItems[existingItemIndex].frequency = Math.min(currentFrequency + 1, MAX_FREQUENCY);
+ frequentItems[existingItemIndex].lastUsed = new Date().getTime();
+ } else {
+ // Only store a max of 5 items
+ if (frequentItems.length >= MAX_FREQUENT_ITEMS) {
+ frequentItems.pop();
+ }
+
+ frequentItems.push({ ...item, frequency: 1, lastUsed: new Date().getTime() });
+ }
+
+ // Sort by frequency and lastUsed
+ frequentItems.sort((a, b) => {
+ if (a.frequency > b.frequency) {
+ return -1;
+ } else if (a.frequency < b.frequency) {
+ return 1;
+ }
+ return b.lastUsed - a.lastUsed;
+ });
+
+ // Note we do not need to commit a mutation here as immediately after this we refresh the page to
+ // update the search results.
+ localStorage.setItem(key, JSON.stringify(frequentItems));
+ } catch {
+ // The LS got in a bad state, let's wipe it
+ localStorage.removeItem(key);
+ }
+};
+
+export const mergeById = (inflatedData, storedData) => {
+ return inflatedData.map((data) => {
+ const stored = storedData?.find((d) => d.id === data.id) || {};
+ return { ...stored, ...data };
+ });
+};
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/topbar/components/group_filter.vue
index da9252eeacd..45a6ae73fac 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/group_filter.vue
@@ -1,6 +1,6 @@
<script>
import { isEmpty } from 'lodash';
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
import SearchableDropdown from './searchable_dropdown.vue';
@@ -19,13 +19,19 @@ export default {
},
computed: {
...mapState(['groups', 'fetchingGroups']),
+ ...mapGetters(['frequentGroups']),
selectedGroup() {
return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
},
},
methods: {
- ...mapActions(['fetchGroups']),
+ ...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
handleGroupChange(group) {
+ // If group.id is null we are clearing the filter and don't need to store that in LS.
+ if (group.id) {
+ this.setFrequentGroup(group);
+ }
+
visitUrl(
setUrlParams({ [GROUP_DATA.queryParam]: group.id, [PROJECT_DATA.queryParam]: null }),
);
@@ -44,6 +50,8 @@ export default {
:loading="fetchingGroups"
:selected-item="selectedGroup"
:items="groups"
+ :frequent-items="frequentGroups"
+ @first-open="loadFrequentGroups"
@search="fetchGroups"
@change="handleGroupChange"
/>
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
index dbe8ba54216..1ca31db61e5 100644
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ b/app/assets/javascripts/search/topbar/components/project_filter.vue
@@ -1,5 +1,5 @@
<script>
-import { mapState, mapActions } from 'vuex';
+import { mapState, mapActions, mapGetters } from 'vuex';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
import SearchableDropdown from './searchable_dropdown.vue';
@@ -18,13 +18,19 @@ export default {
},
computed: {
...mapState(['projects', 'fetchingProjects']),
+ ...mapGetters(['frequentProjects']),
selectedProject() {
return this.initialData ? this.initialData : ANY_OPTION;
},
},
methods: {
- ...mapActions(['fetchProjects']),
+ ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
handleProjectChange(project) {
+ // If project.id is null we are clearing the filter and don't need to store that in LS.
+ if (project.id) {
+ this.setFrequentProject(project);
+ }
+
// This determines if we need to update the group filter or not
const queryParams = {
...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
@@ -47,6 +53,8 @@ export default {
:loading="fetchingProjects"
:selected-item="selectedProject"
:items="projects"
+ :frequent-items="frequentProjects"
+ @first-open="loadFrequentProjects"
@search="fetchProjects"
@change="handleProjectChange"
/>
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
index 2e2aa052dd8..5653cddda60 100644
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
+++ b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
@@ -2,6 +2,7 @@
import {
GlDropdown,
GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
@@ -16,11 +17,13 @@ import SearchableDropdownItem from './searchable_dropdown_item.vue';
export default {
i18n: {
clearLabel: __('Clear'),
+ frequentlySearched: __('Frequently searched'),
},
name: 'SearchableDropdown',
components: {
GlDropdown,
GlDropdownItem,
+ GlDropdownSectionHeader,
GlSearchBoxByType,
GlLoadingIcon,
GlIcon,
@@ -61,17 +64,33 @@ export default {
required: false,
default: () => [],
},
+ frequentItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
data() {
return {
searchText: '',
+ hasBeenOpened: false,
};
},
+ computed: {
+ showFrequentItems() {
+ return !this.searchText && this.frequentItems.length > 0;
+ },
+ },
methods: {
isSelected(selected) {
return selected.id === this.selectedItem.id;
},
openDropdown() {
+ if (!this.hasBeenOpened) {
+ this.hasBeenOpened = true;
+ this.$emit('first-open');
+ }
+
this.$emit('search', this.searchText);
},
resetDropdown() {
@@ -99,7 +118,7 @@ export default {
<span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
{{ selectedItem[name] }}
</span>
- <gl-loading-icon v-if="loading" inline class="gl-mr-3" />
+ <gl-loading-icon v-if="loading" size="sm" inline class="gl-mr-3" />
<gl-button
v-if="!isSelected($options.ANY_OPTION)"
v-gl-tooltip
@@ -133,6 +152,25 @@ export default {
<span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
</gl-dropdown-item>
</div>
+ <div
+ v-if="showFrequentItems"
+ class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2 gl-mb-2"
+ >
+ <gl-dropdown-section-header>{{
+ $options.i18n.frequentlySearched
+ }}</gl-dropdown-section-header>
+ <searchable-dropdown-item
+ v-for="item in frequentItems"
+ :key="item.id"
+ :item="item"
+ :selected-item="selectedItem"
+ :search-text="searchText"
+ :name="name"
+ :full-name="fullName"
+ data-testid="frequent-items"
+ @change="updateDropdown"
+ />
+ </div>
<div v-if="!loading">
<searchable-dropdown-item
v-for="item in items"
@@ -142,6 +180,7 @@ export default {
:search-text="searchText"
:name="name"
:full-name="fullName"
+ data-testid="searchable-items"
@change="updateDropdown"
/>
</div>
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 498d4af59b4..42d6444e690 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,5 @@
<script>
-import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
+import { GlDropdownItem, GlAvatar, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import highlight from '~/lib/utils/highlight';
import { truncateNamespace } from '~/lib/utils/text_utility';
@@ -9,6 +9,9 @@ export default {
GlDropdownItem,
GlAvatar,
},
+ directives: {
+ SafeHtml,
+ },
props: {
item: {
type: Object,
@@ -62,8 +65,7 @@ export default {
:size="32"
/>
<div class="gl-display-flex gl-flex-direction-column">
- <!-- eslint-disable-next-line vue/no-v-html -->
- <span data-testid="item-title" v-html="highlightedItemName">{{ item[name] }}</span>
+ <span v-safe-html="highlightedItemName" data-testid="item-title"></span>
<span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{
truncatedNamespace
}}</span>
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 9c133a79607..4f278677c5f 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -4,16 +4,17 @@ import $ from 'jquery';
import { escape, throttle } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
import { getIdenticonBackgroundClass, getIdenticonTitle } from '~/helpers/avatar_helper';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import axios from './lib/utils/axios_utils';
+import { spriteIcon } from './lib/utils/common_utils';
import {
isInGroupsPage,
isInProjectPage,
getGroupSlug,
getProjectSlug,
- spriteIcon,
-} from './lib/utils/common_utils';
+} from './search_autocomplete_utils';
/**
* Search input in top navigation bar.
@@ -343,7 +344,10 @@ export class SearchAutocomplete {
this.searchInput.on('focus', this.onSearchInputFocus);
this.searchInput.on('blur', this.onSearchInputBlur);
this.clearInput.on('click', this.onClearInputClick);
- this.dropdownContent.on('scroll', throttle(this.setScrollFade, 250));
+ this.dropdownContent.on(
+ 'scroll',
+ throttle(this.setScrollFade, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
+ );
this.searchInput.on('click', (e) => {
e.stopPropagation();
diff --git a/app/assets/javascripts/search_autocomplete_utils.js b/app/assets/javascripts/search_autocomplete_utils.js
new file mode 100644
index 00000000000..a9a0f941e93
--- /dev/null
+++ b/app/assets/javascripts/search_autocomplete_utils.js
@@ -0,0 +1,19 @@
+import { getPagePath } from './lib/utils/common_utils';
+
+export const isInGroupsPage = () => getPagePath() === 'groups';
+
+export const isInProjectPage = () => getPagePath() === 'projects';
+
+export const getProjectSlug = () => {
+ if (isInProjectPage()) {
+ return document?.body?.dataset?.project;
+ }
+ return null;
+};
+
+export const getGroupSlug = () => {
+ if (isInProjectPage() || isInGroupsPage()) {
+ return document?.body?.dataset?.group;
+ }
+ return null;
+};
diff --git a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
new file mode 100644
index 00000000000..ce6a1b4888b
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
@@ -0,0 +1,41 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlSprintf,
+ GlAlert,
+ GlLink,
+ },
+ inject: ['autoDevopsHelpPagePath', 'autoDevopsPath'],
+ i18n: {
+ primaryButtonText: s__('SecurityConfiguration|Enable Auto DevOps'),
+ body: s__(
+ 'SecurityConfiguration|Quickly enable all continuous testing and compliance tools by enabling %{linkStart}Auto DevOps%{linkEnd}',
+ ),
+ },
+ methods: {
+ dismissMethod() {
+ this.$emit('dismiss');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-alert
+ variant="info"
+ :primary-button-link="autoDevopsPath"
+ :primary-button-text="$options.i18n.primaryButtonText"
+ @dismiss="dismissMethod"
+ >
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="autoDevopsHelpPagePath">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/security_configuration/components/configuration_table.vue b/app/assets/javascripts/security_configuration/components/configuration_table.vue
index 2110af1522b..7f250bf1365 100644
--- a/app/assets/javascripts/security_configuration/components/configuration_table.vue
+++ b/app/assets/javascripts/security_configuration/components/configuration_table.vue
@@ -8,6 +8,7 @@ import {
REPORT_TYPE_DAST_PROFILES,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
@@ -46,6 +47,7 @@ export default {
[REPORT_TYPE_DAST_PROFILES]: Upgrade,
[REPORT_TYPE_DEPENDENCY_SCANNING]: Upgrade,
[REPORT_TYPE_CONTAINER_SCANNING]: Upgrade,
+ [REPORT_TYPE_CLUSTER_IMAGE_SCANNING]: Upgrade,
[REPORT_TYPE_COVERAGE_FUZZING]: Upgrade,
[REPORT_TYPE_API_FUZZING]: Upgrade,
[REPORT_TYPE_LICENSE_COMPLIANCE]: Upgrade,
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index 142dade914b..5cb9277040d 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -1,7 +1,6 @@
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, s__ } from '~/locale';
-import configureSastMutation from '~/security_configuration/graphql/configure_sast.mutation.graphql';
import {
REPORT_TYPE_SAST,
REPORT_TYPE_DAST,
@@ -9,11 +8,15 @@ import {
REPORT_TYPE_SECRET_DETECTION,
REPORT_TYPE_DEPENDENCY_SCANNING,
REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
REPORT_TYPE_COVERAGE_FUZZING,
REPORT_TYPE_API_FUZZING,
REPORT_TYPE_LICENSE_COMPLIANCE,
} from '~/vue_shared/security_reports/constants';
+import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
+import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
+
/**
* Translations & helpPagePaths for Static Security Configuration Page
*/
@@ -34,8 +37,8 @@ export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/das
});
export const DAST_PROFILES_NAME = __('DAST Scans');
-export const DAST_PROFILES_DESCRIPTION = __(
- 'Saved scan settings and target site settings which are reusable.',
+export const DAST_PROFILES_DESCRIPTION = s__(
+ 'SecurityConfiguration|Manage profiles for use by DAST scans.',
);
export const DAST_PROFILES_HELP_PATH = helpPagePath('user/application_security/dast/index');
export const DAST_PROFILES_CONFIG_TEXT = s__('SecurityConfiguration|Manage scans');
@@ -76,6 +79,18 @@ export const CONTAINER_SCANNING_CONFIG_HELP_PATH = helpPagePath(
{ anchor: 'configuration' },
);
+export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
+export const CLUSTER_IMAGE_SCANNING_DESCRIPTION = __(
+ 'Check your Kubernetes cluster images for known vulnerabilities.',
+);
+export const CLUSTER_IMAGE_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/cluster_image_scanning/index',
+);
+export const CLUSTER_IMAGE_SCANNING_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/cluster_image_scanning/index',
+ { anchor: 'configuration' },
+);
+
export const COVERAGE_FUZZING_NAME = __('Coverage Fuzzing');
export const COVERAGE_FUZZING_DESCRIPTION = __(
'Find bugs in your code with coverage-guided fuzzing.',
@@ -132,6 +147,12 @@ export const scanners = [
type: REPORT_TYPE_CONTAINER_SCANNING,
},
{
+ name: CLUSTER_IMAGE_SCANNING_NAME,
+ description: CLUSTER_IMAGE_SCANNING_DESCRIPTION,
+ helpPath: CLUSTER_IMAGE_SCANNING_HELP_PATH,
+ type: REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
+ },
+ {
name: SECRET_DETECTION_NAME,
description: SECRET_DETECTION_DESCRIPTION,
helpPath: SECRET_DETECTION_HELP_PATH,
@@ -195,6 +216,10 @@ export const securityFeatures = [
helpPath: DEPENDENCY_SCANNING_HELP_PATH,
configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
type: REPORT_TYPE_DEPENDENCY_SCANNING,
+
+ // This field will eventually come from the backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: window.gon.features?.secDependencyScanningUiEnable,
},
{
name: CONTAINER_SCANNING_NAME,
@@ -204,12 +229,28 @@ export const securityFeatures = [
type: REPORT_TYPE_CONTAINER_SCANNING,
},
{
+ name: CLUSTER_IMAGE_SCANNING_NAME,
+ description: CLUSTER_IMAGE_SCANNING_DESCRIPTION,
+ helpPath: CLUSTER_IMAGE_SCANNING_HELP_PATH,
+ configurationHelpPath: CLUSTER_IMAGE_SCANNING_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_CLUSTER_IMAGE_SCANNING,
+ },
+ {
name: SECRET_DETECTION_NAME,
description: SECRET_DETECTION_DESCRIPTION,
helpPath: SECRET_DETECTION_HELP_PATH,
configurationHelpPath: SECRET_DETECTION_CONFIG_HELP_PATH,
type: REPORT_TYPE_SECRET_DETECTION,
+
+ // This field is currently hardcoded because Secret Detection is always
+ // available. It will eventually come from the Backend, the progress is
+ // tracked in https://gitlab.com/gitlab-org/gitlab/-/issues/333113
available: true,
+
+ // This field is currently hardcoded because SAST can always be enabled via MR
+ // It will eventually come from the Backend, the progress is tracked in
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/331621
+ canEnableByMergeRequest: true,
},
{
name: API_FUZZING_NAME,
@@ -247,4 +288,15 @@ export const featureToMutationMap = {
},
}),
},
+ [REPORT_TYPE_SECRET_DETECTION]: {
+ mutationId: 'configureSecretDetection',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSecretDetectionMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ }),
+ },
};
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 518a6ede3de..23cffde1f83 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -46,8 +46,7 @@ export default {
return button;
},
showManageViaMr() {
- const { available, configured, canEnableByMergeRequest } = this.feature;
- return canEnableByMergeRequest && available && !configured;
+ return ManageViaMr.canRender(this.feature);
},
cardClasses() {
return { 'gl-bg-gray-10': !this.available };
diff --git a/app/assets/javascripts/security_configuration/components/redesigned_app.vue b/app/assets/javascripts/security_configuration/components/redesigned_app.vue
index d8a12f4a792..915da378a4f 100644
--- a/app/assets/javascripts/security_configuration/components/redesigned_app.vue
+++ b/app/assets/javascripts/security_configuration/components/redesigned_app.vue
@@ -2,18 +2,22 @@
import { GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
import FeatureCard from './feature_card.vue';
import SectionLayout from './section_layout.vue';
import UpgradeBanner from './upgrade_banner.vue';
export const i18n = {
compliance: s__('SecurityConfiguration|Compliance'),
+ configurationHistory: s__('SecurityConfiguration|Configuration history'),
securityTesting: s__('SecurityConfiguration|Security testing'),
- securityTestingDescription: s__(
+ latestPipelineDescription: s__(
`SecurityConfiguration|The status of the tools only applies to the
- default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.
- Once you've enabled a scan for the default branch, any subsequent feature
- branch you create will include the scan.`,
+ default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.`,
+ ),
+ description: s__(
+ `SecurityConfiguration|Once you've enabled a scan for the default branch,
+ any subsequent feature branch you create will include the scan.`,
),
securityConfiguration: __('Security Configuration'),
};
@@ -28,6 +32,7 @@ export default {
FeatureCard,
SectionLayout,
UpgradeBanner,
+ AutoDevOpsAlert,
UserCalloutDismisser,
},
props: {
@@ -44,6 +49,16 @@ export default {
required: false,
default: false,
},
+ autoDevopsEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ canEnableAutoDevops: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
gitlabCiHistoryPath: {
type: String,
required: false,
@@ -64,16 +79,26 @@ export default {
canViewCiHistory() {
return Boolean(this.gitlabCiPresent && this.gitlabCiHistoryPath);
},
+ shouldShowDevopsAlert() {
+ return !this.autoDevopsEnabled && !this.gitlabCiPresent && this.canEnableAutoDevops;
+ },
},
};
</script>
<template>
<article>
+ <user-callout-dismisser
+ v-if="shouldShowDevopsAlert"
+ feature-name="security_configuration_devops_alert"
+ >
+ <template #default="{ dismiss, shouldShowCallout }">
+ <auto-dev-ops-alert v-if="shouldShowCallout" class="gl-mt-3" @dismiss="dismiss" />
+ </template>
+ </user-callout-dismisser>
<header>
<h1 class="gl-font-size-h1">{{ $options.i18n.securityConfiguration }}</h1>
</header>
-
<user-callout-dismisser v-if="canUpgrade" feature-name="security_configuration_upgrade_banner">
<template #default="{ dismiss, shouldShowCallout }">
<upgrade-banner v-if="shouldShowCallout" @close="dismiss" />
@@ -84,16 +109,19 @@ export default {
<gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
<section-layout :heading="$options.i18n.securityTesting">
<template #description>
- <p
- v-if="latestPipelinePath"
- data-testid="latest-pipeline-info-security"
- class="gl-line-height-20"
- >
- <gl-sprintf :message="$options.i18n.securityTestingDescription">
- <template #link="{ content }">
- <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <p>
+ <span data-testid="latest-pipeline-info-security">
+ <gl-sprintf
+ v-if="latestPipelinePath"
+ :message="$options.i18n.latestPipelineDescription"
+ >
+ <template #link="{ content }">
+ <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+
+ {{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
<gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
@@ -106,6 +134,7 @@ export default {
<feature-card
v-for="feature in augmentedSecurityFeatures"
:key="feature.type"
+ data-testid="security-testing-card"
:feature="feature"
class="gl-mb-6"
/>
@@ -115,16 +144,19 @@ export default {
<gl-tab data-testid="compliance-testing-tab" :title="$options.i18n.compliance">
<section-layout :heading="$options.i18n.compliance">
<template #description>
- <p
- v-if="latestPipelinePath"
- class="gl-line-height-20"
- data-testid="latest-pipeline-info-compliance"
- >
- <gl-sprintf :message="$options.i18n.securityTestingDescription">
- <template #link="{ content }">
- <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
+ <p>
+ <span data-testid="latest-pipeline-info-compliance">
+ <gl-sprintf
+ v-if="latestPipelinePath"
+ :message="$options.i18n.latestPipelineDescription"
+ >
+ <template #link="{ content }">
+ <gl-link :href="latestPipelinePath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </span>
+
+ {{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
<gl-link data-testid="compliance-view-history-link" :href="gitlabCiHistoryPath">{{
diff --git a/app/assets/javascripts/security_configuration/components/section_layout.vue b/app/assets/javascripts/security_configuration/components/section_layout.vue
index 1e1f83a6d99..e351f9b9d8d 100644
--- a/app/assets/javascripts/security_configuration/components/section_layout.vue
+++ b/app/assets/javascripts/security_configuration/components/section_layout.vue
@@ -11,12 +11,12 @@ export default {
</script>
<template>
- <div class="row">
- <div class="col-lg-5">
+ <div class="row gl-line-height-20">
+ <div class="col-lg-4">
<h2 class="gl-font-size-h2 gl-mt-0">{{ heading }}</h2>
<slot name="description"></slot>
</div>
- <div class="col-lg-7">
+ <div class="col-lg-8">
<slot name="features"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_secret_detection.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_secret_detection.mutation.graphql
new file mode 100644
index 00000000000..e42a8de64f3
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_secret_detection.mutation.graphql
@@ -0,0 +1,6 @@
+mutation configureSecretDetection($input: ConfigureSecretDetectionInput!) {
+ configureSecretDetection(input: $input) {
+ successPath
+ errors
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index e1dc6f24737..f05bd79258e 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -7,11 +7,7 @@ import { securityFeatures, complianceFeatures } from './components/constants';
import RedesignedSecurityConfigurationApp from './components/redesigned_app.vue';
import { augmentFeatures } from './utils';
-export const initStaticSecurityConfiguration = (el) => {
- if (!el) {
- return null;
- }
-
+export const initRedesignedSecurityConfiguration = (el) => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
@@ -24,35 +20,60 @@ export const initStaticSecurityConfiguration = (el) => {
features,
latestPipelinePath,
gitlabCiHistoryPath,
+ autoDevopsHelpPagePath,
+ autoDevopsPath,
} = el.dataset;
- if (gon.features.securityConfigurationRedesign) {
- const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
- securityFeatures,
- complianceFeatures,
- features ? JSON.parse(features) : [],
- );
+ const { augmentedSecurityFeatures, augmentedComplianceFeatures } = augmentFeatures(
+ securityFeatures,
+ complianceFeatures,
+ features ? JSON.parse(features) : [],
+ );
+
+ return new Vue({
+ el,
+ apolloProvider,
+ provide: {
+ projectPath,
+ upgradePath,
+ autoDevopsHelpPagePath,
+ autoDevopsPath,
+ },
+ render(createElement) {
+ return createElement(RedesignedSecurityConfigurationApp, {
+ props: {
+ augmentedComplianceFeatures,
+ augmentedSecurityFeatures,
+ latestPipelinePath,
+ gitlabCiHistoryPath,
+ ...parseBooleanDataAttributes(el, [
+ 'gitlabCiPresent',
+ 'autoDevopsEnabled',
+ 'canEnableAutoDevops',
+ ]),
+ },
+ });
+ },
+ });
+};
+
+export const initCESecurityConfiguration = (el) => {
+ if (!el) {
+ return null;
+ }
- return new Vue({
- el,
- apolloProvider,
- provide: {
- projectPath,
- upgradePath,
- },
- render(createElement) {
- return createElement(RedesignedSecurityConfigurationApp, {
- props: {
- augmentedComplianceFeatures,
- augmentedSecurityFeatures,
- latestPipelinePath,
- gitlabCiHistoryPath,
- ...parseBooleanDataAttributes(el, ['gitlabCiPresent']),
- },
- });
- },
- });
+ if (gon.features?.securityConfigurationRedesign) {
+ return initRedesignedSecurityConfiguration(el);
}
+
+ Vue.use(VueApollo);
+
+ const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+ });
+
+ const { projectPath, upgradePath } = el.dataset;
+
return new Vue({
el,
apolloProvider,
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index 071ebff4f21..ec6b93c6193 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,6 +1,8 @@
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
export const augmentFeatures = (securityFeatures, complianceFeatures, features = []) => {
const featuresByType = features.reduce((acc, feature) => {
- acc[feature.type] = feature;
+ acc[feature.type] = convertObjectPropsToCamelCase(feature, { deep: true });
return acc;
}, {});
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 c608c71714b..4c1f0d892af 100644
--- a/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
+++ b/app/assets/javascripts/self_monitor/components/self_monitor_form.vue
@@ -82,7 +82,7 @@ export default {
text: this.alertContent.actionText,
onClick: (_, toastObject) => {
this[this.alertContent.actionName]();
- toastObject.goAway(0);
+ toastObject.hide();
},
},
};
diff --git a/app/assets/javascripts/sentry/index.js b/app/assets/javascripts/sentry/index.js
index a875ef84088..176745b4177 100644
--- a/app/assets/javascripts/sentry/index.js
+++ b/app/assets/javascripts/sentry/index.js
@@ -14,6 +14,7 @@ const index = function index() {
release: gon.revision,
tags: {
revision: gon.revision,
+ feature_category: gon.feature_category,
},
});
diff --git a/app/assets/javascripts/sentry/sentry_config.js b/app/assets/javascripts/sentry/sentry_config.js
index bc3b2f16a6a..a3a2c794a67 100644
--- a/app/assets/javascripts/sentry/sentry_config.js
+++ b/app/assets/javascripts/sentry/sentry_config.js
@@ -59,16 +59,18 @@ const SentryConfig = {
configure() {
const { dsn, release, tags, whitelistUrls, environment } = this.options;
+
Sentry.init({
dsn,
release,
- tags,
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,
});
+
+ Sentry.setTags(tags);
},
setUser() {
diff --git a/app/assets/javascripts/usage_ping_consent.js b/app/assets/javascripts/service_ping_consent.js
index 3876aa62b75..f145a1b30db 100644
--- a/app/assets/javascripts/usage_ping_consent.js
+++ b/app/assets/javascripts/service_ping_consent.js
@@ -1,23 +1,24 @@
import $ from 'jquery';
-import { deprecatedCreateFlash as Flash, hideFlash } from './flash';
+import createFlash, { hideFlash } from './flash';
import axios from './lib/utils/axios_utils';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
export default () => {
- $('body').on('click', '.js-usage-consent-action', (e) => {
+ $('body').on('click', '.js-service-ping-consent-action', (e) => {
e.preventDefault();
e.stopImmediatePropagation(); // overwrite rails listener
- const { url, checkEnabled, pingEnabled } = e.target.dataset;
+ const { url, checkEnabled, servicePingEnabled } = e.target.dataset;
const data = {
application_setting: {
version_check_enabled: parseBoolean(checkEnabled),
- usage_ping_enabled: parseBoolean(pingEnabled),
+ service_ping_enabled: parseBoolean(servicePingEnabled),
},
};
- const hideConsentMessage = () => hideFlash(document.querySelector('.ping-consent-message'));
+ const hideConsentMessage = () =>
+ hideFlash(document.querySelector('.service-ping-consent-message'));
axios
.put(url, data)
@@ -26,7 +27,9 @@ export default () => {
})
.catch(() => {
hideConsentMessage();
- Flash(__('Something went wrong. Try again later.'));
+ createFlash({
+ message: __('Something went wrong. Try again later.'),
+ });
});
});
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
index adb573db652..4b3b22f6db3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/assignee_title.vue
@@ -47,7 +47,7 @@ export default {
<template>
<div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
{{ assigneeTitle }}
- <gl-loading-icon v-if="loading" inline class="align-bottom" />
+ <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 9840aa4ed66..c6877226b7d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,6 +1,6 @@
<script>
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -113,7 +113,9 @@ export default {
})
.catch(() => {
this.loading = false;
- return new Flash(__('Error occurred when saving assignees'));
+ return createFlash({
+ message: __('Error occurred when saving assignees'),
+ });
});
},
exposeAvailabilityStatus(users) {
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 d9a974202a3..1dd05d3886e 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -3,6 +3,7 @@ import { GlDropdownItem } from '@gitlab/ui';
import { cloneDeep } from 'lodash';
import Vue from 'vue';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { IssuableType } from '~/issue_show/constants';
import { __, n__ } from '~/locale';
import SidebarAssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
@@ -80,6 +81,8 @@ export default {
selected: [],
isSettingAssignees: false,
isDirty: false,
+ oldIid: null,
+ oldSelected: null,
};
},
apollo: {
@@ -142,6 +145,14 @@ export default {
return this.currentUser.username !== undefined;
},
},
+ watch: {
+ iid(_, oldIid) {
+ if (this.isDirty) {
+ this.oldIid = oldIid;
+ this.oldSelected = this.selected;
+ }
+ },
+ },
created() {
assigneesWidget.updateAssignees = this.updateAssignees;
},
@@ -157,10 +168,14 @@ export default {
variables: {
...this.queryVariables,
assigneeUsernames,
+ iid: this.oldIid || this.iid,
},
})
.then(({ data }) => {
- this.$emit('assignees-updated', data.issuableSetAssignees.issuable.assignees.nodes);
+ this.$emit('assignees-updated', {
+ id: getIdFromGraphQLId(data.issuableSetAssignees.issuable.id),
+ assignees: data.issuableSetAssignees.issuable.assignees.nodes,
+ });
return data;
})
.catch(() => {
@@ -176,7 +191,10 @@ export default {
saveAssignees() {
if (this.isDirty) {
this.isDirty = false;
- this.updateAssignees(this.selected.map(({ username }) => username));
+ const usernames = this.oldSelected || this.selected;
+ this.updateAssignees(usernames.map(({ username }) => username));
+ this.oldIid = null;
+ this.oldSelected = null;
}
this.$el.dispatchEvent(hideDropdownEvent);
},
diff --git a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue
index 41b3b6c9a45..bed84dc5706 100644
--- a/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/user_name_with_status.vue
@@ -22,8 +22,16 @@ export default {
required: false,
default: '',
},
+ pronouns: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
+ hasPronouns() {
+ return this.pronouns !== null && this.pronouns.trim() !== '';
+ },
isBusy() {
return isUserBusy(this.availability);
},
@@ -32,9 +40,18 @@ export default {
</script>
<template>
<span :class="containerClasses">
- <gl-sprintf v-if="isBusy" :message="s__('UserAvailability|%{author} (Busy)')">
- <template #author>{{ name }}</template>
+ <gl-sprintf :message="s__('UserAvailability|%{author} %{spanStart}(Busy)%{spanEnd}')">
+ <template #author
+ >{{ name }}
+ <span v-if="hasPronouns" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal"
+ >({{ pronouns }})</span
+ ></template
+ >
+ <template #span="{ content }"
+ ><span v-if="isBusy" class="gl-text-gray-500 gl-font-sm gl-font-weight-normal">{{
+ content
+ }}</span>
+ </template>
</gl-sprintf>
- <template v-else>{{ name }}</template>
</span>
</template>
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 372368707af..dc0f2b54a7b 100644
--- a/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
+++ b/app/assets/javascripts/sidebar/components/confidential/sidebar_confidentiality_widget.vue
@@ -4,7 +4,7 @@ import Vue from 'vue';
import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { confidentialityQueries } from '~/sidebar/constants';
+import { confidentialityQueries, Tracking } from '~/sidebar/constants';
import SidebarConfidentialityContent from './sidebar_confidentiality_content.vue';
import SidebarConfidentialityForm from './sidebar_confidentiality_form.vue';
@@ -18,8 +18,8 @@ const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
export default {
tracking: {
- event: 'click_edit_button',
- label: 'right_sidebar',
+ event: Tracking.editEvent,
+ label: Tracking.rightSidebarLabel,
property: 'confidentiality',
},
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 c3dfa5f8b14..1ff24dec884 100644
--- a/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
+++ b/app/assets/javascripts/sidebar/components/date/sidebar_date_widget.vue
@@ -5,7 +5,13 @@ import { IssuableType } from '~/issue_show/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 } from '~/sidebar/constants';
+import {
+ dateFields,
+ dateTypes,
+ dueDateQueries,
+ startDateQueries,
+ Tracking,
+} from '~/sidebar/constants';
import SidebarFormattedDate from './sidebar_formatted_date.vue';
import SidebarInheritDate from './sidebar_inherit_date.vue';
@@ -15,8 +21,8 @@ const hideDropdownEvent = new CustomEvent('hiddenGlDropdown', {
export default {
tracking: {
- event: 'click_edit_button',
- label: 'right_sidebar',
+ event: Tracking.editEvent,
+ label: Tracking.rightSidebarLabel,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -149,6 +155,9 @@ export default {
},
},
methods: {
+ epicDatePopoverEl() {
+ return this.$refs?.epicDatePopover?.$el;
+ },
closeForm() {
this.$refs.editable.collapse();
this.$el.dispatchEvent(hideDropdownEvent);
@@ -249,12 +258,7 @@ export default {
:aria-label="$options.i18n.help"
data-testid="inherit-date-popover"
/>
- <gl-popover
- :target="() => $refs.epicDatePopover.$el"
- triggers="focus"
- placement="left"
- boundary="viewport"
- >
+ <gl-popover :target="epicDatePopoverEl" triggers="focus" placement="left" boundary="viewport">
<p>{{ $options.i18n.dateHelpValidMessage }}</p>
<gl-link :href="$options.dateHelpUrl" target="_blank">{{
$options.i18n.learnMore
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index c3f31a3d220..42d2e456a07 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -2,7 +2,7 @@
import { GlButton } from '@gitlab/ui';
import $ from 'jquery';
import { mapActions } from 'vuex';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __, sprintf } from '../../../locale';
import eventHub from '../../event_hub';
@@ -52,7 +52,9 @@ export default {
const flashMessage = __(
'Something went wrong trying to change the locked state of this %{issuableDisplayName}',
);
- Flash(sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }));
+ createFlash({
+ message: sprintf(flashMessage, { issuableDisplayName: this.issuableDisplayName }),
+ });
})
.finally(() => {
this.closeForm();
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index e85e416881c..650aa603f18 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -92,11 +92,11 @@ export default {
@click="onClickCollapsedIcon"
>
<gl-icon name="users" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<span v-else data-testid="collapsed-count"> {{ participantCount }} </span>
</div>
<div v-if="showParticipantLabel" class="title hide-collapsed gl-mb-2">
- <gl-loading-icon v-if="loading" :inline="true" />
+ <gl-loading-icon v-if="loading" size="sm" :inline="true" />
{{ participantLabel }}
</div>
<div class="participants-list hide-collapsed">
diff --git a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
index 88c0b18ccc7..295027186cc 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/reviewer_title.vue
@@ -35,7 +35,7 @@ export default {
<template>
<div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
{{ reviewerTitle }}
- <gl-loading-icon v-if="loading" inline class="align-bottom" />
+ <gl-loading-icon v-if="loading" size="sm" inline class="align-bottom" />
<a
v-if="editable"
class="js-sidebar-dropdown-toggle edit-link float-right"
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index c0bd54c60da..e414aaf719b 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -2,7 +2,7 @@
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import eventHub from '~/sidebar/event_hub';
import Store from '~/sidebar/stores/sidebar_store';
@@ -80,7 +80,9 @@ export default {
})
.catch(() => {
this.loading = false;
- return new Flash(__('Error occurred when saving reviewers'));
+ return createFlash({
+ message: __('Error occurred when saving reviewers'),
+ });
});
},
requestReview(data) {
diff --git a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
index 592cfea5e32..fdf63c23552 100644
--- a/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
+++ b/app/assets/javascripts/sidebar/components/severity/sidebar_severity.vue
@@ -181,7 +181,7 @@ export default {
</gl-dropdown-item>
</gl-dropdown>
- <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" />
<severity-token v-else-if="!isDropdownShowing" :severity="selectedItem" />
</div>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index c80ccc928b3..2e00a23de7c 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -16,11 +16,13 @@ import { IssuableType } from '~/issue_show/constants';
import { __, s__, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import {
+ Tracking,
IssuableAttributeState,
IssuableAttributeType,
issuableAttributesQueries,
noAttributeId,
-} from '../constants';
+ defaultEpicSort,
+} from '~/sidebar/constants';
export default {
noAttributeId,
@@ -28,6 +30,7 @@ export default {
issuableAttributesQueries,
i18n: {
[IssuableAttributeType.Milestone]: __('Milestone'),
+ expired: __('(expired)'),
none: __('None'),
},
directives: {
@@ -73,9 +76,14 @@ export default {
type: String,
required: true,
validator(value) {
- return value === IssuableType.Issue;
+ return [IssuableType.Issue, IssuableType.MergeRequest].includes(value);
},
},
+ icon: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
},
apollo: {
currentAttribute: {
@@ -117,7 +125,9 @@ export default {
return {
fullPath: this.attrWorkspacePath,
title: this.searchTerm,
+ in: this.searchTerm && this.issuableAttribute === IssuableType.Epic ? 'TITLE' : undefined,
state: this.$options.IssuableAttributeState[this.issuableAttribute],
+ sort: this.issuableAttribute === IssuableType.Epic ? defaultEpicSort : null,
};
},
update(data) {
@@ -140,8 +150,8 @@ export default {
currentAttribute: null,
attributesList: [],
tracking: {
- label: 'right_sidebar',
- event: 'click_edit_button',
+ event: Tracking.editEvent,
+ label: Tracking.rightSidebarLabel,
property: this.issuableAttribute,
},
};
@@ -170,6 +180,9 @@ export default {
attributeTypeTitle() {
return this.$options.i18n[this.issuableAttribute];
},
+ attributeTypeIcon() {
+ return this.icon || this.issuableAttribute;
+ },
i18n() {
return {
noAttribute: sprintf(s__('DropdownWidget|No %{issuableAttribute}'), {
@@ -222,7 +235,8 @@ export default {
variables: {
fullPath: this.workspacePath,
attributeId:
- this.issuableAttribute === IssuableAttributeType.Milestone
+ this.issuableAttribute === IssuableAttributeType.Milestone &&
+ this.issuableType === IssuableType.Issue
? getIdFromGraphQLId(attributeId)
: attributeId,
iid: this.iid,
@@ -253,6 +267,11 @@ export default {
attributeId === this.currentAttribute?.id || (!this.currentAttribute?.id && !attributeId)
);
},
+ isAttributeOverdue(attribute) {
+ return this.issuableAttribute === IssuableAttributeType.Milestone
+ ? attribute?.expired
+ : false;
+ },
showDropdown() {
this.$refs.newDropdown.show();
},
@@ -282,8 +301,10 @@ export default {
>
<template #collapsed>
<div v-if="isClassicSidebar" v-gl-tooltip class="sidebar-collapsed-icon">
- <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="issuableAttribute" />
- <span class="collapse-truncated-title">{{ attributeTitle }}</span>
+ <gl-icon :size="16" :aria-label="attributeTypeTitle" :name="attributeTypeIcon" />
+ <span class="collapse-truncated-title">
+ {{ attributeTitle }}
+ </span>
</div>
<div
:data-testid="`select-${issuableAttribute}`"
@@ -300,8 +321,13 @@ export default {
:attributeUrl="attributeUrl"
:currentAttribute="currentAttribute"
>
- <gl-link class="gl-text-gray-900! gl-font-weight-bold" :href="attributeUrl">
+ <gl-link
+ class="gl-text-gray-900! gl-font-weight-bold"
+ :href="attributeUrl"
+ :data-qa-selector="`${issuableAttribute}_link`"
+ >
{{ attributeTitle }}
+ <span v-if="isAttributeOverdue(currentAttribute)">{{ $options.i18n.expired }}</span>
</gl-link>
</slot>
</div>
@@ -328,6 +354,7 @@ export default {
<gl-dropdown-divider />
<gl-loading-icon
v-if="$apollo.queries.attributesList.loading"
+ size="sm"
class="gl-py-4"
data-testid="loading-icon-dropdown"
/>
@@ -351,6 +378,7 @@ export default {
@click="updateAttribute(attrItem.id)"
>
{{ attrItem.title }}
+ <span v-if="isAttributeOverdue(attrItem)">{{ $options.i18n.expired }}</span>
</gl-dropdown-item>
</slot>
</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 825d7ff5841..7c496cc422a 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -117,9 +117,15 @@ export default {
{{ title }}
</span>
<slot name="title-extra"></slot>
- <gl-loading-icon v-if="loading || initialLoading" inline class="gl-ml-2 hide-collapsed" />
+ <gl-loading-icon
+ v-if="loading || initialLoading"
+ size="sm"
+ inline
+ class="gl-ml-2 hide-collapsed"
+ />
<gl-loading-icon
v-if="loading && isClassicSidebar"
+ size="sm"
inline
class="gl-mx-auto gl-my-0 hide-expanded"
/>
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 e97742a1339..bc7e377a966 100644
--- a/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
+++ b/app/assets/javascripts/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue
@@ -2,17 +2,18 @@
import { GlIcon, GlLoadingIcon, GlToggle, GlTooltipDirective } from '@gitlab/ui';
import createFlash from '~/flash';
import { IssuableType } from '~/issue_show/constants';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { __, sprintf } from '~/locale';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
-import { subscribedQueries } from '~/sidebar/constants';
+import { subscribedQueries, Tracking } from '~/sidebar/constants';
const ICON_ON = 'notifications';
const ICON_OFF = 'notifications-off';
export default {
tracking: {
- event: 'click_edit_button',
- label: 'right_sidebar',
+ event: Tracking.editEvent,
+ label: Tracking.rightSidebarLabel,
property: 'subscriptions',
},
directives: {
@@ -102,7 +103,7 @@ export default {
});
},
isLoggedIn() {
- return Boolean(gon.current_user_id);
+ return isLoggedIn();
},
canSubscribe() {
return this.emailsDisabled || !this.isLoggedIn;
@@ -195,7 +196,7 @@ export default {
class="sidebar-collapsed-icon"
@click="toggleSubscribed"
>
- <gl-loading-icon v-if="isLoading" class="sidebar-item-icon is-active" />
+ <gl-loading-icon v-if="isLoading" size="sm" class="sidebar-item-icon is-active" />
<gl-icon v-else :name="notificationIcon" :size="16" class="sidebar-item-icon is-active" />
</span>
<div v-show="emailsDisabled" class="gl-mt-3 hide-collapsed gl-text-gray-500">
diff --git a/app/assets/javascripts/sidebar/components/time_tracking/report.vue b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
index f91a78b7f1d..8a14998910b 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/report.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/report.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlTable } from '@gitlab/ui';
import createFlash from '~/flash';
+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 { __ } from '~/locale';
@@ -52,8 +53,7 @@ export default {
return this.issuableType === 'issue';
},
getGraphQLEntityType() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return this.isIssue() ? 'Issue' : 'MergeRequest';
+ return this.isIssue() ? TYPE_ISSUE : TYPE_MERGE_REQUEST;
},
extractTimelogs(data) {
const timelogs = data?.issuable?.timelogs?.nodes || [];
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 87ddbbf256a..9a9d03353dc 100644
--- a/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
+++ b/app/assets/javascripts/sidebar/components/time_tracking/time_tracker.vue
@@ -200,7 +200,7 @@ export default {
/>
<div class="hide-collapsed gl-line-height-20 gl-text-gray-900">
{{ __('Time tracking') }}
- <gl-loading-icon v-if="isTimeTrackingInfoLoading" inline />
+ <gl-loading-icon v-if="isTimeTrackingInfoLoading" size="sm" inline />
<div
v-if="!showHelpState"
data-testid="helpButton"
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
new file mode 100644
index 00000000000..a9c4203af22
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -0,0 +1,195 @@
+<script>
+import { GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { produce } from 'immer';
+import createFlash 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';
+
+export default {
+ components: {
+ GlButton,
+ GlIcon,
+ TodoButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: {
+ isClassicSidebar: {
+ default: false,
+ },
+ },
+ props: {
+ issuableId: {
+ type: String,
+ required: true,
+ },
+ issuableIid: {
+ type: String,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ issuableType: {
+ required: true,
+ type: String,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ };
+ },
+ apollo: {
+ todoId: {
+ query() {
+ return todoQueries[this.issuableType].query;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.issuableIid),
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.currentUserTodos.nodes[0]?.id;
+ },
+ result({ data }) {
+ const currentUserTodos = data.workspace?.issuable?.currentUserTodos?.nodes ?? [];
+ this.todoId = currentUserTodos[0]?.id;
+ this.$emit('todoUpdated', currentUserTodos.length > 0);
+ },
+ error() {
+ createFlash({
+ message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
+ issuableType: this.issuableType,
+ }),
+ });
+ },
+ },
+ },
+ computed: {
+ todoIdQuery() {
+ return todoQueries[this.issuableType].query;
+ },
+ todoIdQueryVariables() {
+ return {
+ fullPath: this.fullPath,
+ iid: String(this.issuableIid),
+ };
+ },
+ isLoading() {
+ return this.$apollo.queries?.todoId?.loading || this.loading;
+ },
+ hasTodo() {
+ return Boolean(this.todoId);
+ },
+ todoMutationType() {
+ if (this.hasTodo) {
+ return TodoMutationTypes.MarkDone;
+ }
+ return TodoMutationTypes.Create;
+ },
+ collapsedButtonIcon() {
+ return this.hasTodo ? 'todo-done' : 'todo-add';
+ },
+ tootltipTitle() {
+ return todoLabel(this.hasTodo);
+ },
+ },
+ methods: {
+ toggleTodo() {
+ this.loading = true;
+ this.$apollo
+ .mutate({
+ mutation: todoMutations[this.todoMutationType],
+ variables: {
+ input: {
+ targetId: !this.hasTodo ? this.issuableId : undefined,
+ id: this.hasTodo ? this.todoId : undefined,
+ },
+ },
+ update: (
+ store,
+ {
+ data: {
+ todoMutation: { todo },
+ },
+ },
+ ) => {
+ const queryProps = {
+ query: this.todoIdQuery,
+ variables: this.todoIdQueryVariables,
+ };
+
+ const sourceData = store.readQuery(queryProps);
+ const data = produce(sourceData, (draftState) => {
+ draftState.workspace.issuable.currentUserTodos.nodes = this.hasTodo ? [] : [todo];
+ });
+ store.writeQuery({
+ data,
+ ...queryProps,
+ });
+ },
+ })
+ .then(
+ ({
+ data: {
+ todoMutation: { errors },
+ },
+ }) => {
+ if (errors.length) {
+ createFlash({
+ message: errors[0],
+ });
+ }
+ },
+ )
+ .catch(() => {
+ createFlash({
+ message: sprintf(__('Something went wrong while setting %{issuableType} to-do item.'), {
+ issuableType: this.issuableType,
+ }),
+ });
+ })
+ .finally(() => {
+ this.loading = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div data-testid="sidebar-todo">
+ <todo-button
+ :issuable-type="issuableType"
+ :issuable-id="issuableId"
+ :is-todo="hasTodo"
+ :loading="isLoading"
+ size="small"
+ class="hide-collapsed"
+ @click.stop.prevent="toggleTodo"
+ />
+ <gl-button
+ v-if="isClassicSidebar"
+ category="tertiary"
+ type="reset"
+ class="sidebar-collapsed-icon sidebar-collapsed-container gl-rounded-0! gl-shadow-none!"
+ @click.stop.prevent="toggleTodo"
+ >
+ <gl-icon
+ v-gl-tooltip.left.viewport
+ :title="tootltipTitle"
+ :size="16"
+ :class="{ 'todo-undone': hasTodo }"
+ :name="collapsedButtonIcon"
+ :aria-label="collapsedButtonIcon"
+ />
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
index f589e7555b3..f7e76cc2b7f 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/todo.vue
@@ -85,6 +85,6 @@ export default {
:name="collapsedButtonIcon"
/>
<span v-show="!collapsed" class="issuable-todo-inner">{{ buttonLabel }}</span>
- <gl-loading-icon v-show="isActionActive" :inline="true" />
+ <gl-loading-icon v-show="isActionActive" size="sm" :inline="true" />
</button>
</template>
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index e8e69c19d9f..08ee4379c0c 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -1,18 +1,26 @@
import { IssuableType } from '~/issue_show/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/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';
@@ -20,6 +28,7 @@ import updateEpicSubscriptionMutation from '~/sidebar/queries/update_epic_subscr
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 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 getAlertAssignees from '~/vue_shared/components/sidebar/queries/get_alert_assignees.query.graphql';
@@ -35,7 +44,9 @@ import projectIssueMilestoneMutation from './queries/project_issue_milestone.mut
import projectIssueMilestoneQuery from './queries/project_issue_milestone.query.graphql';
import projectMilestonesQuery from './queries/project_milestones.query.graphql';
-export const ASSIGNEES_DEBOUNCE_DELAY = 250;
+export const ASSIGNEES_DEBOUNCE_DELAY = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
+
+export const defaultEpicSort = 'TITLE_ASC';
export const assigneesQueries = {
[IssuableType.Issue]: {
@@ -87,6 +98,9 @@ export const referenceQueries = {
[IssuableType.MergeRequest]: {
query: mergeRequestReferenceQuery,
},
+ [IssuableType.Epic]: {
+ query: epicReferenceQuery,
+ },
};
export const dateTypes = {
@@ -122,6 +136,11 @@ export const subscribedQueries = {
},
};
+export const Tracking = {
+ editEvent: 'click_edit_button',
+ rightSidebarLabel: 'right_sidebar',
+};
+
export const timeTrackingQueries = {
[IssuableType.Issue]: {
query: issueTimeTrackingQuery,
@@ -165,12 +184,19 @@ export const issuableMilestoneQueries = {
query: projectIssueMilestoneQuery,
mutation: projectIssueMilestoneMutation,
},
+ [IssuableType.MergeRequest]: {
+ query: mergeRequestMilestone,
+ mutation: mergeRequestMilestoneMutation,
+ },
};
export const milestonesQueries = {
[IssuableType.Issue]: {
query: projectMilestonesQuery,
},
+ [IssuableType.MergeRequest]: {
+ query: projectMilestonesQuery,
+ },
};
export const IssuableAttributeType = {
@@ -187,3 +213,25 @@ export const issuableAttributesQueries = {
list: milestonesQueries,
},
};
+
+export const todoQueries = {
+ [IssuableType.Epic]: {
+ query: epicTodoQuery,
+ },
+ [IssuableType.Issue]: {
+ query: issueTodoQuery,
+ },
+ [IssuableType.MergeRequest]: {
+ query: mergeRequestTodoQuery,
+ },
+};
+
+export const TodoMutationTypes = {
+ Create: 'create',
+ MarkDone: 'mark-done',
+};
+
+export const todoMutations = {
+ [TodoMutationTypes.Create]: todoCreateMutation,
+ [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
+};
diff --git a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
index 21cd24b0842..5a3122e83d0 100644
--- a/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
+++ b/app/assets/javascripts/sidebar/lib/sidebar_move_issue.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import { escape } from 'lodash';
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
+import createFlash from '~/flash';
import { __ } from '~/locale';
function isValidProjectId(id) {
@@ -42,8 +43,10 @@ class SidebarMoveIssue {
this.mediator
.fetchAutocompleteProjects(searchTerm)
.then(callback)
- .catch(
- () => new window.Flash(__('An error occurred while fetching projects autocomplete.')),
+ .catch(() =>
+ createFlash({
+ message: __('An error occurred while fetching projects autocomplete.'),
+ }),
);
},
renderRow: (project) => `
@@ -76,7 +79,7 @@ class SidebarMoveIssue {
this.$confirmButton.disable().addClass('is-loading');
this.mediator.moveIssue().catch(() => {
- window.Flash(__('An error occurred while moving the issue.'));
+ createFlash({ message: __('An error occurred while moving the issue.') });
this.$confirmButton.enable().removeClass('is-loading');
});
}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index 67c72b17f1f..dd1b439c482 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -2,6 +2,8 @@ import $ from 'jquery';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createFlash from '~/flash';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+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 '~/issue_show/constants';
@@ -18,6 +20,8 @@ import SidebarConfidentialityWidget from '~/sidebar/components/confidential/side
import SidebarDueDateWidget from '~/sidebar/components/date/sidebar_date_widget.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 '~/sidebar/graphql';
import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import Translate from '../vue_shared/translate';
@@ -29,6 +33,7 @@ import SidebarReviewers from './components/reviewers/sidebar_reviewers.vue';
import SidebarSeverity from './components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from './components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTimeTracking from './components/time_tracking/sidebar_time_tracking.vue';
+import { IssuableAttributeType } from './constants';
import SidebarMoveIssue from './lib/sidebar_move_issue';
Vue.use(Translate);
@@ -38,6 +43,40 @@ function getSidebarOptions(sidebarOptEl = document.querySelector('.js-sidebar-op
return JSON.parse(sidebarOptEl.innerHTML);
}
+function mountSidebarToDoWidget() {
+ const el = document.querySelector('.js-issuable-todo');
+
+ if (!el) {
+ return false;
+ }
+
+ const { projectPath, iid, id } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ components: {
+ SidebarTodoWidget,
+ },
+ provide: {
+ isClassicSidebar: true,
+ },
+ render: (createElement) =>
+ createElement('sidebar-todo-widget', {
+ props: {
+ fullPath: projectPath,
+ issuableId:
+ isInIssuePage() || isInDesignPage()
+ ? convertToGraphQLId(TYPE_ISSUE, id)
+ : convertToGraphQLId(TYPE_MERGE_REQUEST, id),
+ issuableIid: iid,
+ issuableType:
+ isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
+ },
+ }),
+ });
+}
+
function getSidebarAssigneeAvailabilityData() {
const sidebarAssigneeEl = document.querySelectorAll('.js-sidebar-assignee-data input');
return Array.from(sidebarAssigneeEl)
@@ -154,7 +193,8 @@ function mountReviewersComponent(mediator) {
issuableIid: String(iid),
projectPath: fullPath,
field: el.dataset.field,
- issuableType: isInIssuePage() || isInDesignPage() ? 'issue' : 'merge_request',
+ issuableType:
+ isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
},
}),
});
@@ -166,6 +206,40 @@ function mountReviewersComponent(mediator) {
}
}
+function mountMilestoneSelect() {
+ const el = document.querySelector('.js-milestone-select');
+
+ if (!el) {
+ return false;
+ }
+
+ const { canEdit, projectPath, issueIid } = el.dataset;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ components: {
+ SidebarDropdownWidget,
+ },
+ provide: {
+ canUpdate: parseBoolean(canEdit),
+ isClassicSidebar: true,
+ },
+ render: (createElement) =>
+ createElement('sidebar-dropdown-widget', {
+ props: {
+ attrWorkspacePath: projectPath,
+ workspacePath: projectPath,
+ iid: issueIid,
+ issuableType:
+ isInIssuePage() || isInDesignPage() ? IssuableType.Issue : IssuableType.MergeRequest,
+ issuableAttribute: IssuableAttributeType.Milestone,
+ icon: 'clock',
+ },
+ }),
+ });
+}
+
export function mountSidebarLabels() {
const el = document.querySelector('.js-sidebar-labels');
@@ -460,12 +534,14 @@ export function mountSidebar(mediator) {
initInviteMembersModal();
initInviteMembersTrigger();
+ mountSidebarToDoWidget();
if (isAssigneesWidgetShown) {
mountAssigneesComponent();
} else {
mountAssigneesComponentDeprecated(mediator);
}
mountReviewersComponent(mediator);
+ mountMilestoneSelect();
mountConfidentialComponent(mediator);
mountDueDateComponent(mediator);
mountReferenceComponent(mediator);
diff --git a/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql b/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql
new file mode 100644
index 00000000000..bd10f09aed8
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_reference.query.graphql
@@ -0,0 +1,10 @@
+query epicReference($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ reference(full: true)
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql b/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql
new file mode 100644
index 00000000000..1e6f9bad5b2
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/epic_todo.query.graphql
@@ -0,0 +1,14 @@
+query epicTodos($fullPath: ID!, $iid: ID) {
+ workspace: group(fullPath: $fullPath) {
+ __typename
+ issuable: epic(iid: $iid) {
+ __typename
+ id
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql b/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql
new file mode 100644
index 00000000000..783d36352fe
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/issue_todo.query.graphql
@@ -0,0 +1,14 @@
+query issueTodos($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: issue(iid: $iid) {
+ __typename
+ id
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql
new file mode 100644
index 00000000000..5c0edf5acee
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/merge_request_milestone.query.graphql
@@ -0,0 +1,14 @@
+#import "./milestone.fragment.graphql"
+
+query mergeRequestMilestone($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: mergeRequest(iid: $iid) {
+ __typename
+ id
+ attribute: milestone {
+ ...MilestoneFragment
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql b/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql
new file mode 100644
index 00000000000..93a1c9ea925
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/merge_request_todo.query.graphql
@@ -0,0 +1,14 @@
+query mergeRequestTodos($fullPath: ID!, $iid: String!) {
+ workspace: project(fullPath: $fullPath) {
+ __typename
+ issuable: mergeRequest(iid: $iid) {
+ __typename
+ id
+ currentUserTodos(state: pending) {
+ nodes {
+ id
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
index 8db5359dac0..2ffd58a2da1 100644
--- a/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
+++ b/app/assets/javascripts/sidebar/queries/milestone.fragment.graphql
@@ -2,4 +2,5 @@ fragment MilestoneFragment on Milestone {
id
title
webUrl: webPath
+ expired
}
diff --git a/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql b/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql
index d88ad8b1087..721a71bef63 100644
--- a/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/project_issue_milestone.mutation.graphql
@@ -11,6 +11,7 @@ mutation projectIssueMilestoneMutation($fullPath: ID!, $iid: String!, $attribute
title
id
state
+ expired
}
}
}
diff --git a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
index 1237640c468..a3ab1ebc872 100644
--- a/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/project_milestones.query.graphql
@@ -3,7 +3,13 @@
query projectMilestones($fullPath: ID!, $title: String, $state: MilestoneStateEnum) {
workspace: project(fullPath: $fullPath) {
__typename
- attributes: milestones(searchTitle: $title, state: $state) {
+ attributes: milestones(
+ searchTitle: $title
+ state: $state
+ sort: EXPIRED_LAST_DUE_DATE_ASC
+ first: 20
+ includeAncestors: true
+ ) {
nodes {
...MilestoneFragment
state
diff --git a/app/assets/javascripts/sidebar/queries/todo_create.mutation.graphql b/app/assets/javascripts/sidebar/queries/todo_create.mutation.graphql
new file mode 100644
index 00000000000..4675db9153e
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/todo_create.mutation.graphql
@@ -0,0 +1,9 @@
+mutation issuableTodoCreate($input: TodoCreateInput!) {
+ todoMutation: todoCreate(input: $input) {
+ __typename
+ todo {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/todo_mark_done.mutation.graphql b/app/assets/javascripts/sidebar/queries/todo_mark_done.mutation.graphql
new file mode 100644
index 00000000000..8253e5e82bc
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/todo_mark_done.mutation.graphql
@@ -0,0 +1,9 @@
+mutation issuableTodoMarkDone($input: TodoMarkDoneInput!) {
+ todoMutation: todoMarkDone(input: $input) {
+ __typename
+ todo {
+ id
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
index b45b6b46c8f..28a47735143 100644
--- a/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
+++ b/app/assets/javascripts/sidebar/queries/updateStatus.mutation.graphql
@@ -1,6 +1,7 @@
mutation($projectPath: ID!, $iid: String!, $healthStatus: HealthStatus) {
updateIssue(input: { projectPath: $projectPath, iid: $iid, healthStatus: $healthStatus }) {
- issue {
+ issuable: issue {
+ id
healthStatus
}
errors
diff --git a/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql
new file mode 100644
index 00000000000..368f06fac7f
--- /dev/null
+++ b/app/assets/javascripts/sidebar/queries/update_merge_request_milestone.mutation.graphql
@@ -0,0 +1,17 @@
+mutation mergeRequestSetMilestone($fullPath: ID!, $iid: String!, $attributeId: ID) {
+ issuableSetAttribute: mergeRequestSetMilestone(
+ input: { projectPath: $fullPath, iid: $iid, milestoneId: $attributeId }
+ ) {
+ __typename
+ errors
+ issuable: mergeRequest {
+ __typename
+ id
+ attribute: milestone {
+ title
+ id
+ state
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/sidebar/services/sidebar_service.js b/app/assets/javascripts/sidebar/services/sidebar_service.js
index 88501f2c305..ace2a163adc 100644
--- a/app/assets/javascripts/sidebar/services/sidebar_service.js
+++ b/app/assets/javascripts/sidebar/services/sidebar_service.js
@@ -1,4 +1,5 @@
import sidebarDetailsIssueQuery from 'ee_else_ce/sidebar/queries/sidebarDetails.query.graphql';
+import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import createGqClient, { fetchPolicies } from '~/lib/graphql';
import axios from '~/lib/utils/axios_utils';
@@ -88,7 +89,7 @@ export default class SidebarService {
return gqClient.mutate({
mutation: reviewerRereviewMutation,
variables: {
- userId: convertToGraphQLId('User', `${userId}`), // eslint-disable-line @gitlab/require-i18n-strings
+ userId: convertToGraphQLId(TYPE_USER, `${userId}`),
projectPath: this.fullPath,
iid: this.iid.toString(),
},
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index 3595354da80..0a5e44a9b95 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -1,7 +1,7 @@
import Store from 'ee_else_ce/sidebar/stores/sidebar_store';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import toast from '~/vue_shared/plugins/global_toast';
-import { deprecatedCreateFlash as Flash } from '../flash';
import { visitUrl } from '../lib/utils/url_utility';
import Service from './services/sidebar_service';
@@ -74,7 +74,11 @@ export default class SidebarMediator {
.then(([restResponse, graphQlResponse]) => {
this.processFetchedData(restResponse.data, graphQlResponse.data);
})
- .catch(() => new Flash(__('Error occurred when fetching sidebar data')));
+ .catch(() =>
+ createFlash({
+ message: __('Error occurred when fetching sidebar data'),
+ }),
+ );
}
processFetchedData(data) {
diff --git a/app/assets/javascripts/smart_interval.js b/app/assets/javascripts/smart_interval.js
index 15d04dadb15..6d77952f24e 100644
--- a/app/assets/javascripts/smart_interval.js
+++ b/app/assets/javascripts/smart_interval.js
@@ -3,6 +3,35 @@ import $ from 'jquery';
/**
* Instances of SmartInterval extend the functionality of `setInterval`, make it configurable
* and controllable by a public API.
+ *
+ * This component has two intervals:
+ *
+ * - current interval - when the page is visible - defined by `startingInterval`, `maxInterval`, and `incrementByFactorOf`
+ * - Example:
+ * - `startingInterval: 10000`, `maxInterval: 240000`, `incrementByFactorOf: 2`
+ * - results in `10s, 20s, 40s, 80s, ..., 240s`, it stops increasing at `240s` and keeps this interval indefinitely.
+ * - hidden interval - when the page is not visible
+ *
+ * Visibility transitions:
+ *
+ * - `visible -> not visible`
+ * - `document.addEventListener('visibilitychange', () => ...)`
+ *
+ * > This event fires with a visibilityState of hidden when a user navigates to a new page, switches tabs, closes the tab, minimizes or closes the browser, or, on mobile, switches from the browser to a different app.
+ *
+ * Source [Document: visibilitychange event - Web APIs | MDN](https://developer.mozilla.org/en-US/docs/Web/API/Document/visibilitychange_event)
+ *
+ * - `window.addEventListener('blur', () => ...)` - every time user clicks somewhere else then in the browser page
+ * - `not visible -> visible`
+ * - `document.addEventListener('visibilitychange', () => ...)` same as the transition `visible -> not visible`
+ * - `window.addEventListener('focus', () => ...)`
+ *
+ * The combination of these two listeners can result in an unexpected resumption of polling:
+ *
+ * - switch to a different window (causes `blur`)
+ * - switch to a different desktop (causes `visibilitychange` (not visible))
+ * - switch back to the original desktop (causes `visibilitychange` (visible))
+ * - *now the polling happens even in window that user doesn't work in*
*/
export default class SmartInterval {
diff --git a/app/assets/javascripts/snippets/components/edit.vue b/app/assets/javascripts/snippets/components/edit.vue
index c53d0575752..f07fb9d926a 100644
--- a/app/assets/javascripts/snippets/components/edit.vue
+++ b/app/assets/javascripts/snippets/components/edit.vue
@@ -2,7 +2,7 @@
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import eventHub from '~/blob/components/eventhub';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { redirectTo, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import {
@@ -135,7 +135,9 @@ export default {
const defaultErrorMsg = this.newSnippet
? SNIPPET_CREATE_MUTATION_ERROR
: SNIPPET_UPDATE_MUTATION_ERROR;
- Flash(sprintf(defaultErrorMsg, { err }));
+ createFlash({
+ message: sprintf(defaultErrorMsg, { err }),
+ });
this.isUpdating = false;
},
getAttachedFiles() {
diff --git a/app/assets/javascripts/snippets/components/embed_dropdown.vue b/app/assets/javascripts/snippets/components/embed_dropdown.vue
index ad1b08a5a07..0fdbc89a038 100644
--- a/app/assets/javascripts/snippets/components/embed_dropdown.vue
+++ b/app/assets/javascripts/snippets/components/embed_dropdown.vue
@@ -60,7 +60,7 @@ export default {
class="gl-dropdown-text-py-0 gl-dropdown-text-block"
data-testid="input"
>
- <gl-form-input-group :value="value" readonly select-on-click :aria-label="name">
+ <gl-form-input-group :value="value" readonly select-on-click :label="name">
<template #append>
<gl-button
v-gl-tooltip.hover
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 612b4c7d2e3..fe169775f96 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -6,13 +6,13 @@ import axios from '~/lib/utils/axios_utils';
import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
import { SNIPPET_BLOB_CONTENT_FETCH_ERROR } from '~/snippets/constants';
-import EditorLite from '~/vue_shared/components/editor_lite.vue';
+import SourceEditor from '~/vue_shared/components/source_editor.vue';
export default {
components: {
BlobHeaderEdit,
GlLoadingIcon,
- EditorLite,
+ SourceEditor,
},
inheritAttrs: false,
props: {
@@ -85,7 +85,7 @@ export default {
size="lg"
class="loading-animation prepend-top-20 gl-mb-6"
/>
- <editor-lite
+ <source-editor
v-else
:value="blob.content"
:file-global-id="blob.id"
diff --git a/app/assets/javascripts/snippets/components/snippet_header.vue b/app/assets/javascripts/snippets/components/snippet_header.vue
index bf19b63650e..a8f95748e7e 100644
--- a/app/assets/javascripts/snippets/components/snippet_header.vue
+++ b/app/assets/javascripts/snippets/components/snippet_header.vue
@@ -274,7 +274,7 @@ export default {
data-qa-selector="delete_snippet_button"
@click="deleteSnippet"
>
- <gl-loading-icon v-if="isDeleting" inline />
+ <gl-loading-icon v-if="isDeleting" size="sm" inline />
{{ __('Delete snippet') }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/sortable/sortable_config.js b/app/assets/javascripts/sortable/sortable_config.js
index 43ef5d66422..a4c4cb7f101 100644
--- a/app/assets/javascripts/sortable/sortable_config.js
+++ b/app/assets/javascripts/sortable/sortable_config.js
@@ -4,4 +4,5 @@ export default {
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
+ fallbackTolerance: 1,
};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index eb3eaa66df5..7cba445d9b1 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { deprecatedCreateFlash as Flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
import { spriteIcon } from './lib/utils/common_utils';
import { __, s__ } from './locale';
@@ -28,7 +28,11 @@ export default class Star {
$this.prepend(spriteIcon('star', iconClasses));
}
})
- .catch(() => Flash(__('Star toggle failed. Try again later.')));
+ .catch(() =>
+ createFlash({
+ message: __('Star toggle failed. Try again later.'),
+ }),
+ );
});
}
}
diff --git a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
index 0685dfdb1d1..781e23cd6c8 100644
--- a/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
+++ b/app/assets/javascripts/static_site_editor/components/edit_drawer.vue
@@ -21,7 +21,7 @@ export default {
</script>
<template>
<gl-drawer class="gl-pt-8" :open="isOpen" @close="$emit('close')">
- <template #header>{{ __('Page settings') }}</template>
+ <template #title>{{ __('Page settings') }}</template>
<front-matter-controls :settings="settings" @updateSettings="$emit('updateSettings', $event)" />
</gl-drawer>
</template>
diff --git a/app/assets/javascripts/static_site_editor/constants.js b/app/assets/javascripts/static_site_editor/constants.js
index b08bf26e1dc..ab7fd0542bf 100644
--- a/app/assets/javascripts/static_site_editor/constants.js
+++ b/app/assets/javascripts/static_site_editor/constants.js
@@ -28,7 +28,8 @@ export const TRACKING_ACTION_CREATE_COMMIT = 'create_commit';
export const TRACKING_ACTION_CREATE_MERGE_REQUEST = 'create_merge_request';
export const TRACKING_ACTION_INITIALIZE_EDITOR = 'initialize_editor';
-export const USAGE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
-export const USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST = 'static_site_editor_merge_requests';
+export const SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT = 'static_site_editor_commits';
+export const SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST =
+ 'static_site_editor_merge_requests';
export const MR_META_LOCAL_STORAGE_KEY = 'sse-merge-request-meta-storage-key';
diff --git a/app/assets/javascripts/static_site_editor/image_repository.js b/app/assets/javascripts/static_site_editor/image_repository.js
index 57f32ab4847..4ad2e2618ac 100644
--- a/app/assets/javascripts/static_site_editor/image_repository.js
+++ b/app/assets/javascripts/static_site_editor/image_repository.js
@@ -1,10 +1,13 @@
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import { getBinary } from './services/image_service';
const imageRepository = () => {
const images = new Map();
- const flash = (message) => new Flash(message);
+ const flash = (message) =>
+ createFlash({
+ message,
+ });
const add = (file, url) => {
getBinary(file)
diff --git a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
index 99bb2080610..5ce2c17f8de 100644
--- a/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
+++ b/app/assets/javascripts/static_site_editor/rich_content_editor/modals/insert_video_modal.vue
@@ -81,11 +81,13 @@ export default {
:invalid-feedback="urlError"
>
<gl-form-input id="video-modal-url-input" ref="urlInput" v-model="url" />
- <gl-sprintf slot="description" :message="description" class="text-gl-muted">
- <template #id>
- <strong>{{ __('0t1DgySidms') }}</strong>
- </template>
- </gl-sprintf>
+ <template #description>
+ <gl-sprintf :message="description" class="text-gl-muted">
+ <template #id>
+ <strong>{{ __('0t1DgySidms') }}</strong>
+ </template>
+ </gl-sprintf>
+ </template>
</gl-form-group>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
index ecb7f60a421..99534413d92 100644
--- a/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
+++ b/app/assets/javascripts/static_site_editor/services/submit_content_changes.js
@@ -9,8 +9,8 @@ import {
SUBMIT_CHANGES_MERGE_REQUEST_ERROR,
TRACKING_ACTION_CREATE_COMMIT,
TRACKING_ACTION_CREATE_MERGE_REQUEST,
- USAGE_PING_TRACKING_ACTION_CREATE_COMMIT,
- USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
+ SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT,
+ SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST,
DEFAULT_FORMATTING_CHANGES_COMMIT_MESSAGE,
DEFAULT_FORMATTING_CHANGES_COMMIT_DESCRIPTION,
} from '../constants';
@@ -58,7 +58,7 @@ const createUpdateSourceFileAction = (sourcePath, content) => [
const commit = (projectId, message, branch, actions) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_COMMIT);
- Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_COMMIT);
+ Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_COMMIT);
return Api.commitMultiple(
projectId,
@@ -74,7 +74,7 @@ const commit = (projectId, message, branch, actions) => {
const createMergeRequest = (projectId, title, description, sourceBranch, targetBranch) => {
Tracking.event(document.body.dataset.page, TRACKING_ACTION_CREATE_MERGE_REQUEST);
- Api.trackRedisCounterEvent(USAGE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
+ Api.trackRedisCounterEvent(SERVICE_PING_TRACKING_ACTION_CREATE_MERGE_REQUEST);
return Api.createProjectMergeRequest(
projectId,
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index 3b2210b9ef2..93353b400e5 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
-import { deprecatedCreateFlash as Flash } from './flash';
+import createFlash from './flash';
import axios from './lib/utils/axios_utils';
export default class TaskList {
@@ -22,7 +22,9 @@ export default class TaskList {
errorMessages = e.response.data.errors.join(' ');
}
- return new Flash(errorMessages || __('Update failed'), 'alert');
+ return createFlash({
+ message: errorMessages || __('Update failed'),
+ });
};
this.init();
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index 2577664a5e8..d066834540f 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -137,7 +137,7 @@ export default {
<div v-if="item.loadingLock" class="gl-mx-3">
<p class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0">
- <gl-loading-icon class="gl-pr-1" />
+ <gl-loading-icon size="sm" class="gl-pr-1" />
{{ loadingLockText(item) }}
</p>
</div>
@@ -146,7 +146,7 @@ export default {
<p
class="gl-display-flex gl-justify-content-start gl-align-items-baseline gl-m-0 gl-text-red-500"
>
- <gl-loading-icon class="gl-pr-1" />
+ <gl-loading-icon size="sm" class="gl-pr-1" />
{{ $options.i18n.removing }}
</p>
</div>
diff --git a/app/assets/javascripts/terraform/components/terraform_list.vue b/app/assets/javascripts/terraform/components/terraform_list.vue
index a18f33ebb1f..7eb79120fb8 100644
--- a/app/assets/javascripts/terraform/components/terraform_list.vue
+++ b/app/assets/javascripts/terraform/components/terraform_list.vue
@@ -98,7 +98,7 @@ export default {
<section>
<gl-tabs>
<gl-tab>
- <template slot="title">
+ <template #title>
<p class="gl-m-0">
{{ s__('Terraform|States') }}
<gl-badge v-if="statesCount">{{ statesCount }}</gl-badge>
diff --git a/app/assets/javascripts/toggle_buttons.js b/app/assets/javascripts/toggle_buttons.js
index 03c975d5fe8..5b85107991a 100644
--- a/app/assets/javascripts/toggle_buttons.js
+++ b/app/assets/javascripts/toggle_buttons.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import { deprecatedCreateFlash as Flash } from './flash';
+import createFlash from './flash';
import { parseBoolean } from './lib/utils/common_utils';
import { __ } from './locale';
@@ -42,7 +42,9 @@ function onToggleClicked(toggle, input, clickCallback) {
$(input).trigger('trigger-change');
})
.catch(() => {
- Flash(__('Something went wrong when toggling the button'));
+ createFlash({
+ message: __('Something went wrong when toggling the button'),
+ });
});
}
diff --git a/app/assets/javascripts/token_access/components/token_access.vue b/app/assets/javascripts/token_access/components/token_access.vue
new file mode 100644
index 00000000000..24565c441d8
--- /dev/null
+++ b/app/assets/javascripts/token_access/components/token_access.vue
@@ -0,0 +1,206 @@
+<script>
+import { GlButton, GlCard, GlFormInput, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import createFlash from '~/flash';
+import { __, s__ } from '~/locale';
+import addProjectCIJobTokenScopeMutation from '../graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
+import removeProjectCIJobTokenScopeMutation from '../graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
+import updateCIJobTokenScopeMutation from '../graphql/mutations/update_ci_job_token_scope.mutation.graphql';
+import getCIJobTokenScopeQuery from '../graphql/queries/get_ci_job_token_scope.query.graphql';
+import getProjectsWithCIJobTokenScopeQuery from '../graphql/queries/get_projects_with_ci_job_token_scope.query.graphql';
+import TokenProjectsTable from './token_projects_table.vue';
+
+export default {
+ i18n: {
+ toggleLabelTitle: s__('CICD|Limit CI_JOB_TOKEN access'),
+ toggleHelpText: s__(
+ `CICD|Select projects that can be accessed by API requests authenticated with this project's CI_JOB_TOKEN CI/CD variable.`,
+ ),
+ cardHeaderTitle: s__('CICD|Add an existing project to the scope'),
+ addProject: __('Add project'),
+ cancel: __('Cancel'),
+ addProjectPlaceholder: __('Paste project path (i.e. gitlab-org/gitlab)'),
+ projectsFetchError: __('There was a problem fetching the projects'),
+ scopeFetchError: __('There was a problem fetching the job token scope value'),
+ },
+ components: {
+ GlButton,
+ GlCard,
+ GlFormInput,
+ GlLoadingIcon,
+ GlToggle,
+ TokenProjectsTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ apollo: {
+ jobTokenScopeEnabled: {
+ query: getCIJobTokenScopeQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return data.project.ciCdSettings.jobTokenScopeEnabled;
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.scopeFetchError });
+ },
+ },
+ projects: {
+ query: getProjectsWithCIJobTokenScopeQuery,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ };
+ },
+ update(data) {
+ return data.project?.ciJobTokenScope?.projects?.nodes ?? [];
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.projectsFetchError });
+ },
+ },
+ },
+ data() {
+ return {
+ jobTokenScopeEnabled: null,
+ targetProjectPath: '',
+ projects: [],
+ };
+ },
+ computed: {
+ isProjectPathEmpty() {
+ return this.targetProjectPath === '';
+ },
+ },
+ methods: {
+ async updateCIJobTokenScope() {
+ try {
+ const {
+ data: {
+ ciCdSettingsUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateCIJobTokenScopeMutation,
+ variables: {
+ input: {
+ fullPath: this.fullPath,
+ jobTokenScopeEnabled: this.jobTokenScopeEnabled,
+ },
+ },
+ });
+
+ if (errors.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ createFlash({ message: error });
+ } finally {
+ if (this.jobTokenScopeEnabled) {
+ this.getProjects();
+ }
+ }
+ },
+ async addProject() {
+ try {
+ const {
+ data: {
+ ciJobTokenScopeAddProject: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: addProjectCIJobTokenScopeMutation,
+ variables: {
+ input: {
+ projectPath: this.fullPath,
+ targetProjectPath: this.targetProjectPath,
+ },
+ },
+ });
+
+ if (errors.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ createFlash({ message: error });
+ } finally {
+ this.clearTargetProjectPath();
+ this.getProjects();
+ }
+ },
+ async removeProject(removeTargetPath) {
+ try {
+ const {
+ data: {
+ ciJobTokenScopeRemoveProject: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: removeProjectCIJobTokenScopeMutation,
+ variables: {
+ input: {
+ projectPath: this.fullPath,
+ targetProjectPath: removeTargetPath,
+ },
+ },
+ });
+
+ if (errors.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ createFlash({ message: error });
+ } finally {
+ this.getProjects();
+ }
+ },
+ clearTargetProjectPath() {
+ this.targetProjectPath = '';
+ },
+ getProjects() {
+ this.$apollo.queries.projects.refetch();
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-loading-icon v-if="$apollo.loading" size="md" class="gl-mt-5" />
+ <template v-else>
+ <gl-toggle
+ v-model="jobTokenScopeEnabled"
+ :label="$options.i18n.toggleLabelTitle"
+ :help="$options.i18n.toggleHelpText"
+ @change="updateCIJobTokenScope"
+ />
+ <div v-if="jobTokenScopeEnabled" data-testid="token-section">
+ <gl-card class="gl-mt-5">
+ <template #header>
+ <h5 class="gl-my-0">{{ $options.i18n.cardHeaderTitle }}</h5>
+ </template>
+ <template #default>
+ <gl-form-input
+ v-model="targetProjectPath"
+ :placeholder="$options.i18n.addProjectPlaceholder"
+ />
+ </template>
+ <template #footer>
+ <gl-button
+ variant="confirm"
+ :disabled="isProjectPathEmpty"
+ data-testid="add-project-button"
+ @click="addProject"
+ >
+ {{ $options.i18n.addProject }}
+ </gl-button>
+ <gl-button @click="clearTargetProjectPath">{{ $options.i18n.cancel }}</gl-button>
+ </template>
+ </gl-card>
+
+ <token-projects-table :projects="projects" @removeProject="removeProject" />
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/token_access/components/token_projects_table.vue b/app/assets/javascripts/token_access/components/token_projects_table.vue
new file mode 100644
index 00000000000..777eda1c4d7
--- /dev/null
+++ b/app/assets/javascripts/token_access/components/token_projects_table.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlButton, GlTable } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+
+const defaultTableClasses = {
+ thClass: 'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!',
+};
+
+export default {
+ i18n: {
+ emptyText: s__('CI/CD|No projects have been added to the scope'),
+ },
+ fields: [
+ {
+ key: 'project',
+ label: __('Projects that can be accessed'),
+ tdClass: 'gl-p-5!',
+ ...defaultTableClasses,
+ columnClass: 'gl-w-85p',
+ },
+ {
+ key: 'actions',
+ label: '',
+ tdClass: 'gl-p-5! gl-text-right',
+ ...defaultTableClasses,
+ columnClass: 'gl-w-15p',
+ },
+ ],
+ components: {
+ GlButton,
+ GlTable,
+ },
+ inject: {
+ fullPath: {
+ default: '',
+ },
+ },
+ props: {
+ projects: {
+ type: Array,
+ required: true,
+ },
+ },
+ methods: {
+ removeProject(project) {
+ this.$emit('removeProject', project);
+ },
+ },
+};
+</script>
+<template>
+ <gl-table
+ :items="projects"
+ :fields="$options.fields"
+ :tbody-tr-attr="{ 'data-testid': 'projects-token-table-row' }"
+ :empty-text="$options.i18n.emptyText"
+ show-empty
+ stacked="sm"
+ fixed
+ >
+ <template #table-colgroup="{ fields }">
+ <col v-for="field in fields" :key="field.key" :class="field.columnClass" />
+ </template>
+
+ <template #cell(project)="{ item }">
+ {{ item.name }}
+ </template>
+
+ <template #cell(actions)="{ item }">
+ <gl-button
+ v-if="item.fullPath !== fullPath"
+ category="primary"
+ variant="danger"
+ icon="remove"
+ :aria-label="__('Remove access')"
+ data-testid="remove-project-button"
+ @click="removeProject(item.fullPath)"
+ />
+ </template>
+ </gl-table>
+</template>
diff --git a/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql
new file mode 100644
index 00000000000..0a7c76dd580
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql
@@ -0,0 +1,5 @@
+mutation addProjectCIJobTokenScope($input: CiJobTokenScopeAddProjectInput!) {
+ ciJobTokenScopeAddProject(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql
new file mode 100644
index 00000000000..5107ea30cd1
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql
@@ -0,0 +1,5 @@
+mutation removeProjectCIJobTokenScope($input: CiJobTokenScopeRemoveProjectInput!) {
+ ciJobTokenScopeRemoveProject(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql b/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql
new file mode 100644
index 00000000000..d99f2e3597d
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql
@@ -0,0 +1,8 @@
+mutation updateCIJobTokenScope($input: CiCdSettingsUpdateInput!) {
+ ciCdSettingsUpdate(input: $input) {
+ ciCdSettings {
+ jobTokenScopeEnabled
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql
new file mode 100644
index 00000000000..d4f559c3701
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/queries/get_ci_job_token_scope.query.graphql
@@ -0,0 +1,7 @@
+query getCIJobTokenScope($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ ciCdSettings {
+ jobTokenScopeEnabled
+ }
+ }
+}
diff --git a/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
new file mode 100644
index 00000000000..bec0710a1dd
--- /dev/null
+++ b/app/assets/javascripts/token_access/graphql/queries/get_projects_with_ci_job_token_scope.query.graphql
@@ -0,0 +1,12 @@
+query getProjectsWithCIJobTokenScope($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ ciJobTokenScope {
+ projects {
+ nodes {
+ name
+ fullPath
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/token_access/index.js b/app/assets/javascripts/token_access/index.js
new file mode 100644
index 00000000000..6a29883290a
--- /dev/null
+++ b/app/assets/javascripts/token_access/index.js
@@ -0,0 +1,31 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import TokenAccess from './components/token_access.vue';
+
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
+
+export const initTokenAccess = (containerId = 'js-ci-token-access-app') => {
+ const containerEl = document.getElementById(containerId);
+
+ if (!containerEl) {
+ return false;
+ }
+
+ const { fullPath } = containerEl.dataset;
+
+ return new Vue({
+ el: containerEl,
+ apolloProvider,
+ provide: {
+ fullPath,
+ },
+ render(createElement) {
+ return createElement(TokenAccess);
+ },
+ });
+};
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index e0ba7dba97f..3714cac3fba 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -34,6 +34,12 @@ const addExperimentContext = (opts) => {
return options;
};
+const renameKey = (o, oldKey, newKey) => {
+ const ret = {};
+ delete Object.assign(ret, o, { [newKey]: o[oldKey] })[oldKey];
+ return ret;
+};
+
const createEventPayload = (el, { suffix = '' } = {}) => {
const {
trackAction,
@@ -186,15 +192,18 @@ export default class Tracking {
(context) => context.schema !== standardContext.schema,
);
- const mappedConfig = {
- forms: { whitelist: config.forms?.allow || [] },
- fields: { whitelist: config.fields?.allow || [] },
- };
+ const mappedConfig = {};
+ if (config.forms) mappedConfig.forms = renameKey(config.forms, 'allow', 'whitelist');
+ if (config.fields) mappedConfig.fields = renameKey(config.fields, 'allow', 'whitelist');
const enabler = () => window.snowplow('enableFormTracking', mappedConfig, userProvidedContexts);
- if (document.readyState !== 'loading') enabler();
- else document.addEventListener('DOMContentLoaded', enabler);
+ if (document.readyState === 'complete') enabler();
+ else {
+ document.addEventListener('readystatechange', () => {
+ if (document.readyState === 'complete') enabler();
+ });
+ }
}
static mixin(opts = {}) {
diff --git a/app/assets/javascripts/user_lists/components/user_lists.vue b/app/assets/javascripts/user_lists/components/user_lists.vue
index 80be894c689..0e3c6b396db 100644
--- a/app/assets/javascripts/user_lists/components/user_lists.vue
+++ b/app/assets/javascripts/user_lists/components/user_lists.vue
@@ -3,12 +3,8 @@ import { GlBadge, GlButton } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { mapState, mapActions } from 'vuex';
import EmptyState from '~/feature_flags/components/empty_state.vue';
-import {
- buildUrlWithCurrentLocation,
- getParameterByName,
- historyPushState,
-} from '~/lib/utils/common_utils';
-import { objectToQuery } from '~/lib/utils/url_utility';
+import { buildUrlWithCurrentLocation, historyPushState } from '~/lib/utils/common_utils';
+import { objectToQuery, getParameterByName } from '~/lib/utils/url_utility';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import UserListsTable from './user_lists_table.vue';
diff --git a/app/assets/javascripts/user_popovers.js b/app/assets/javascripts/user_popovers.js
index 21368edb6af..0e25f71fe05 100644
--- a/app/assets/javascripts/user_popovers.js
+++ b/app/assets/javascripts/user_popovers.js
@@ -44,6 +44,7 @@ const populateUserInfo = (user) => {
bioHtml: sanitize(userData.bio_html),
workInformation: userData.work_information,
websiteUrl: userData.website_url,
+ pronouns: userData.pronouns,
loaded: true,
});
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue
index dc766176617..68f4609f14d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/artifacts_list.vue
@@ -14,27 +14,25 @@ export default {
};
</script>
<template>
- <table class="table m-0">
- <thead class="thead-white text-nowrap">
- <tr class="d-none d-sm-table-row">
- <th class="w-0"></th>
- <th>{{ __('Artifact') }}</th>
- <th class="w-50"></th>
- <th>{{ __('Job') }}</th>
- </tr>
- </thead>
+ <div class="gl-pl-7">
+ <table class="table m-0">
+ <thead class="thead-white text-nowrap">
+ <tr class="d-none d-sm-table-row">
+ <th>{{ __('Artifact') }}</th>
+ <th>{{ __('Job') }}</th>
+ </tr>
+ </thead>
- <tbody>
- <tr v-for="item in artifacts" :key="item.text">
- <td class="w-0"></td>
- <td>
- <gl-link :href="item.url" target="_blank">{{ item.text }}</gl-link>
- </td>
- <td class="w-0"></td>
- <td>
- <gl-link :href="item.job_path">{{ item.job_name }}</gl-link>
- </td>
- </tr>
- </tbody>
- </table>
+ <tbody>
+ <tr v-for="item in artifacts" :key="item.text">
+ <td>
+ <gl-link :href="item.url" target="_blank">{{ item.text }}</gl-link>
+ </td>
+ <td>
+ <gl-link :href="item.job_path">{{ item.job_name }}</gl-link>
+ </td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
</template>
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 410d2740e1d..bb1837399ed 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
@@ -136,7 +136,7 @@ export default {
<template>
<div class="mr-info-list clearfix mr-memory-usage js-mr-memory-usage">
<p v-if="shouldShowLoading" class="usage-info js-usage-info usage-info-loading">
- <gl-loading-icon class="usage-info-load-spinner" />{{
+ <gl-loading-icon size="sm" class="usage-info-load-spinner" />{{
s__('mrWidget|Loading deployment statistics')
}}
</p>
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 33809b953ee..0ac98f6c982 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
@@ -122,7 +122,7 @@ export default {
</div>
<div v-if="!isCollapsed" class="mr-widget-grouped-section">
<div v-if="isLoadingExpanded" class="report-block-container">
- <gl-loading-icon inline /> {{ __('Loading...') }}
+ <gl-loading-icon size="sm" inline /> {{ __('Loading...') }}
</div>
<smart-virtual-list
v-else-if="fullData"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
index a619ae9c351..b75f2dce54e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_collapsible_extension.vue
@@ -58,13 +58,13 @@ export default {
<template v-else>
<button
- class="btn-blank btn s32 square gl-mr-3"
+ class="btn-blank btn s32 square"
type="button"
:aria-label="ariaLabel"
:disabled="isLoading"
@click="toggleCollapsed"
>
- <gl-loading-icon v-if="isLoading" />
+ <gl-loading-icon v-if="isLoading" size="sm" />
<gl-icon v-else :name="arrowIconName" class="js-icon" />
</button>
<template v-if="isCollapsed">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
index f1230e2fdeb..5e401fc17e9 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue
@@ -1,16 +1,17 @@
<script>
-/* eslint-disable vue/no-v-html */
import {
GlButton,
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
+ GlLink,
GlTooltipDirective,
GlModalDirective,
+ GlSafeHtmlDirective as SafeHtml,
+ GlSprintf,
} from '@gitlab/ui';
-import { escape } from 'lodash';
import { mergeUrlParams, webIDEUrl } from '~/lib/utils/url_utility';
-import { n__, s__, sprintf } from '~/locale';
+import { s__ } from '~/locale';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate.vue';
import MrWidgetHowToMergeModal from './mr_widget_how_to_merge_modal.vue';
@@ -27,10 +28,13 @@ export default {
GlDropdown,
GlDropdownSectionHeader,
GlDropdownItem,
+ GlLink,
+ GlSprintf,
},
directives: {
GlTooltip: GlTooltipDirective,
GlModalDirective,
+ SafeHtml,
},
props: {
mr: {
@@ -42,19 +46,6 @@ export default {
shouldShowCommitsBehindText() {
return this.mr.divergedCommitsCount > 0;
},
- commitsBehindText() {
- return sprintf(
- s__(
- 'mrWidget|The source branch is %{commitsBehindLinkStart}%{commitsBehind}%{commitsBehindLinkEnd} the target branch',
- ),
- {
- commitsBehindLinkStart: `<a href="${escape(this.mr.targetBranchPath)}">`,
- commitsBehind: n__('%d commit behind', '%d commits behind', this.mr.divergedCommitsCount),
- commitsBehindLinkEnd: '</a>',
- },
- false,
- );
- },
branchNameClipboardData() {
// This supports code in app/assets/javascripts/copy_to_clipboard.js that
// works around ClipboardJS limitations to allow the context-specific
@@ -100,10 +91,10 @@ export default {
<strong>
{{ s__('mrWidget|Request to merge') }}
<tooltip-on-truncate
+ v-safe-html="mr.sourceBranchLink"
:title="mr.sourceBranch"
truncate-target="child"
class="label-branch label-truncate js-source-branch"
- v-html="mr.sourceBranchLink"
/><clipboard-button
data-testid="mr-widget-copy-clipboard"
:text="branchNameClipboardData"
@@ -119,11 +110,15 @@ export default {
<a :href="mr.targetBranchTreePath" class="js-target-branch"> {{ mr.targetBranch }} </a>
</tooltip-on-truncate>
</strong>
- <div
- v-if="shouldShowCommitsBehindText"
- class="diverged-commits-count"
- v-html="commitsBehindText"
- ></div>
+ <div v-if="shouldShowCommitsBehindText" class="diverged-commits-count">
+ <gl-sprintf :message="s__('mrWidget|The source branch is %{link} the target branch')">
+ <template #link>
+ <gl-link :href="mr.targetBranchPath">{{
+ n__('%d commit behind', '%d commits behind', mr.divergedCommitsCount)
+ }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
</div>
<div class="branch-actions d-flex">
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 6c162a06161..9bb955c534f 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
@@ -171,7 +171,7 @@ export default {
<template v-else-if="!hasPipeline">
<gl-loading-icon size="md" />
<p
- class="gl-flex-grow-1 gl-display-flex gl-ml-5 gl-mb-0"
+ class="gl-flex-grow-1 gl-display-flex gl-ml-3 gl-mb-0"
data-testid="monitoring-pipeline-message"
>
{{ $options.monitoringPipelineText }}
@@ -190,7 +190,7 @@ export default {
</p>
</template>
<template v-else-if="hasPipeline">
- <a :href="status.details_path" class="align-self-start gl-mr-3">
+ <a :href="status.details_path" class="gl-align-self-center gl-mr-3">
<ci-icon :status="status" :size="24" />
</a>
<div class="ci-widget-container d-flex">
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
index 0cd280c42d2..f99b825ff30 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue
@@ -2,10 +2,10 @@
import { GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
import autoMergeMixin from 'ee_else_ce/vue_merge_request_widget/mixins/auto_merge';
import autoMergeEnabledQuery from 'ee_else_ce/vue_merge_request_widget/queries/states/auto_merge_enabled.query.graphql';
+import createFlash from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { deprecatedCreateFlash as Flash } from '../../../flash';
import { AUTO_MERGE_STRATEGIES } from '../../constants';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -109,7 +109,9 @@ export default {
})
.catch(() => {
this.isCancellingAutoMerge = false;
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
});
},
removeSourceBranch() {
@@ -135,7 +137,9 @@ export default {
})
.catch(() => {
this.isRemovingSourceBranch = false;
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
});
},
},
@@ -173,7 +177,7 @@ export default {
data-testid="cancelAutomaticMergeButton"
@click.prevent="cancelAutomaticMerge"
>
- <gl-loading-icon v-if="isCancellingAutoMerge" inline class="gl-mr-1" />
+ <gl-loading-icon v-if="isCancellingAutoMerge" size="sm" inline class="gl-mr-1" />
{{ cancelButtonText }}
</a>
</h4>
@@ -196,7 +200,7 @@ export default {
data-testid="removeSourceBranchButton"
@click.prevent="removeSourceBranch"
>
- <gl-loading-icon v-if="isRemovingSourceBranch" inline class="gl-mr-1" />
+ <gl-loading-icon v-if="isRemovingSourceBranch" size="sm" inline class="gl-mr-1" />
{{ s__('mrWidget|Delete source branch') }}
</a>
</p>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
index 5f8630bf7b3..1a764d3d091 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue
@@ -63,7 +63,7 @@ export default {
size="small"
@click="refreshWidget"
>
- <gl-loading-icon v-if="isRefreshing" :inline="true" />
+ <gl-loading-icon v-if="isRefreshing" size="sm" :inline="true" />
{{ s__('mrWidget|Refresh') }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index ee90d734ecb..5a93021978c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
@@ -112,7 +112,7 @@ export default {
<div v-else class="media-body space-children gl-display-flex gl-align-items-center">
<span v-if="shouldBeRebased" class="bold">
{{
- s__(`mrWidget|Fast-forward merge is not possible.
+ s__(`mrWidget|Merge blocked: fast-forward merge is not possible.
To merge this request, first rebase locally.`)
}}
</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
index 9da3bea9362..5177eab790b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue
@@ -1,14 +1,13 @@
<script>
/* eslint-disable @gitlab/vue-require-i18n-strings */
-import { GlLoadingIcon, GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import { GlLoadingIcon, GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import createFlash from '~/flash';
import { s__, __ } from '~/locale';
import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants';
import modalEventHub from '~/projects/commit/event_hub';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import eventHub from '../../event_hub';
import MrWidgetAuthorTime from '../mr_widget_author_time.vue';
-import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetMerged',
@@ -17,7 +16,7 @@ export default {
},
components: {
MrWidgetAuthorTime,
- statusIcon,
+ GlIcon,
ClipboardButton,
GlLoadingIcon,
GlButton,
@@ -100,7 +99,9 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
});
},
openRevertModal() {
@@ -114,7 +115,7 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
- <status-icon status="success" />
+ <gl-icon name="merge" :size="24" class="gl-text-blue-500 gl-mr-3 gl-mt-1" />
<div class="media-body">
<div class="space-children">
<mr-widget-author-time
@@ -129,7 +130,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
- variant="warning"
data-qa-selector="revert_button"
@click="openRevertModal"
>
@@ -142,7 +142,6 @@ export default {
:title="revertTitle"
size="small"
category="secondary"
- variant="warning"
data-method="post"
>
{{ revertLabel }}
@@ -167,6 +166,15 @@ export default {
>
{{ cherryPickLabel }}
</gl-button>
+ <gl-button
+ v-if="shouldShowRemoveSourceBranch"
+ :disabled="isMakingRequest"
+ size="small"
+ class="js-remove-branch-button"
+ @click="removeSourceBranch"
+ >
+ {{ s__('mrWidget|Delete source branch') }}
+ </gl-button>
</div>
<section class="mr-info-list" data-qa-selector="merged_status_content">
<p>
@@ -194,19 +202,8 @@ export default {
<p v-if="mr.sourceBranchRemoved">
{{ s__('mrWidget|The source branch has been deleted') }}
</p>
- <p v-if="shouldShowRemoveSourceBranch" class="space-children">
- <span>{{ s__('mrWidget|You can delete the source branch now') }}</span>
- <gl-button
- :disabled="isMakingRequest"
- size="small"
- class="js-remove-branch-button"
- @click="removeSourceBranch"
- >
- {{ s__('mrWidget|Delete source branch') }}
- </gl-button>
- </p>
<p v-if="shouldShowSourceBranchRemoving">
- <gl-loading-icon :inline="true" />
+ <gl-loading-icon size="sm" :inline="true" />
<span> {{ s__('mrWidget|The source branch is being deleted') }} </span>
</p>
</section>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index a82a8a22873..22f41b43095 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -2,9 +2,9 @@
/* eslint-disable vue/no-v-html */
import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { escape } from 'lodash';
+import createFlash from '~/flash';
import { __, sprintf } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { deprecatedCreateFlash as Flash } from '../../../flash';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
@@ -87,9 +87,7 @@ export default {
},
fastForwardMergeText() {
return sprintf(
- __(
- 'Fast-forward merge is not possible. Rebase the source branch onto %{targetBranch} to allow this merge request to be merged.',
- ),
+ __('Merge blocked: the source branch must be rebased onto the target branch.'),
{
targetBranch: `<span class="label-branch">${escape(this.targetBranch)}</span>`,
},
@@ -113,7 +111,9 @@ export default {
if (error.response && error.response.data && error.response.data.merge_error) {
this.rebasingError = error.response.data.merge_error;
} else {
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
}
});
},
@@ -129,7 +129,9 @@ export default {
if (res.merge_error && res.merge_error.length) {
this.rebasingError = res.merge_error;
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
}
eventHub.$emit('MRWidgetRebaseSuccess');
@@ -138,7 +140,9 @@ export default {
})
.catch(() => {
this.isMakingRequest = false;
- Flash(__('Something went wrong. Please try again.'));
+ createFlash({
+ message: __('Something went wrong. Please try again.'),
+ });
stopPolling();
});
},
@@ -187,9 +191,7 @@ export default {
data-testid="rebase-message"
data-qa-selector="no_fast_forward_message_content"
>{{
- __(
- 'Fast-forward merge is not possible. Rebase the source branch onto the target branch.',
- )
+ __('Merge blocked: the source branch must be rebased onto the target branch.')
}}</span
>
<span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{
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 07de525b1fa..2d0b7fe46a6 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
@@ -412,7 +412,6 @@ export default {
// If state is merged we should update the widget and stop the polling
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('FetchActionsContent');
- MergeRequest.setStatusBoxToMerged();
MergeRequest.hideCloseButton();
MergeRequest.decreaseCounter();
stopPolling();
@@ -629,11 +628,9 @@ export default {
input-id="squash-message-edit"
squash
>
- <commit-message-dropdown
- slot="header"
- v-model="squashCommitMessage"
- :commits="commits"
- />
+ <template #header>
+ <commit-message-dropdown v-model="squashCommitMessage" :commits="commits" />
+ </template>
</commit-edit>
<commit-edit
v-if="shouldShowMergeEdit"
@@ -641,14 +638,16 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
>
- <label slot="checkbox">
- <input
- id="include-description"
- type="checkbox"
- @change="updateMergeCommitMessage($event.target.checked)"
- />
- {{ __('Include merge request description') }}
- </label>
+ <template #checkbox>
+ <label>
+ <input
+ id="include-description"
+ type="checkbox"
+ @change="updateMergeCommitMessage($event.target.checked)"
+ />
+ {{ __('Include merge request description') }}
+ </label>
+ </template>
</commit-edit>
</ul>
</commits-header>
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 e9dcf494099..5fe04269e33 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
@@ -5,12 +5,12 @@ import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/ap
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';
import stateMaps from 'ee_else_ce/vue_merge_request_widget/stores/state_maps';
+import createFlash from '~/flash';
import { secondsToMilliseconds } from '~/lib/utils/datetime_utility';
import notify from '~/lib/utils/notify';
import { sprintf, s__, __ } from '~/locale';
import Project from '~/pages/projects/project';
import SmartInterval from '~/smart_interval';
-import createFlash from '../flash';
import { setFaviconOverlay } from '../lib/utils/favicon';
import GroupedAccessibilityReportsApp from '../reports/accessibility_report/grouped_accessibility_reports_app.vue';
import GroupedCodequalityReportsApp from '../reports/codequality_report/grouped_codequality_reports_app.vue';
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 4cc2f423d73..8e3160ce2f2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -1,10 +1,11 @@
-import { format } from 'timeago.js';
import getStateKey from 'ee_else_ce/vue_merge_request_widget/stores/get_state_key';
import { statusBoxState } from '~/issuable/components/status_box.vue';
-import { formatDate } from '../../lib/utils/datetime_utility';
+import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
import { MTWPS_MERGE_STRATEGY, MT_MERGE_STRATEGY, MWPS_MERGE_STRATEGY } from '../constants';
import { stateKey } from './state_maps';
+const { format } = getTimeago();
+
export default class MergeRequestStore {
constructor(data) {
this.sha = data.diff_head_sha;
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 b7544a4a5d0..c24318cb9ad 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
@@ -204,7 +204,7 @@ export default {
@click="$emit('toggle-sidebar')"
>
<gl-icon name="user" />
- <gl-loading-icon v-if="isUpdating" />
+ <gl-loading-icon v-if="isUpdating" size="sm" />
</div>
<gl-tooltip :target="() => $refs.assignees" boundary="viewport" placement="left">
<gl-sprintf :message="$options.i18n.ASSIGNEES_BLOCK">
@@ -270,12 +270,12 @@ export default {
<p v-else-if="userListEmpty" class="gl-mx-5 gl-my-4">
{{ __('No Matching Results') }}
</p>
- <gl-loading-icon v-else />
+ <gl-loading-icon v-else size="sm" />
</div>
</gl-dropdown>
</div>
- <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" />
<div
v-else-if="!isDropdownShowing"
class="hide-collapsed value gl-m-0"
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
index ce90a759cee..eaa5fc5af04 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/sidebar/sidebar_status.vue
@@ -81,7 +81,7 @@ export default {
<template v-if="sidebarCollapsed">
<div ref="status" class="gl-ml-6" data-testid="status-icon" @click="$emit('toggle-sidebar')">
<gl-icon name="status" />
- <gl-loading-icon v-if="isUpdating" />
+ <gl-loading-icon v-if="isUpdating" size="sm" />
</div>
<gl-tooltip :target="() => $refs.status" boundary="viewport" placement="left">
<gl-sprintf :message="s__('AlertManagement|Alert status: %{status}')">
@@ -120,7 +120,7 @@ export default {
@handle-updating="handleUpdating"
/>
- <gl-loading-icon v-if="isUpdating" :inline="true" />
+ <gl-loading-icon v-if="isUpdating" size="sm" :inline="true" />
<p
v-else-if="!isDropdownShowing"
class="value gl-m-0"
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index 13472b48e84..bab13fe7c75 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -68,7 +68,7 @@ export default {
split
@click="handleClick(selectedAction, $event)"
>
- <template slot="button-content">
+ <template #button-content>
<span class="gl-new-dropdown-button-text" v-bind="selectedAction.attrs">
{{ selectedAction.text }}
</span>
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index e6d9a38d1fb..f4c73d12923 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -93,12 +93,12 @@ export default {
return {
name,
list,
- title: this.getAwardListTitle(list),
+ title: this.getAwardListTitle(list, name),
classes: this.getAwardClassBindings(list),
html: glEmojiTag(name),
};
},
- getAwardListTitle(awardsList) {
+ getAwardListTitle(awardsList, name) {
if (!awardsList.length) {
return '';
}
@@ -128,7 +128,7 @@ export default {
// We have 10+ awarded user, join them with comma and add `and x more`.
if (remainingAwardList.length) {
title = sprintf(
- __(`%{listToShow}, and %{awardsListLength} more.`),
+ __(`%{listToShow}, and %{awardsListLength} more`),
{
listToShow: namesToShow.join(', '),
awardsListLength: remainingAwardList.length,
@@ -146,7 +146,7 @@ export default {
title = namesToShow.join(__(' and '));
}
- return title;
+ return title + sprintf(__(' reacted with :%{name}:'), { name });
},
handleAward(awardName) {
if (!this.canAwardEmoji) {
diff --git a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
index 9c2ed5abf04..0c1d55ae707 100644
--- a/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
+++ b/app/assets/javascripts/vue_shared/components/blob_viewers/mixins.js
@@ -5,7 +5,13 @@ export default {
props: {
content: {
type: String,
- required: true,
+ required: false,
+ default: null,
+ },
+ richViewer: {
+ type: String,
+ default: '',
+ required: false,
},
type: {
type: String,
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 a8a053c0d9e..dc4d1bd56e9 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
@@ -18,5 +18,5 @@ export default {
};
</script>
<template>
- <markdown-field-view ref="content" v-safe-html="content" />
+ <markdown-field-view ref="content" v-safe-html="richViewer || content" />
</template>
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 f6ab3cac536..0589b47edbd 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
@@ -9,8 +9,8 @@ export default {
name: 'SimpleViewer',
components: {
GlIcon,
- EditorLite: () =>
- import(/* webpackChunkName: 'EditorLite' */ '~/vue_shared/components/editor_lite.vue'),
+ SourceEditor: () =>
+ import(/* webpackChunkName: 'SourceEditor' */ '~/vue_shared/components/source_editor.vue'),
},
mixins: [ViewerMixin, glFeatureFlagsMixin()],
inject: ['blobHash'],
@@ -53,7 +53,7 @@ export default {
</script>
<template>
<div>
- <editor-lite
+ <source-editor
v-if="isRawContent && refactorBlobViewerEnabled"
:value="content"
:file-name="fileName"
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 4b53f55b856..14e99977a85 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -82,13 +82,7 @@ export default {
data-qa-selector="changed_file_icon_content"
:data-qa-title="tooltipTitle"
>
- <gl-icon
- v-if="showIcon"
- :name="changedIcon"
- :size="size"
- :class="changedIconClass"
- use-deprecated-sizes
- />
+ <gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
index 2552236a073..fb7105bd416 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_area_chart.vue
@@ -28,18 +28,23 @@ export default {
<slot></slot>
</p>
<resizable-chart-container>
- <gl-area-chart
- slot-scope="{ width }"
- v-bind="$attrs"
- :width="width"
- :height="$options.chartContainerHeight"
- :data="chartData"
- :include-legend-avg-max="false"
- :option="areaChartOptions"
- >
- <slot slot="tooltip-title" name="tooltip-title"></slot>
- <slot slot="tooltip-content" name="tooltip-content"></slot>
- </gl-area-chart>
+ <template #default="{ width }">
+ <gl-area-chart
+ v-bind="$attrs"
+ :width="width"
+ :height="$options.chartContainerHeight"
+ :data="chartData"
+ :include-legend-avg-max="false"
+ :option="areaChartOptions"
+ >
+ <template #tooltip-title>
+ <slot name="tooltip-title"></slot>
+ </template>
+ <template #tooltip-content>
+ <slot name="tooltip-content"></slot>
+ </template>
+ </gl-area-chart>
+ </template>
</resizable-chart-container>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
index f4fd57e4cdc..0575d7f6404 100644
--- a/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue
@@ -46,9 +46,12 @@ export default {
:area-chart-options="chartOptions"
>
{{ dateRange }}
-
- <slot slot="tooltip-title" name="tooltip-title"></slot>
- <slot slot="tooltip-content" name="tooltip-content"></slot>
+ <template #tooltip-title>
+ <slot name="tooltip-title"></slot>
+ </template>
+ <template #tooltip-content>
+ <slot name="tooltip-content"></slot>
+ </template>
</ci-cd-analytics-area-chart>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon.vue
index dbf459cb289..07bd6019b80 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon.vue
@@ -64,12 +64,6 @@ export default {
</script>
<template>
<span :class="cssClass">
- <gl-icon
- :name="icon"
- :size="size"
- :class="cssClasses"
- :aria-label="status.icon"
- use-deprecated-sizes
- />
+ <gl-icon :name="icon" :size="size" :class="cssClasses" :aria-label="status.icon" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/default.vue
index 4bc70870767..733accdff44 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/default.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/default.vue
@@ -3,6 +3,7 @@ import Identicon from '../identicon.vue';
import ProjectAvatarImage from './image.vue';
export default {
+ name: 'DeprecatedProjectAvatar',
components: {
Identicon,
ProjectAvatarImage,
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/image.vue
index 269736c799c..269736c799c 100644
--- a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
+++ b/app/assets/javascripts/vue_shared/components/deprecated_project_avatar/image.vue
diff --git a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
index b3edd05b0ee..b786f7752df 100644
--- a/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
+++ b/app/assets/javascripts/vue_shared/components/diff_viewer/viewers/renamed.vue
@@ -89,7 +89,7 @@ export default {
<template>
<div class="nothing-here-block">
- <gl-loading-icon v-if="is($options.STATE_LOADING)" />
+ <gl-loading-icon v-if="is($options.STATE_LOADING)" size="sm" />
<template v-else>
<gl-alert
v-show="is($options.STATE_ERRORED)"
diff --git a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
index 8494f99fd7d..52371e42ba1 100644
--- a/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
+++ b/app/assets/javascripts/vue_shared/components/dismissible_alert.vue
@@ -1,11 +1,14 @@
<script>
-/* eslint-disable vue/no-v-html */
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
export default {
+ name: 'DismissibleAlert',
components: {
GlAlert,
},
+ directives: {
+ SafeHtml,
+ },
props: {
html: {
type: String,
@@ -28,6 +31,6 @@ export default {
<template>
<gl-alert v-if="!isDismissed" v-bind="$attrs" @dismiss="dismiss" v-on="$listeners">
- <div v-html="html"></div>
+ <div v-safe-html="html"></div>
</gl-alert>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
index a1c7c4dd142..a512eb687b7 100644
--- a/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
+++ b/app/assets/javascripts/vue_shared/components/dropdown/dropdown_button.vue
@@ -36,7 +36,7 @@ export default {
data-toggle="dropdown"
aria-expanded="false"
>
- <gl-loading-icon v-show="isLoading" :inline="true" />
+ <gl-loading-icon v-show="isLoading" size="sm" :inline="true" />
<slot v-if="$slots.default"></slot>
<span v-else class="dropdown-toggle-text"> {{ toggleText }} </span>
<gl-icon
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 546ee56355f..0b92c947fc7 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -7,7 +7,7 @@ import { __ } from '~/locale';
*
* @example
* <expand-button>
- * <template slot="expanded">
+ * <template #expanded>
* Text goes here.
* </template>
* </expand-button>
diff --git a/app/assets/javascripts/vue_shared/components/file_finder/index.vue b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
index fbadb202d51..b0c1c1531aa 100644
--- a/app/assets/javascripts/vue_shared/components/file_finder/index.vue
+++ b/app/assets/javascripts/vue_shared/components/file_finder/index.vue
@@ -103,6 +103,9 @@ export default {
focusedIndex() {
if (!this.mouseOver) {
this.$nextTick(() => {
+ if (!this.$refs.virtualScrollList?.$el) {
+ return;
+ }
const el = this.$refs.virtualScrollList.$el;
const scrollTop = this.focusedIndex * FILE_FINDER_ROW_HEIGHT;
const bottom = this.listShowCount * FILE_FINDER_ROW_HEIGHT;
@@ -218,7 +221,7 @@ export default {
</script>
<template>
- <div class="file-finder-overlay" @mousedown.self="toggle(false)">
+ <div v-if="visible" class="file-finder-overlay" @mousedown.self="toggle(false)">
<div class="dropdown-menu diff-file-changes file-finder show">
<div :class="{ 'has-value': showClearInputButton }" class="dropdown-input">
<input
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 4244cab902a..276fb35b51f 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -85,7 +85,7 @@ export default {
</script>
<template>
<span>
- <gl-loading-icon v-if="loading" :inline="true" />
+ <gl-loading-icon v-if="loading" size="sm" :inline="true" />
<gl-icon v-else-if="isSymlink" name="symlink" :size="size" use-deprecated-sizes />
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use v-bind="{ 'xlink:href': spriteHref }" />
@@ -95,7 +95,6 @@ export default {
:name="folderIconName"
:size="size"
class="folder-icon"
- use-deprecated-sizes
data-qa-selector="folder_icon_content"
/>
</span>
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 9775a9119c6..994ce6a762a 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
@@ -10,8 +10,11 @@ export const FILTER_CURRENT = 'Current';
export const OPERATOR_IS = '=';
export const OPERATOR_IS_TEXT = __('is');
export const OPERATOR_IS_NOT = '!=';
+export const OPERATOR_IS_NOT_TEXT = __('is not');
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_IS_AND_IS_NOT = [...OPERATOR_IS_ONLY, ...OPERATOR_IS_NOT_ONLY];
export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __(FILTER_NONE) };
export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __(FILTER_ANY) };
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
index 37436de907f..571d24b50cf 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_utils.js
@@ -215,35 +215,35 @@ export function urlQueryToFilter(query = '', options = {}) {
/**
* Returns array of token values from localStorage
- * based on provided recentTokenValuesStorageKey
+ * based on provided recentSuggestionsStorageKey
*
- * @param {String} recentTokenValuesStorageKey
+ * @param {String} recentSuggestionsStorageKey
* @returns
*/
-export function getRecentlyUsedTokenValues(recentTokenValuesStorageKey) {
- let recentlyUsedTokenValues = [];
+export function getRecentlyUsedSuggestions(recentSuggestionsStorageKey) {
+ let recentlyUsedSuggestions = [];
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- recentlyUsedTokenValues = JSON.parse(localStorage.getItem(recentTokenValuesStorageKey)) || [];
+ recentlyUsedSuggestions = JSON.parse(localStorage.getItem(recentSuggestionsStorageKey)) || [];
}
- return recentlyUsedTokenValues;
+ return recentlyUsedSuggestions;
}
/**
* Sets provided token value to recently used array
- * within localStorage for provided recentTokenValuesStorageKey
+ * within localStorage for provided recentSuggestionsStorageKey
*
- * @param {String} recentTokenValuesStorageKey
+ * @param {String} recentSuggestionsStorageKey
* @param {Object} tokenValue
*/
-export function setTokenValueToRecentlyUsed(recentTokenValuesStorageKey, tokenValue) {
- const recentlyUsedTokenValues = getRecentlyUsedTokenValues(recentTokenValuesStorageKey);
+export function setTokenValueToRecentlyUsed(recentSuggestionsStorageKey, tokenValue) {
+ const recentlyUsedSuggestions = getRecentlyUsedSuggestions(recentSuggestionsStorageKey);
- recentlyUsedTokenValues.splice(0, 0, { ...tokenValue });
+ recentlyUsedSuggestions.splice(0, 0, { ...tokenValue });
if (AccessorUtilities.isLocalStorageAccessSafe()) {
localStorage.setItem(
- recentTokenValuesStorageKey,
- JSON.stringify(uniqWith(recentlyUsedTokenValues, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
+ recentSuggestionsStorageKey,
+ JSON.stringify(uniqWith(recentlyUsedSuggestions, isEqual).slice(0, MAX_RECENT_TOKENS_SIZE)),
);
}
}
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/author_token.vue
index 3b261f5ac25..a25a19a006c 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/author_token.vue
@@ -74,13 +74,13 @@ export default {
:config="config"
:value="value"
:active="active"
- :tokens-list-loading="loading"
- :token-values="authors"
+ :suggestions-loading="loading"
+ :suggestions="authors"
:fn-active-token-value="getActiveAuthor"
- :default-token-values="defaultAuthors"
- :preloaded-token-values="preloadedAuthors"
- :recent-token-values-storage-key="config.recentTokenValuesStorageKey"
- @fetch-token-values="fetchAuthorBySearchTerm"
+ :default-suggestions="defaultAuthors"
+ :preloaded-suggestions="preloadedAuthors"
+ :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
+ @fetch-suggestions="fetchAuthorBySearchTerm"
v-on="$listeners"
>
<template #view="{ viewTokenProps: { inputValue, activeTokenValue } }">
@@ -93,9 +93,9 @@ export default {
/>
<span>{{ activeTokenValue ? activeTokenValue.name : inputValue }}</span>
</template>
- <template #token-values-list="{ tokenValues }">
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="author in tokenValues"
+ v-for="author in suggestions"
:key="author.username"
:value="author.username"
>
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 bda6b340871..a4804525a53 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
@@ -6,9 +6,10 @@ import {
GlDropdownSectionHeader,
GlLoadingIcon,
} from '@gitlab/ui';
+import { debounce } from 'lodash';
import { DEBOUNCE_DELAY } from '../constants';
-import { getRecentlyUsedTokenValues, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
+import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
components: {
@@ -31,12 +32,12 @@ export default {
type: Boolean,
required: true,
},
- tokensListLoading: {
+ suggestionsLoading: {
type: Boolean,
required: false,
default: false,
},
- tokenValues: {
+ suggestions: {
type: Array,
required: false,
default: () => [],
@@ -44,21 +45,21 @@ export default {
fnActiveTokenValue: {
type: Function,
required: false,
- default: (tokenValues, currentTokenValue) => {
- return tokenValues.find(({ value }) => value === currentTokenValue);
+ default: (suggestions, currentTokenValue) => {
+ return suggestions.find(({ value }) => value === currentTokenValue);
},
},
- defaultTokenValues: {
+ defaultSuggestions: {
type: Array,
required: false,
default: () => [],
},
- preloadedTokenValues: {
+ preloadedSuggestions: {
type: Array,
required: false,
default: () => [],
},
- recentTokenValuesStorageKey: {
+ recentSuggestionsStorageKey: {
type: String,
required: false,
default: '',
@@ -77,21 +78,21 @@ export default {
data() {
return {
searchKey: '',
- recentTokenValues: this.recentTokenValuesStorageKey
- ? getRecentlyUsedTokenValues(this.recentTokenValuesStorageKey)
+ recentSuggestions: this.recentSuggestionsStorageKey
+ ? getRecentlyUsedSuggestions(this.recentSuggestionsStorageKey)
: [],
loading: false,
};
},
computed: {
- isRecentTokenValuesEnabled() {
- return Boolean(this.recentTokenValuesStorageKey);
+ isRecentSuggestionsEnabled() {
+ return Boolean(this.recentSuggestionsStorageKey);
},
recentTokenIds() {
- return this.recentTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]);
+ return this.recentSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
preloadedTokenIds() {
- return this.preloadedTokenValues.map((tokenValue) => tokenValue[this.valueIdentifier]);
+ return this.preloadedSuggestions.map((tokenValue) => tokenValue[this.valueIdentifier]);
},
currentTokenValue() {
if (this.fnCurrentTokenValue) {
@@ -100,17 +101,17 @@ export default {
return this.value.data.toLowerCase();
},
activeTokenValue() {
- return this.fnActiveTokenValue(this.tokenValues, this.currentTokenValue);
+ return this.fnActiveTokenValue(this.suggestions, this.currentTokenValue);
},
/**
- * Return all the tokenValues when searchKey is present
- * otherwise return only the tokenValues which aren't
+ * Return all the suggestions when searchKey is present
+ * otherwise return only the suggestions which aren't
* present in "Recently used"
*/
- availableTokenValues() {
+ availableSuggestions() {
return this.searchKey
- ? this.tokenValues
- : this.tokenValues.filter(
+ ? this.suggestions
+ : this.suggestions.filter(
(tokenValue) =>
!this.recentTokenIds.includes(tokenValue[this.valueIdentifier]) &&
!this.preloadedTokenIds.includes(tokenValue[this.valueIdentifier]),
@@ -121,30 +122,30 @@ export default {
active: {
immediate: true,
handler(newValue) {
- if (!newValue && !this.tokenValues.length) {
- this.$emit('fetch-token-values', this.value.data);
+ if (!newValue && !this.suggestions.length) {
+ this.$emit('fetch-suggestions', this.value.data);
}
},
},
},
methods: {
- handleInput({ data }) {
+ handleInput: debounce(function debouncedSearch({ data }) {
this.searchKey = data;
- setTimeout(() => {
- if (!this.tokensListLoading) this.$emit('fetch-token-values', data);
- }, DEBOUNCE_DELAY);
- },
+ if (!this.suggestionsLoading) {
+ this.$emit('fetch-suggestions', data);
+ }
+ }, DEBOUNCE_DELAY),
handleTokenValueSelected(activeTokenValue) {
// Make sure that;
// 1. Recently used values feature is enabled
// 2. User has actually selected a value
// 3. Selected value is not part of preloaded list.
if (
- this.isRecentTokenValuesEnabled &&
+ this.isRecentSuggestionsEnabled &&
activeTokenValue &&
!this.preloadedTokenIds.includes(activeTokenValue[this.valueIdentifier])
) {
- setTokenValueToRecentlyUsed(this.recentTokenValuesStorageKey, activeTokenValue);
+ setTokenValueToRecentlyUsed(this.recentSuggestionsStorageKey, activeTokenValue);
}
},
},
@@ -168,9 +169,9 @@ export default {
<slot name="view" :view-token-props="{ ...viewTokenProps, activeTokenValue }"></slot>
</template>
<template #suggestions>
- <template v-if="defaultTokenValues.length">
+ <template v-if="defaultSuggestions.length">
<gl-filtered-search-suggestion
- v-for="token in defaultTokenValues"
+ v-for="token in defaultSuggestions"
:key="token.value"
:value="token.value"
>
@@ -178,19 +179,19 @@ export default {
</gl-filtered-search-suggestion>
<gl-dropdown-divider />
</template>
- <template v-if="isRecentTokenValuesEnabled && recentTokenValues.length && !searchKey">
+ <template v-if="isRecentSuggestionsEnabled && recentSuggestions.length && !searchKey">
<gl-dropdown-section-header>{{ __('Recently used') }}</gl-dropdown-section-header>
- <slot name="token-values-list" :token-values="recentTokenValues"></slot>
+ <slot name="suggestions-list" :suggestions="recentSuggestions"></slot>
<gl-dropdown-divider />
</template>
<slot
- v-if="preloadedTokenValues.length && !searchKey"
- name="token-values-list"
- :token-values="preloadedTokenValues"
+ v-if="preloadedSuggestions.length && !searchKey"
+ name="suggestions-list"
+ :suggestions="preloadedSuggestions"
></slot>
- <gl-loading-icon v-if="tokensListLoading" />
+ <gl-loading-icon v-if="suggestionsLoading" size="sm" />
<template v-else>
- <slot name="token-values-list" :token-values="availableTokenValues"></slot>
+ <slot name="suggestions-list" :suggestions="availableSuggestions"></slot>
</template>
</template>
</gl-filtered-search-token>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
index 694dcd95b5e..5859fd10688 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/branch_token.vue
@@ -97,7 +97,7 @@ export default {
{{ branch.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultBranches.length" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="branch in branches"
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 9ba7f3d1a1d..d186f46866c 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
@@ -101,7 +101,7 @@ export default {
{{ emoji.value }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultEmojis.length" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="emoji in emojis"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
index d21fa9a344a..aa234cf86d9 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/epic_token.vue
@@ -56,7 +56,7 @@ export default {
}
// Current value is a string.
- const [groupPath, idProperty] = this.currentValue?.split('::&');
+ const [groupPath, idProperty] = this.currentValue?.split(this.$options.separator);
return this.epics.find(
(epic) =>
epic.group_full_path === groupPath &&
@@ -65,6 +65,9 @@ export default {
}
return null;
},
+ displayText() {
+ return `${this.activeEpic?.title}${this.$options.separator}${this.activeEpic?.iid}`;
+ },
},
watch: {
active: {
@@ -103,8 +106,10 @@ export default {
this.fetchEpicsBySearchTerm({ epicPath, search: data });
}, DEBOUNCE_DELAY),
- getEpicDisplayText(epic) {
- return `${epic.title}${this.$options.separator}${epic.iid}`;
+ getValue(epic) {
+ return this.config.useIdValue
+ ? String(epic[this.idProperty])
+ : `${epic.group_full_path}${this.$options.separator}${epic[this.idProperty]}`;
},
},
};
@@ -118,7 +123,7 @@ export default {
@input="searchEpics"
>
<template #view="{ inputValue }">
- {{ activeEpic ? getEpicDisplayText(activeEpic) : inputValue }}
+ {{ activeEpic ? displayText : inputValue }}
</template>
<template #suggestions>
<gl-filtered-search-suggestion
@@ -129,13 +134,9 @@ export default {
{{ epic.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultEpics.length" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
- <gl-filtered-search-suggestion
- v-for="epic in epics"
- :key="epic.id"
- :value="`${epic.group_full_path}::&${epic[idProperty]}`"
- >
+ <gl-filtered-search-suggestion v-for="epic in epics" :key="epic.id" :value="getValue(epic)">
{{ epic.title }}
</gl-filtered-search-suggestion>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
index 7b6a590279a..ba8b2421726 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/iteration_token.vue
@@ -7,6 +7,7 @@ import {
} from '@gitlab/ui';
import { debounce } from 'lodash';
import createFlash from '~/flash';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY, DEFAULT_ITERATIONS } from '../constants';
@@ -30,8 +31,7 @@ export default {
data() {
return {
iterations: this.config.initialIterations || [],
- defaultIterations: this.config.defaultIterations || DEFAULT_ITERATIONS,
- loading: true,
+ loading: false,
};
},
computed: {
@@ -39,7 +39,12 @@ export default {
return this.value.data;
},
activeIteration() {
- return this.iterations.find((iteration) => iteration.title === this.currentValue);
+ return this.iterations.find(
+ (iteration) => getIdFromGraphQLId(iteration.id) === Number(this.currentValue),
+ );
+ },
+ defaultIterations() {
+ return this.config.defaultIterations || DEFAULT_ITERATIONS;
},
},
watch: {
@@ -53,6 +58,9 @@ export default {
},
},
methods: {
+ getValue(iteration) {
+ return String(getIdFromGraphQLId(iteration.id));
+ },
fetchIterationBySearchTerm(searchTerm) {
const fetchPromise = this.config.fetchPath
? this.config.fetchIterations(this.config.fetchPath, searchTerm)
@@ -95,12 +103,12 @@ export default {
{{ iteration.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultIterations.length" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="iteration in iterations"
- :key="iteration.title"
- :value="iteration.title"
+ :key="iteration.id"
+ :value="getValue(iteration)"
>
{{ iteration.title }}
</gl-filtered-search-suggestion>
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 e496d099a42..4d08f81fee9 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
@@ -96,12 +96,12 @@ export default {
:config="config"
:value="value"
:active="active"
- :tokens-list-loading="loading"
- :token-values="labels"
+ :suggestions-loading="loading"
+ :suggestions="labels"
:fn-active-token-value="getActiveLabel"
- :default-token-values="defaultLabels"
- :recent-token-values-storage-key="config.recentTokenValuesStorageKey"
- @fetch-token-values="fetchLabelBySearchTerm"
+ :default-suggestions="defaultLabels"
+ :recent-suggestions-storage-key="config.recentSuggestionsStorageKey"
+ @fetch-suggestions="fetchLabelBySearchTerm"
v-on="$listeners"
>
<template
@@ -115,9 +115,9 @@ export default {
>~{{ activeTokenValue ? getLabelName(activeTokenValue) : inputValue }}</gl-token
>
</template>
- <template #token-values-list="{ tokenValues }">
+ <template #suggestions-list="{ suggestions }">
<gl-filtered-search-suggestion
- v-for="label in tokenValues"
+ v-for="label in suggestions"
:key="label.id"
:value="getLabelName(label)"
>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
index cda6e4d6726..66ad5ef5b4e 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue
@@ -9,6 +9,7 @@ import { debounce } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
+import { sortMilestonesByDueDate } from '~/milestones/milestone_utils';
import { DEFAULT_MILESTONES, DEBOUNCE_DELAY } from '../constants';
import { stripQuotes } from '../filtered_search_utils';
@@ -34,7 +35,7 @@ export default {
return {
milestones: this.config.initialMilestones || [],
defaultMilestones: this.config.defaultMilestones || DEFAULT_MILESTONES,
- loading: true,
+ loading: false,
};
},
computed: {
@@ -59,11 +60,16 @@ export default {
},
methods: {
fetchMilestoneBySearchTerm(searchTerm = '') {
+ if (this.loading) {
+ return;
+ }
+
this.loading = true;
this.config
.fetchMilestones(searchTerm)
- .then(({ data }) => {
- this.milestones = data;
+ .then((response) => {
+ const data = Array.isArray(response) ? response : response.data;
+ this.milestones = data.slice().sort(sortMilestonesByDueDate);
})
.catch(() => createFlash({ message: __('There was a problem fetching milestones.') }))
.finally(() => {
@@ -96,7 +102,7 @@ export default {
{{ milestone.text }}
</gl-filtered-search-suggestion>
<gl-dropdown-divider v-if="defaultMilestones.length" />
- <gl-loading-icon v-if="loading" />
+ <gl-loading-icon v-if="loading" size="sm" />
<template v-else>
<gl-filtered-search-suggestion
v-for="milestone in milestones"
diff --git a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue
index 74f988476e3..26c50345c19 100644
--- a/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue
+++ b/app/assets/javascripts/vue_shared/components/form/form_footer_actions.vue
@@ -1,3 +1,4 @@
+<!-- eslint-disable-next-line vue/no-deprecated-functional-template -->
<template functional>
<footer class="form-actions d-flex justify-content-between">
<div><slot name="prepend"></slot></div>
diff --git a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
index 96d99faa952..dd0c0358ef6 100644
--- a/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_modal_vuex.vue
@@ -74,6 +74,8 @@ export default {
@hidden="syncHide"
>
<slot></slot>
- <slot slot="modal-footer" name="modal-footer" :ok="ok" :cancel="cancel"></slot>
+ <template #modal-footer>
+ <slot name="modal-footer" :ok="ok" :cancel="cancel"></slot>
+ </template>
</gl-modal>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 80b7a9b7d05..9ea48050079 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -4,7 +4,7 @@ import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
import { unescape } from 'lodash';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
import { stripHtml } from '~/lib/utils/text_utility';
@@ -222,7 +222,11 @@ export default {
axios
.post(this.markdownPreviewPath, { text: this.textareaValue })
.then((response) => this.renderMarkdown(response.data))
- .catch(() => new Flash(__('Error loading markdown preview')));
+ .catch(() =>
+ createFlash({
+ message: __('Error loading markdown preview'),
+ }),
+ );
} else {
this.renderMarkdown();
}
@@ -245,7 +249,11 @@ export default {
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
- .catch(() => new Flash(__('Error rendering markdown preview')));
+ .catch(() =>
+ createFlash({
+ message: __('Error rendering markdown preview'),
+ }),
+ );
},
},
};
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index 83b8a6ae562..065d9b1b5dd 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLoadingIcon, GlTooltipDirective, GlIcon } from '@gitlab/ui';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { __ } from '~/locale';
import ApplySuggestion from './apply_suggestion.vue';
@@ -73,7 +74,7 @@ export default {
return __('Applying suggestions...');
},
isLoggedIn() {
- return Boolean(gon.current_user_id);
+ return isLoggedIn();
},
},
methods: {
@@ -110,7 +111,7 @@ export default {
</div>
<div v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</div>
<div v-else-if="isApplying" class="d-flex align-items-center text-secondary">
- <gl-loading-icon class="d-flex-center mr-2" />
+ <gl-loading-icon size="sm" class="d-flex-center mr-2" />
<span>{{ applyingSuggestionsMessage }}</span>
</div>
<div v-else-if="canApply && isBatched" class="d-flex align-items-center">
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 9059f0d2a8b..a04f8616acb 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,7 +1,11 @@
<script>
-/* eslint-disable vue/no-v-html */
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+
export default {
name: 'SuggestionDiffRow',
+ directives: {
+ SafeHtml,
+ },
props: {
line: {
type: Object,
@@ -32,7 +36,7 @@ export default {
:class="[{ 'd-table-cell': displayAsCell }, lineType]"
data-testid="suggestion-diff-content"
>
- <span v-if="line.rich_text" class="line" v-html="line.rich_text"></span>
+ <span v-if="line.rich_text" v-safe-html="line.rich_text" class="line"></span>
<span v-else-if="line.text" class="line">{{ line.text }}</span>
<span v-else class="line"></span>
</td>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
index 53d1cca7af3..63774c6c498 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue
@@ -1,7 +1,7 @@
<script>
import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import Vue from 'vue';
-import { deprecatedCreateFlash as Flash } from '~/flash';
+import createFlash from '~/flash';
import { __ } from '~/locale';
import SuggestionDiff from './suggestion_diff.vue';
@@ -79,7 +79,10 @@ export default {
const suggestionElements = container.querySelectorAll('.js-render-suggestion');
if (this.lineType === 'old') {
- Flash(__('Unable to apply suggestions to a deleted line.'), 'alert', this.$el);
+ createFlash({
+ message: __('Unable to apply suggestions to a deleted line.'),
+ parent: this.$el,
+ });
}
suggestionElements.forEach((suggestionEl, i) => {
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 7393a8791b7..7112295fa57 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -82,7 +82,7 @@ export default {
<span class="attaching-file-message"></span>
<!-- eslint-disable-next-line @gitlab/vue-require-i18n-strings -->
<span class="uploading-progress">0%</span>
- <gl-loading-icon inline />
+ <gl-loading-icon size="sm" inline />
</span>
<span class="uploading-error-container hide">
<span class="uploading-error-icon">
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 69afd711797..d6501a37a35 100644
--- a/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/placeholder_note.vue
@@ -16,12 +16,15 @@
* :note="{body: 'This is a note'}"
* />
*/
+import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
import { mapGetters } from 'vuex';
+import { renderMarkdown } from '~/notes/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import userAvatarLink from '../user_avatar/user_avatar_link.vue';
export default {
name: 'PlaceholderNote',
+ directives: { SafeHtml },
components: {
userAvatarLink,
TimelineEntryItem,
@@ -34,6 +37,9 @@ export default {
},
computed: {
...mapGetters(['getUserData']),
+ renderedNote() {
+ return renderMarkdown(this.note.body);
+ },
},
};
</script>
@@ -57,9 +63,7 @@ export default {
</div>
</div>
<div class="note-body">
- <div class="note-text md">
- <p>{{ note.body }}</p>
- </div>
+ <div v-safe-html="renderedNote" class="note-text md"></div>
</div>
</div>
</timeline-entry-item>
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 149909d263e..c3d861d74bc 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -111,7 +111,7 @@ export default {
<div class="note-header">
<note-header :author="note.author" :created-at="note.created_at" :note-id="note.id">
<span v-safe-html="actionTextHtml"></span>
- <template v-if="canSeeDescriptionVersion" slot="extra-controls">
+ <template v-if="canSeeDescriptionVersion" #extra-controls>
&middot;
<gl-button
variant="link"
diff --git a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue
index ff2847624c5..e37a663ace3 100644
--- a/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue
+++ b/app/assets/javascripts/vue_shared/components/oncall_schedules_list.vue
@@ -27,9 +27,13 @@ export default {
title() {
return this.isCurrentUser
? s__('OnCallSchedules|You are currently a part of:')
- : sprintf(s__('OnCallSchedules|User %{name} is currently part of:'), {
- name: this.userName,
- });
+ : sprintf(
+ s__('OnCallSchedules|User %{name} is currently part of:'),
+ {
+ name: this.userName,
+ },
+ false,
+ );
},
footer() {
return this.isCurrentUser
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 d05e45e90b3..79a9e1fca8c 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
@@ -169,6 +169,12 @@ export default {
methods: {
filterItemsByStatus(tabIndex) {
this.resetPagination();
+ const activeStatusTab = this.statusTabs[tabIndex];
+
+ if (activeStatusTab == null) {
+ return;
+ }
+
const { filters, status } = this.statusTabs[tabIndex];
this.statusFilter = filters;
this.filteredByStatus = status;
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar.stories.js b/app/assets/javascripts/vue_shared/components/project_avatar.stories.js
new file mode 100644
index 00000000000..110c6c73bad
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar.stories.js
@@ -0,0 +1,30 @@
+import ProjectAvatar from './project_avatar.vue';
+
+export default {
+ component: ProjectAvatar,
+ title: 'vue_shared/components/project_avatar',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { ProjectAvatar },
+ props: Object.keys(argTypes),
+ template: '<project-avatar v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ projectAvatarUrl:
+ 'https://gitlab.com/uploads/-/system/project/avatar/278964/logo-extra-whitespace.png?width=64',
+ projectName: 'GitLab',
+};
+
+export const FallbackAvatar = Template.bind({});
+FallbackAvatar.args = {
+ projectName: 'GitLab',
+};
+
+export const EmptyAltTag = Template.bind({});
+EmptyAltTag.args = {
+ ...Default.args,
+ alt: '',
+};
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar.vue b/app/assets/javascripts/vue_shared/components/project_avatar.vue
new file mode 100644
index 00000000000..f16187022a5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlAvatar } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlAvatar,
+ },
+ props: {
+ projectName: {
+ type: String,
+ required: true,
+ },
+ projectAvatarUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ size: {
+ type: Number,
+ default: 32,
+ required: false,
+ },
+ alt: {
+ type: String,
+ required: false,
+ default: undefined,
+ },
+ },
+ computed: {
+ avatarAlt() {
+ return this.alt ?? this.projectName;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-avatar
+ shape="rect"
+ :entity-name="projectName"
+ :src="projectAvatarUrl"
+ :alt="avatarAlt"
+ :size="size"
+ />
+</template>
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 ddc8bbf9b27..69f43c9e464 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
@@ -4,7 +4,7 @@ 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/default.vue';
+import ProjectAvatar from '~/vue_shared/components/deprecated_project_avatar/default.vue';
export default {
name: 'ProjectListItem',
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
index 580e1668f41..d55c93fd146 100644
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions_modal.vue
@@ -194,7 +194,7 @@ export default {
<template v-if="selectedPlatform">
<h5>
{{ $options.i18n.architecture }}
- <gl-loading-icon v-if="$apollo.loading" inline />
+ <gl-loading-icon v-if="$apollo.loading" size="sm" inline />
</h5>
<gl-dropdown class="gl-mb-3" :text="selectedArchitectureName">
diff --git a/app/assets/javascripts/vue_shared/components/select2_select.vue b/app/assets/javascripts/vue_shared/components/select2_select.vue
deleted file mode 100644
index bb1a8fae7b0..00000000000
--- a/app/assets/javascripts/vue_shared/components/select2_select.vue
+++ /dev/null
@@ -1,48 +0,0 @@
-<script>
-import $ from 'jquery';
-import 'select2';
-import { loadCSSFile } from '~/lib/utils/css_utils';
-
-export default {
- // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26
- // eslint-disable-next-line @gitlab/require-i18n-strings
- name: 'Select2Select',
- props: {
- options: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- value: {
- type: String,
- required: false,
- default: '',
- },
- },
-
- watch: {
- value() {
- $(this.$refs.dropdownInput).val(this.value).trigger('change');
- },
- },
-
- mounted() {
- loadCSSFile(gon.select2_css_path)
- .then(() => {
- $(this.$refs.dropdownInput)
- .val(this.value)
- .select2(this.options)
- .on('change', (event) => this.$emit('input', event.target.value));
- })
- .catch(() => {});
- },
-
- beforeDestroy() {
- $(this.$refs.dropdownInput).select2('destroy');
- },
-};
-</script>
-
-<template>
- <input ref="dropdownInput" type="hidden" />
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
index bbc7e6e7a6e..5c3a6852219 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/copyable_field.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { s__, __, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
@@ -10,8 +10,9 @@ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
export default {
name: 'CopyableField',
components: {
- GlLoadingIcon,
ClipboardButton,
+ GlLoadingIcon,
+ GlSprintf,
},
props: {
value: {
@@ -48,12 +49,6 @@ export default {
loadingIconLabel() {
return sprintf(this.$options.i18n.loadingIconLabel, { name: this.name });
},
- templateText() {
- return sprintf(this.$options.i18n.templateText, {
- name: this.name,
- value: this.value,
- });
- },
},
i18n: {
loadingIconLabel: __('Loading %{name}'),
@@ -78,10 +73,13 @@ export default {
class="gl-overflow-hidden gl-text-overflow-ellipsis gl-white-space-nowrap"
:title="value"
>
- {{ templateText }}
+ <gl-sprintf :message="$options.i18n.templateText">
+ <template #name>{{ name }}</template>
+ <template #value>{{ value }}</template>
+ </gl-sprintf>
</span>
- <gl-loading-icon v-if="isLoading" inline :label="loadingIconLabel" />
+ <gl-loading-icon v-if="isLoading" size="sm" inline :label="loadingIconLabel" />
<clipboard-button v-else size="small" v-bind="clipboardProps" />
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 075681de320..4531fafbf72 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -104,7 +104,7 @@ export default {
<collapsed-calendar-icon :text="collapsedText" class="sidebar-collapsed-icon" />
<div class="title">
{{ label }}
- <gl-loading-icon v-if="isLoading" :inline="true" />
+ <gl-loading-icon v-if="isLoading" size="sm" :inline="true" />
<div class="float-right">
<button
v-if="editable && !editing"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
index 320e2048f1c..12daaea8758 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/issuable_move_dropdown.vue
@@ -148,7 +148,7 @@ export default {
@hide="handleDropdownHide"
>
<template #button-content
- ><gl-loading-icon v-if="moveInProgress" class="gl-mr-3" />{{
+ ><gl-loading-icon v-if="moveInProgress" size="sm" class="gl-mr-3" />{{
dropdownButtonTitle
}}</template
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
index f8cc981ba3d..3ec33a653b8 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_create_view.vue
@@ -108,7 +108,7 @@ export default {
class="float-left d-flex align-items-center"
@click="handleCreateClick"
>
- <gl-loading-icon v-show="labelCreateInProgress" :inline="true" class="mr-1" />
+ <gl-loading-icon v-show="labelCreateInProgress" size="sm" :inline="true" class="mr-1" />
{{ __('Create') }}
</gl-button>
<gl-button class="float-right js-btn-cancel-create" @click="toggleDropdownContentsCreateView">
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
index 86788a84260..9914bfc6026 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue
@@ -48,6 +48,12 @@ export default {
}
return this.labels;
},
+ showDropdownFooter() {
+ return (
+ (this.isDropdownVariantSidebar || this.isDropdownVariantEmbedded) &&
+ (this.allowLabelCreate || this.labelsManagePath)
+ );
+ },
showNoMatchingResultsMessage() {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
@@ -192,11 +198,7 @@ export default {
</li>
</ul>
</div>
- <div
- v-if="isDropdownVariantSidebar || isDropdownVariantEmbedded"
- class="dropdown-footer"
- data-testid="dropdown-footer"
- >
+ <div v-if="showDropdownFooter" class="dropdown-footer" data-testid="dropdown-footer">
<ul class="list-unstyled">
<li v-if="allowLabelCreate">
<gl-link
@@ -206,7 +208,7 @@ export default {
{{ footerCreateLabelTitle }}
</gl-link>
</li>
- <li>
+ <li v-if="labelsManagePath">
<gl-link
:href="labelsManagePath"
class="gl-display-flex flex-row text-break-word label-item"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
index 813de528c0b..aad754e15b0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue
@@ -26,7 +26,7 @@ export default {
<div class="hide-collapsed gl-line-height-20 gl-mb-2 gl-text-gray-900">
{{ __('Labels') }}
<template v-if="allowLabelEdit">
- <gl-loading-icon v-show="labelsSelectInProgress" inline />
+ <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
variant="link"
class="float-right gl-text-gray-900! gl-hover-text-blue-800! js-sidebar-dropdown-toggle"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
index 89f96ab916b..178be0f6da0 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -16,7 +16,9 @@ export const receiveLabelsSuccess = ({ commit }, labels) =>
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
export const receiveLabelsFailure = ({ commit }) => {
commit(types.RECEIVE_SET_LABELS_FAILURE);
- flash(__('Error fetching labels.'));
+ createFlash({
+ message: __('Error fetching labels.'),
+ });
};
export const fetchLabels = ({ state, dispatch }) => {
dispatch('requestLabels');
@@ -32,7 +34,9 @@ export const requestCreateLabel = ({ commit }) => commit(types.REQUEST_CREATE_LA
export const receiveCreateLabelSuccess = ({ commit }) => commit(types.RECEIVE_CREATE_LABEL_SUCCESS);
export const receiveCreateLabelFailure = ({ commit }) => {
commit(types.RECEIVE_CREATE_LABEL_FAILURE);
- flash(__('Error creating label.'));
+ createFlash({
+ message: __('Error creating label.'),
+ });
};
export const createLabel = ({ state, dispatch }, label) => {
dispatch('requestCreateLabel');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
index 55716e1105e..2e0a57f15dd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/store/mutations.js
@@ -1,3 +1,4 @@
+import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
@@ -66,5 +67,16 @@ export default {
candidateLabel.touched = true;
candidateLabel.set = !candidateLabel.set;
}
+
+ if (isScopedLabel(candidateLabel)) {
+ const scopedBase = scopedLabelKey(candidateLabel);
+ const currentActiveScopedLabel = state.labels.find(({ title }) => {
+ return title.startsWith(scopedBase) && title !== '' && title !== candidateLabel.title;
+ });
+
+ if (currentActiveScopedLabel) {
+ currentActiveScopedLabel.set = false;
+ }
+ }
},
};
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
index a7f20fbe851..4651e7a1576 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue
@@ -117,7 +117,7 @@ export default {
data-testid="create-button"
@click="createLabel"
>
- <gl-loading-icon v-if="labelCreateInProgress" :inline="true" class="mr-1" />
+ <gl-loading-icon v-if="labelCreateInProgress" size="sm" :inline="true" class="mr-1" />
{{ __('Create') }}
</gl-button>
<gl-button
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
index 5d1663bc1fd..b6d14965cfa 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_title.vue
@@ -26,7 +26,7 @@ export default {
<div class="title hide-collapsed gl-mb-3">
{{ __('Labels') }}
<template v-if="allowLabelEdit">
- <gl-loading-icon v-show="labelsSelectInProgress" inline />
+ <gl-loading-icon v-show="labelsSelectInProgress" size="sm" inline />
<gl-button
variant="link"
class="float-right js-sidebar-dropdown-toggle"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
index 46ccb9470e5..58a940bca3b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue
@@ -1,7 +1,6 @@
<script>
import { GlLabel } from '@gitlab/ui';
-import { mapState } from 'vuex';
-
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isScopedLabel } from '~/lib/utils/common_utils';
export default {
@@ -14,15 +13,26 @@ export default {
required: false,
default: false,
},
- },
- computed: {
- ...mapState([
- 'selectedLabels',
- 'allowLabelRemove',
- 'allowScopedLabels',
- 'labelsFilterBasePath',
- 'labelsFilterParam',
- ]),
+ selectedLabels: {
+ type: Array,
+ required: true,
+ },
+ allowLabelRemove: {
+ type: Boolean,
+ required: true,
+ },
+ allowScopedLabels: {
+ type: Boolean,
+ required: true,
+ },
+ labelsFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ labelsFilterParam: {
+ type: String,
+ required: true,
+ },
},
methods: {
labelFilterUrl(label) {
@@ -33,6 +43,9 @@ export default {
scopedLabel(label) {
return this.allowScopedLabels && isScopedLabel(label);
},
+ removeLabel(labelId) {
+ this.$emit('onLabelRemove', getIdFromGraphQLId(labelId));
+ },
},
};
</script>
@@ -43,12 +56,14 @@ export default {
'has-labels': selectedLabels.length,
}"
class="hide-collapsed value issuable-show-labels js-value"
+ data-testid="value-wrapper"
>
- <span v-if="!selectedLabels.length" class="text-secondary">
+ <span v-if="!selectedLabels.length" class="text-secondary" data-testid="empty-placeholder">
<slot></slot>
</span>
- <template v-for="label in selectedLabels" v-else>
+ <template v-else>
<gl-label
+ v-for="label in selectedLabels"
:key="label.id"
data-qa-selector="selected_label_content"
:data-qa-label-name="label.title"
@@ -60,7 +75,7 @@ export default {
:show-close-button="allowLabelRemove"
:disabled="disableLabels"
tooltip-placement="top"
- @close="$emit('onLabelRemove', label.id)"
+ @close="removeLabel(label.id)"
/>
</template>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
new file mode 100644
index 00000000000..1c2fd3bb7c0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/graphql/issue_labels.query.graphql
@@ -0,0 +1,15 @@
+query issueLabels($fullPath: ID!, $iid: String) {
+ workspace: project(fullPath: $fullPath) {
+ issuable: issue(iid: $iid) {
+ id
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
index 7728c758e18..87f36a5bb72 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue
@@ -11,6 +11,7 @@ import DropdownContents from './dropdown_contents.vue';
import DropdownTitle from './dropdown_title.vue';
import DropdownValue from './dropdown_value.vue';
import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import issueLabelsQuery from './graphql/issue_labels.query.graphql';
import labelsSelectModule from './store';
Vue.use(Vuex);
@@ -24,6 +25,7 @@ export default {
DropdownContents,
DropdownValueCollapsed,
},
+ inject: ['iid', 'projectPath'],
props: {
allowLabelRemove: {
type: Boolean,
@@ -119,8 +121,23 @@ export default {
data() {
return {
contentIsOnViewport: true,
+ issueLabels: [],
};
},
+ apollo: {
+ issueLabels: {
+ query: issueLabelsQuery,
+ variables() {
+ return {
+ iid: this.iid,
+ fullPath: this.projectPath,
+ };
+ },
+ update(data) {
+ return data.workspace?.issuable?.labels.nodes || [];
+ },
+ },
+ },
computed: {
...mapState(['showDropdownButton', 'showDropdownContents']),
...mapGetters([
@@ -293,7 +310,7 @@ export default {
<template v-if="isDropdownVariantSidebar">
<dropdown-value-collapsed
ref="dropdownButtonCollapsed"
- :labels="selectedLabels"
+ :labels="issueLabels"
@onValueClick="handleCollapsedValueClick"
/>
<dropdown-title
@@ -302,6 +319,11 @@ export default {
/>
<dropdown-value
:disable-labels="labelsSelectInProgress"
+ :selected-labels="issueLabels"
+ :allow-label-remove="allowLabelRemove"
+ :allow-scoped-labels="allowScopedLabels"
+ :labels-filter-base-path="labelsFilterBasePath"
+ :labels-filter-param="labelsFilterParam"
@onLabelRemove="$emit('onLabelRemove', $event)"
>
<slot></slot>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
index 2b96b159ca3..935f020f559 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/actions.js
@@ -1,4 +1,4 @@
-import { deprecatedCreateFlash as flash } from '~/flash';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import * as types from './mutation_types';
@@ -16,7 +16,9 @@ export const receiveLabelsSuccess = ({ commit }, labels) =>
commit(types.RECEIVE_SET_LABELS_SUCCESS, labels);
export const receiveLabelsFailure = ({ commit }) => {
commit(types.RECEIVE_SET_LABELS_FAILURE);
- flash(__('Error fetching labels.'));
+ createFlash({
+ message: __('Error fetching labels.'),
+ });
};
export const fetchLabels = ({ state, dispatch }) => {
dispatch('requestLabels');
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
index 131c6e6fb57..1c03d95f37b 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/store/mutations.js
@@ -1,3 +1,4 @@
+import { isScopedLabel, scopedLabelKey } from '~/lib/utils/common_utils';
import { DropdownVariant } from '../constants';
import * as types from './mutation_types';
@@ -55,5 +56,16 @@ export default {
candidateLabel.touched = true;
candidateLabel.set = !candidateLabel.set;
}
+
+ if (isScopedLabel(candidateLabel)) {
+ const scopedBase = scopedLabelKey(candidateLabel);
+ const currentActiveScopedLabel = state.labels.find(
+ ({ title }) => title.indexOf(scopedBase) === 0 && title !== candidateLabel.title,
+ );
+
+ if (currentActiveScopedLabel) {
+ currentActiveScopedLabel.set = false;
+ }
+ }
},
};
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
new file mode 100644
index 00000000000..d2afc02233e
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.stories.js
@@ -0,0 +1,23 @@
+/* eslint-disable @gitlab/require-i18n-strings */
+
+import TodoButton from './todo_button.vue';
+
+export default {
+ component: TodoButton,
+ title: 'vue_shared/components/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/sidebar/todo_toggle/todo_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
new file mode 100644
index 00000000000..e6229cf0a93
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/todo_button.vue
@@ -0,0 +1,56 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { todoLabel } from './utils';
+
+export default {
+ components: {
+ GlButton,
+ },
+ props: {
+ isTodo: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ computed: {
+ buttonLabel() {
+ return todoLabel(this.isTodo);
+ },
+ },
+ methods: {
+ updateGlobalTodoCount(additionalTodoCount) {
+ const countContainer = document.querySelector('.js-todos-count');
+ if (countContainer === null) return;
+ const currentCount = parseInt(countContainer.innerText, 10);
+ const todoToggleEvent = new CustomEvent('todo:toggle', {
+ detail: {
+ count: Math.max(currentCount + additionalTodoCount, 0),
+ },
+ });
+
+ document.dispatchEvent(todoToggleEvent);
+ },
+ incrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(1);
+ },
+ decrementGlobalTodoCount() {
+ this.updateGlobalTodoCount(-1);
+ },
+ onToggle(event) {
+ if (this.isTodo) {
+ this.decrementGlobalTodoCount();
+ } else {
+ this.incrementGlobalTodoCount();
+ }
+ this.$emit('click', event);
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="onToggle($event)">
+ {{ buttonLabel }}
+ </gl-button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
new file mode 100644
index 00000000000..59e72a2ffe3
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/todo_toggle/utils.js
@@ -0,0 +1,5 @@
+import { __ } from '~/locale';
+
+export const todoLabel = (hasTodo) => {
+ return hasTodo ? __('Mark as done') : __('Add a to do');
+};
diff --git a/app/assets/javascripts/vue_shared/components/editor_lite.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index c3bddabea21..fdf0c9baee3 100644
--- a/app/assets/javascripts/vue_shared/components/editor_lite.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -1,9 +1,9 @@
<script>
import { debounce } from 'lodash';
import { CONTENT_UPDATE_DEBOUNCE, EDITOR_READY_EVENT } from '~/editor/constants';
-import Editor from '~/editor/editor_lite';
+import Editor from '~/editor/source_editor';
-function initEditorLite({ el, ...args }) {
+function initSourceEditor({ el, ...args }) {
const editor = new Editor({
scrollbar: {
alwaysConsumeMouseWheel: false,
@@ -64,7 +64,7 @@ export default {
},
},
mounted() {
- this.editor = initEditorLite({
+ this.editor = initSourceEditor({
el: this.$refs.editor,
blobPath: this.fileName,
blobContent: this.value,
@@ -93,7 +93,7 @@ export default {
</script>
<template>
<div
- :id="`editor-lite-${fileGlobalId}`"
+ :id="`source-editor-${fileGlobalId}`"
ref="editor"
data-editor-loading
@[$options.readyEvent]="$emit($options.readyEvent)"
diff --git a/app/assets/javascripts/vue_shared/components/todo_button.vue b/app/assets/javascripts/vue_shared/components/todo_button.vue
deleted file mode 100644
index 935d222a1a9..00000000000
--- a/app/assets/javascripts/vue_shared/components/todo_button.vue
+++ /dev/null
@@ -1,28 +0,0 @@
-<script>
-import { GlButton } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-export default {
- components: {
- GlButton,
- },
- props: {
- isTodo: {
- type: Boolean,
- required: false,
- default: true,
- },
- },
- computed: {
- buttonLabel() {
- return this.isTodo ? __('Mark as done') : __('Add a to do');
- },
- },
-};
-</script>
-
-<template>
- <gl-button v-bind="$attrs" :aria-label="buttonLabel" @click="$emit('click', $event)">
- {{ buttonLabel }}
- </gl-button>
-</template>
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 deac24d2270..f387f8ca128 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
@@ -72,7 +72,11 @@ export default {
<template v-else>
<div class="gl-mb-3">
<h5 class="gl-m-0">
- <user-name-with-status :name="user.name" :availability="availabilityStatus" />
+ <user-name-with-status
+ :name="user.name"
+ :availability="availabilityStatus"
+ :pronouns="user.pronouns"
+ />
</h5>
<span class="gl-text-gray-500">@{{ user.username }}</span>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 04e44aa2ed1..b85cae0c64f 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -96,9 +96,6 @@ export default {
},
},
searchUsers: {
- // TODO Remove error policy
- // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
- errorPolicy: 'all',
query: searchUsers,
variables() {
return {
@@ -111,28 +108,10 @@ export default {
return !this.isEditing;
},
update(data) {
- // TODO Remove null filter (BE fix required)
- // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
return data.workspace?.users?.nodes.filter((x) => x?.user).map(({ user }) => user) || [];
},
debounce: ASSIGNEES_DEBOUNCE_DELAY,
- error({ graphQLErrors }) {
- // TODO This error suppression is temporary (BE fix required)
- // https://gitlab.com/gitlab-org/gitlab/-/issues/329750
- const isNullError = ({ message }) => {
- return message === 'Cannot return null for non-nullable field GroupMember.user';
- };
-
- if (graphQLErrors?.length > 0 && graphQLErrors.every(isNullError)) {
- // only null-related errors exist, suppress them.
- // eslint-disable-next-line no-console
- console.error(
- "Suppressing the error 'Cannot return null for non-nullable field GroupMember.user'. Please see https://gitlab.com/gitlab-org/gitlab/-/issues/329750",
- );
- this.isSearching = false;
- return;
- }
-
+ error() {
this.$emit('error');
this.isSearching = false;
},
diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index 4bd3e352fd2..5ba7c107c12 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -93,9 +93,8 @@ export default {
tooltip: '',
attrs: {
'data-qa-selector': 'edit_button',
- 'data-track-event': 'click_edit',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- 'data-track-label': 'Edit',
+ 'data-track-action': 'click_consolidated_edit',
+ 'data-track-label': 'edit',
},
...handleOptions,
};
@@ -127,9 +126,8 @@ export default {
tooltip: '',
attrs: {
'data-qa-selector': 'web_ide_button',
- 'data-track-event': 'click_edit_ide',
- // eslint-disable-next-line @gitlab/require-i18n-strings
- 'data-track-label': 'Web IDE',
+ 'data-track-action': 'click_consolidated_edit_ide',
+ 'data-track-label': 'web_ide',
},
...handleOptions,
};
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 e9983af5401..1b20ae57563 100644
--- a/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
+++ b/app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue
@@ -16,14 +16,9 @@ export default {
type: Array,
required: true,
},
- experiment: {
- type: String,
- required: false,
- default: null,
- },
},
created() {
- const trackingMixin = Tracking.mixin({ ...gon.tracking_data, experiment: this.experiment });
+ const trackingMixin = Tracking.mixin();
const trackingInstance = new Vue({
...trackingMixin,
render() {
@@ -35,7 +30,7 @@ export default {
};
</script>
<template>
- <div class="container">
+ <div class="container gl-display-flex gl-flex-direction-column">
<h2 class="gl-my-7 gl-font-size-h1 gl-text-center">
{{ title }}
</h2>
@@ -43,11 +38,12 @@ export default {
<div
v-for="panel in panels"
:key="panel.name"
- class="new-namespace-panel-wrapper gl-display-inline-block gl-px-3 gl-mb-5"
+ class="new-namespace-panel-wrapper gl-display-inline-block gl-float-left gl-px-3 gl-mb-5"
>
<a
:href="`#${panel.name}`"
- :data-qa-selector="`${panel.name}_link`"
+ data-qa-selector="panel_link"
+ :data-qa-panel-name="panel.name"
class="new-namespace-panel gl-display-flex gl-flex-shrink-0 gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-center gl-rounded-base gl-border-gray-100 gl-border-solid gl-border-1 gl-w-full gl-py-6 gl-px-8 gl-hover-text-decoration-none!"
@click="track('click_tab', { label: panel.name })"
>
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 a2b432d11f4..c1e8376d656 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
@@ -36,11 +36,6 @@ export default {
type: String,
required: true,
},
- experiment: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
@@ -103,12 +98,7 @@ export default {
</script>
<template>
- <welcome-page
- v-if="activePanelName === null"
- :panels="panels"
- :title="title"
- :experiment="experiment"
- >
+ <welcome-page v-if="!activePanelName" :panels="panels" :title="title">
<template #footer>
<slot name="welcome-footer"> </slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/plugins/global_toast.js b/app/assets/javascripts/vue_shared/plugins/global_toast.js
index bfea2bedd40..fb52b31c2c8 100644
--- a/app/assets/javascripts/vue_shared/plugins/global_toast.js
+++ b/app/assets/javascripts/vue_shared/plugins/global_toast.js
@@ -2,7 +2,7 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
Vue.use(GlToast);
-const instance = new Vue();
+export const instance = new Vue();
export default function showGlobalToast(...args) {
return instance.$toast.show(...args);
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index 12e5f634a08..0ff858e6afc 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -5,6 +5,10 @@ import { redirectTo } from '~/lib/utils/url_utility';
import { sprintf, s__ } from '~/locale';
import apolloProvider from '../provider';
+function mutationSettingsForFeatureType(type) {
+ return featureToMutationMap[type];
+}
+
export default {
apolloProvider,
components: {
@@ -19,7 +23,7 @@ export default {
variant: {
type: String,
required: false,
- default: 'success',
+ default: 'confirm',
},
category: {
type: String,
@@ -33,17 +37,19 @@ export default {
};
},
computed: {
- featureSettings() {
- return featureToMutationMap[this.feature.type];
+ mutationSettings() {
+ return mutationSettingsForFeatureType(this.feature.type);
},
},
methods: {
async mutate() {
this.isLoading = true;
try {
- const mutation = this.featureSettings;
- const { data } = await this.$apollo.mutate(mutation.getMutationPayload(this.projectPath));
- const { errors, successPath } = data[mutation.mutationId];
+ const { mutationSettings } = this;
+ const { data } = await this.$apollo.mutate(
+ mutationSettings.getMutationPayload(this.projectPath),
+ );
+ const { errors, successPath } = data[mutationSettings.mutationId];
if (errors.length > 0) {
throw new Error(errors[0]);
@@ -62,6 +68,22 @@ export default {
}
},
},
+ /**
+ * Returns a boolean representing whether this component can be rendered for
+ * the given feature. Useful for parent components to determine whether or
+ * not to render this component.
+ * @param {Object} feature The feature to check.
+ * @returns {boolean}
+ */
+ canRender(feature) {
+ const { available, configured, canEnableByMergeRequest, type } = feature;
+ return (
+ canEnableByMergeRequest &&
+ available &&
+ !configured &&
+ Boolean(mutationSettingsForFeatureType(type))
+ );
+ },
i18n: {
buttonLabel: s__('SecurityConfiguration|Configure via Merge Request'),
noSuccessPathError: s__(
@@ -74,6 +96,7 @@ export default {
<template>
<gl-button
v-if="!feature.configured"
+ data-testid="configure-via-mr-button"
:loading="isLoading"
:variant="variant"
:category="category"
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
index 8fdc5ca78db..f3dd26b02cb 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue
@@ -76,6 +76,7 @@ export default {
<template>
<security-report-download-dropdown
+ :title="s__('SecurityReports|Download results')"
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
/>
diff --git a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
index 5d39d740c07..4178c5d1170 100644
--- a/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/components/security_report_download_dropdown.vue
@@ -21,6 +21,16 @@ export default {
required: false,
default: false,
},
+ text: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ title: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
methods: {
artifactText({ name }) {
@@ -35,7 +45,8 @@ export default {
<template>
<gl-dropdown
v-gl-tooltip
- :text="s__('SecurityReports|Download results')"
+ :text="text"
+ :title="title"
:loading="loading"
icon="download"
size="small"
diff --git a/app/assets/javascripts/vue_shared/security_reports/constants.js b/app/assets/javascripts/vue_shared/security_reports/constants.js
index 1cdcf87097f..4a50dfbd82f 100644
--- a/app/assets/javascripts/vue_shared/security_reports/constants.js
+++ b/app/assets/javascripts/vue_shared/security_reports/constants.js
@@ -22,6 +22,7 @@ export const REPORT_TYPE_DAST_PROFILES = 'dast_profiles';
export const REPORT_TYPE_SECRET_DETECTION = 'secret_detection';
export const REPORT_TYPE_DEPENDENCY_SCANNING = 'dependency_scanning';
export const REPORT_TYPE_CONTAINER_SCANNING = 'container_scanning';
+export const REPORT_TYPE_CLUSTER_IMAGE_SCANNING = 'cluster_image_scanning';
export const REPORT_TYPE_COVERAGE_FUZZING = 'coverage_fuzzing';
export const REPORT_TYPE_LICENSE_COMPLIANCE = 'license_scanning';
export const REPORT_TYPE_API_FUZZING = 'api_fuzzing';
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 d7a3d4e611e..3e0310e173e 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
@@ -200,6 +200,7 @@ export default {
<template #action-buttons>
<security-report-download-dropdown
+ :text="s__('SecurityReports|Download results')"
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
/>
@@ -228,6 +229,7 @@ export default {
<template #action-buttons>
<security-report-download-dropdown
+ :text="s__('SecurityReports|Download results')"
:artifacts="reportArtifacts"
:loading="isLoadingReportArtifacts"
/>
diff --git a/app/assets/javascripts/vuex_shared/bindings.js b/app/assets/javascripts/vuex_shared/bindings.js
index 741690886b7..bc3741a3880 100644
--- a/app/assets/javascripts/vuex_shared/bindings.js
+++ b/app/assets/javascripts/vuex_shared/bindings.js
@@ -6,7 +6,7 @@
* @param {string} list[].getter - the name of the getter, leave it empty to not use a getter
* @param {string} list[].updateFn - the name of the action, leave it empty to use the default action
* @param {string} defaultUpdateFn - the default function to dispatch
- * @param {string} root - the key of the state where to search fo they keys described in list
+ * @param {string|function} root - the key of the state where to search for the keys described in list
* @returns {Object} a dictionary with all the computed properties generated
*/
export const mapComputed = (list, defaultUpdateFn, root) => {
@@ -21,6 +21,10 @@ export const mapComputed = (list, defaultUpdateFn, root) => {
if (getter) {
return this.$store.getters[getter];
} else if (root) {
+ if (typeof root === 'function') {
+ return root(this.$store.state)[key];
+ }
+
return this.$store.state[root][key];
}
return this.$store.state[key];
diff --git a/app/assets/javascripts/whats_new/components/app.vue b/app/assets/javascripts/whats_new/components/app.vue
index 4ee586527b5..b74dba686ad 100644
--- a/app/assets/javascripts/whats_new/components/app.vue
+++ b/app/assets/javascripts/whats_new/components/app.vue
@@ -68,7 +68,7 @@ export default {
:open="open"
@close="closeDrawer"
>
- <template #header>
+ <template #title>
<h4 class="page-title gl-my-2">{{ __("What's new") }}</h4>
</template>
<template v-if="features.length">