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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-01-20 12:16:11 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2022-01-20 12:16:11 +0300
commitedaa33dee2ff2f7ea3fac488d41558eb5f86d68c (patch)
tree11f143effbfeba52329fb7afbd05e6e2a3790241 /app/assets/javascripts
parentd8a5691316400a0f7ec4f83832698f1988eb27c1 (diff)
Add latest changes from gitlab-org/gitlab@14-7-stable-eev14.7.0-rc42
Diffstat (limited to 'app/assets/javascripts')
-rw-r--r--app/assets/javascripts/admin/users/components/user_actions.vue3
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue13
-rw-r--r--app/assets/javascripts/api/packages_api.js8
-rw-r--r--app/assets/javascripts/behaviors/copy_code.js3
-rw-r--r--app/assets/javascripts/behaviors/copy_to_clipboard.js46
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js7
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js234
-rw-r--r--app/assets/javascripts/behaviors/preview_markdown.js7
-rw-r--r--app/assets/javascripts/blob/blob_line_permalink_updater.js2
-rw-r--r--app/assets/javascripts/blob/components/blob_header.vue2
-rw-r--r--app/assets/javascripts/blob/components/blob_header_filepath.vue11
-rw-r--r--app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue4
-rw-r--r--app/assets/javascripts/blob/line_highlighter.js (renamed from app/assets/javascripts/line_highlighter.js)0
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue17
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue2
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue2
-rw-r--r--app/assets/javascripts/boards/constants.js4
-rw-r--r--app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/issue.fragment.graphql35
-rw-r--r--app/assets/javascripts/boards/graphql/issue_create.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql2
-rw-r--r--app/assets/javascripts/boards/graphql/lists_issues.query.graphql4
-rw-r--r--app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql4
-rw-r--r--app/assets/javascripts/boards/stores/actions.js2
-rw-r--r--app/assets/javascripts/branches/branches_delete_modal.js53
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue13
-rw-r--r--app/assets/javascripts/clusters/agents/graphql/provider.js26
-rw-r--r--app/assets/javascripts/clusters/agents/index.js24
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_options.vue200
-rw-r--r--app/assets/javascripts/clusters_list/components/agent_table.vue65
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue6
-rw-r--r--app/assets/javascripts/clusters_list/constants.js4
-rw-r--r--app/assets/javascripts/clusters_list/graphql/cache_update.js22
-rw-r--r--app/assets/javascripts/clusters_list/graphql/mutations/delete_agent.mutation.graphql5
-rw-r--r--app/assets/javascripts/clusters_list/index.js2
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js64
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue2
-rw-r--r--app/assets/javascripts/content_editor/constants.js7
-rw-r--r--app/assets/javascripts/content_editor/extensions/code.js13
-rw-r--r--app/assets/javascripts/content_editor/extensions/code_block_highlight.js9
-rw-r--r--app/assets/javascripts/content_editor/extensions/frontmatter.js11
-rw-r--r--app/assets/javascripts/content_editor/extensions/image.js11
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js2
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js9
-rw-r--r--app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js48
-rw-r--r--app/assets/javascripts/crm/components/form.vue232
-rw-r--r--app/assets/javascripts/cycle_analytics/components/base.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/path_navigation.vue2
-rw-r--r--app/assets/javascripts/cycle_analytics/components/stage_table.vue33
-rw-r--r--app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_discussion.vue85
-rw-r--r--app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue50
-rw-r--r--app/assets/javascripts/design_management/components/design_presentation.vue4
-rw-r--r--app/assets/javascripts/design_management/components/design_sidebar.vue24
-rw-r--r--app/assets/javascripts/design_management/index.js4
-rw-r--r--app/assets/javascripts/diffs/components/compare_versions.vue2
-rw-r--r--app/assets/javascripts/diffs/components/image_diff_overlay.vue16
-rw-r--r--app/assets/javascripts/dropzone_input.js1
-rw-r--r--app/assets/javascripts/editor/source_editor.js2
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue16
-rw-r--r--app/assets/javascripts/environments/components/confirm_rollback_modal.vue3
-rw-r--r--app/assets/javascripts/environments/components/deployment.vue25
-rw-r--r--app/assets/javascripts/environments/components/deployment_status_badge.vue60
-rw-r--r--app/assets/javascripts/environments/components/environment_actions.vue25
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/environments/components/environment_stop.vue27
-rw-r--r--app/assets/javascripts/environments/components/new_environment_folder.vue26
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue265
-rw-r--r--app/assets/javascripts/environments/components/new_environments_app.vue39
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue15
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/action.mutation.graphql5
-rw-r--r--app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/queries/is_last_deployment.query.graphql3
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers.js18
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql6
-rw-r--r--app/assets/javascripts/experimental_flags.js15
-rw-r--r--app/assets/javascripts/flash.js139
-rw-r--r--app/assets/javascripts/gitlab_version_check.js20
-rw-r--r--app/assets/javascripts/google_cloud/components/deployments_service_table.vue61
-rw-r--r--app/assets/javascripts/google_cloud/components/home.vue17
-rw-r--r--app/assets/javascripts/google_tag_manager/index.js122
-rw-r--r--app/assets/javascripts/graphql_shared/fragment_types/vulnerability_location_types.js17
-rw-r--r--app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql37
-rw-r--r--app/assets/javascripts/group.js7
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue2
-rw-r--r--app/assets/javascripts/groups/groups_list.js (renamed from app/assets/javascripts/groups_list.js)2
-rw-r--r--app/assets/javascripts/groups/landing.js (renamed from app/assets/javascripts/landing.js)0
-rw-r--r--app/assets/javascripts/groups/store/groups_store.js3
-rw-r--r--app/assets/javascripts/groups/transfer_edit.js (renamed from app/assets/javascripts/transfer_edit.js)0
-rw-r--r--app/assets/javascripts/header_search/components/app.vue7
-rw-r--r--app/assets/javascripts/helpers/event_hub_factory.js7
-rw-r--r--app/assets/javascripts/ide/components/jobs/stage.vue5
-rw-r--r--app/assets/javascripts/ide/stores/modules/pipelines/actions.js8
-rw-r--r--app/assets/javascripts/init_confirm_danger.js18
-rw-r--r--app/assets/javascripts/integrations/constants.js5
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue5
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue143
-rw-r--r--app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/index.js7
-rw-r--r--app/assets/javascripts/integrations/edit/store/actions.js23
-rw-r--r--app/assets/javascripts/integrations/edit/store/mutations.js9
-rw-r--r--app/assets/javascripts/integrations/edit/store/state.js2
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_overrides.vue7
-rw-r--r--app/assets/javascripts/integrations/overrides/components/integration_tabs.vue52
-rw-r--r--app/assets/javascripts/integrations/overrides/index.js5
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/index.js28
-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_sidebar.js10
-rw-r--r--app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js19
-rw-r--r--app/assets/javascripts/issuable/components/csv_import_modal.vue6
-rw-r--r--app/assets/javascripts/issuable/index.js37
-rw-r--r--app/assets/javascripts/issues/constants.js6
-rw-r--r--app/assets/javascripts/issues/create_merge_request_dropdown.js (renamed from app/assets/javascripts/create_merge_request_dropdown.js)14
-rw-r--r--app/assets/javascripts/issues/form.js24
-rw-r--r--app/assets/javascripts/issues/index.js88
-rw-r--r--app/assets/javascripts/issues/init_filtered_search_service_desk.js11
-rw-r--r--app/assets/javascripts/issues/issue.js2
-rw-r--r--app/assets/javascripts/issues/list/components/issue_card_time_info.vue (renamed from app/assets/javascripts/issues_list/components/issue_card_time_info.vue)0
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue (renamed from app/assets/javascripts/issues_list/components/issues_list_app.vue)17
-rw-r--r--app/assets/javascripts/issues/list/components/jira_issues_import_status_app.vue (renamed from app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue)0
-rw-r--r--app/assets/javascripts/issues/list/components/new_issue_dropdown.vue (renamed from app/assets/javascripts/issues_list/components/new_issue_dropdown.vue)2
-rw-r--r--app/assets/javascripts/issues/list/constants.js (renamed from app/assets/javascripts/issues_list/constants.js)64
-rw-r--r--app/assets/javascripts/issues/list/eventhub.js (renamed from app/assets/javascripts/issues_list/eventhub.js)0
-rw-r--r--app/assets/javascripts/issues/list/index.js (renamed from app/assets/javascripts/issues_list/index.js)36
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues.query.graphql (renamed from app/assets/javascripts/issues_list/queries/get_issues.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql (renamed from app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/get_issues_list_details.query.graphql (renamed from app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/issue.fragment.graphql (renamed from app/assets/javascripts/issues_list/queries/issue.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/label.fragment.graphql (renamed from app/assets/javascripts/issues_list/queries/label.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/milestone.fragment.graphql (renamed from app/assets/javascripts/issues_list/queries/milestone.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/reorder_issues.mutation.graphql (renamed from app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/search_labels.query.graphql (renamed from app/assets/javascripts/issues_list/queries/search_labels.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/search_milestones.query.graphql (renamed from app/assets/javascripts/issues_list/queries/search_milestones.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/search_projects.query.graphql (renamed from app/assets/javascripts/issues_list/queries/search_projects.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/search_users.query.graphql (renamed from app/assets/javascripts/issues_list/queries/search_users.query.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/queries/user.fragment.graphql (renamed from app/assets/javascripts/issues_list/queries/user.fragment.graphql)0
-rw-r--r--app/assets/javascripts/issues/list/utils.js (renamed from app/assets/javascripts/issues_list/utils.js)4
-rw-r--r--app/assets/javascripts/issues/manual_ordering.js6
-rw-r--r--app/assets/javascripts/issues/new/index.js4
-rw-r--r--app/assets/javascripts/issues/related_merge_requests/index.js32
-rw-r--r--app/assets/javascripts/issues/sentry_error_stack_trace/index.js22
-rw-r--r--app/assets/javascripts/issues/show.js59
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue8
-rw-r--r--app/assets/javascripts/issues/show/components/fields/type.vue10
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue13
-rw-r--r--app/assets/javascripts/issues/show/components/sentry_error_stack_trace.vue (renamed from app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue)0
-rw-r--r--app/assets/javascripts/issues/show/constants.js26
-rw-r--r--app/assets/javascripts/issues/show/index.js (renamed from app/assets/javascripts/issues/show/incident.js)90
-rw-r--r--app/assets/javascripts/issues/show/issue.js86
-rw-r--r--app/assets/javascripts/issues_list/components/issuable.vue441
-rw-r--r--app/assets/javascripts/issues_list/components/issuables_list_app.vue426
-rw-r--r--app/assets/javascripts/issues_list/service_desk_helper.js111
-rw-r--r--app/assets/javascripts/jira_import/utils/constants.js2
-rw-r--r--app/assets/javascripts/jira_import/utils/jira_import_utils.js2
-rw-r--r--app/assets/javascripts/jobs/bridge/app.vue106
-rw-r--r--app/assets/javascripts/jobs/bridge/components/sidebar.vue65
-rw-r--r--app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql70
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue1
-rw-r--r--app/assets/javascripts/jobs/components/sidebar_job_details_container.vue9
-rw-r--r--app/assets/javascripts/jobs/index.js13
-rw-r--r--app/assets/javascripts/labels/components/delete_label_modal.vue13
-rw-r--r--app/assets/javascripts/labels/create_label_dropdown.js4
-rw-r--r--app/assets/javascripts/labels/index.js2
-rw-r--r--app/assets/javascripts/lib/mermaid.js61
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js10
-rw-r--r--app/assets/javascripts/lib/utils/constants.js1
-rw-r--r--app/assets/javascripts/lib/utils/resize_observer.js58
-rw-r--r--app/assets/javascripts/main.js15
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue43
-rw-r--r--app/assets/javascripts/members/constants.js30
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue44
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue67
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js3
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql4
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js39
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue6
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue9
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue20
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/constants.js7
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql10
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/index.js58
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.js27
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue (renamed from app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue)35
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/router.js20
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue80
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue (renamed from app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue)11
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js35
-rw-r--r--app/assets/javascripts/pages/admin/integrations/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/runners/edit/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/runners/show/index.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js8
-rw-r--r--app/assets/javascripts/pages/explore/groups/index.js6
-rw-r--r--app/assets/javascripts/pages/groups/edit/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/issues/index.js10
-rw-r--r--app/assets/javascripts/pages/groups/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/groups/merge_requests/index.js4
-rw-r--r--app/assets/javascripts/pages/groups/new/index.js7
-rw-r--r--app/assets/javascripts/pages/groups/packages/index.js8
-rw-r--r--app/assets/javascripts/pages/groups/packages/index/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/access_tokens/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/settings/integrations/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/help/index/index.js5
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/blob/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/branches/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/imports/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/incidents/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/issues/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/issues/service_desk/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js5
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/packages/packages/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js4
-rw-r--r--app/assets/javascripts/pages/projects/services/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue40
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/index.js31
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js4
-rw-r--r--app/assets/javascripts/pages/registrations/new/index.js4
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue1
-rw-r--r--app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue10
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue4
-rw-r--r--app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue22
-rw-r--r--app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue8
-rw-r--r--app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue17
-rw-r--r--app/assets/javascripts/pipeline_editor/constants.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue43
-rw-r--r--app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue4
-rw-r--r--app/assets/javascripts/pipelines/components/jobs/jobs_app.vue2
-rw-r--r--app/assets/javascripts/pipelines/components/parsing_utils.js22
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/unwrapping_utils.js9
-rw-r--r--app/assets/javascripts/pipelines/constants.js2
-rw-r--r--app/assets/javascripts/pipelines/graphql/fragmentTypes.json1
-rw-r--r--app/assets/javascripts/pipelines/pipeline_shared_client.js9
-rw-r--r--app/assets/javascripts/pipelines/utils.js6
-rw-r--r--app/assets/javascripts/profile/add_ssh_key_validation.js17
-rw-r--r--app/assets/javascripts/project_select_combo_button.js19
-rw-r--r--app/assets/javascripts/projects/project_find_file.js (renamed from app/assets/javascripts/project_find_file.js)0
-rw-r--r--app/assets/javascripts/projects/project_import.js (renamed from app/assets/javascripts/project_import.js)2
-rw-r--r--app/assets/javascripts/projects/project_visibility.js (renamed from app/assets/javascripts/project_visibility.js)21
-rw-r--r--app/assets/javascripts/projects/star.js (renamed from app/assets/javascripts/star.js)8
-rw-r--r--app/assets/javascripts/related_issues/components/related_issues_list.vue2
-rw-r--r--app/assets/javascripts/repository/components/blob_button_group.vue25
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue10
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue119
-rw-r--r--app/assets/javascripts/repository/components/blob_edit.vue1
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue4
-rw-r--r--app/assets/javascripts/repository/components/fork_suggestion.vue1
-rw-r--r--app/assets/javascripts/repository/components/preview/index.vue9
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/repository/index.js21
-rw-r--r--app/assets/javascripts/repository/queries/blob_controls.query.graphql18
-rw-r--r--app/assets/javascripts/repository/queries/blob_info.query.graphql16
-rw-r--r--app/assets/javascripts/repository/queries/path_locks.fragment.graphql3
-rw-r--r--app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue (renamed from app/assets/javascripts/runner/runner_details/runner_details_app.vue)27
-rw-r--r--app/assets/javascripts/runner/admin_runner_edit/index.js (renamed from app/assets/javascripts/runner/runner_details/index.js)6
-rw-r--r--app/assets/javascripts/runner/admin_runners/admin_runners_app.vue131
-rw-r--r--app/assets/javascripts/runner/admin_runners/index.js30
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_actions_cell.vue17
-rw-r--r--app/assets/javascripts/runner/components/cells/runner_status_cell.vue12
-rw-r--r--app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue34
-rw-r--r--app/assets/javascripts/runner/components/runner_header.vue52
-rw-r--r--app/assets/javascripts/runner/components/runner_status_badge.vue8
-rw-r--r--app/assets/javascripts/runner/components/runner_type_alert.vue54
-rw-r--r--app/assets/javascripts/runner/components/runner_update_form.vue10
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/status_token_config.js4
-rw-r--r--app/assets/javascripts/runner/components/search_tokens/tag_token.vue4
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_online_stat.vue17
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_stats.vue49
-rw-r--r--app/assets/javascripts/runner/components/stat/runner_status_stat.vue65
-rw-r--r--app/assets/javascripts/runner/constants.js5
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners.query.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql20
-rw-r--r--app/assets/javascripts/runner/graphql/get_runners.query.graphql1
-rw-r--r--app/assets/javascripts/runner/graphql/get_runners_count.query.graphql10
-rw-r--r--app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql2
-rw-r--r--app/assets/javascripts/runner/graphql/runner_node.fragment.graphql4
-rw-r--r--app/assets/javascripts/runner/group_runners/group_runners_app.vue56
-rw-r--r--app/assets/javascripts/runner/runner_search_utils.js28
-rw-r--r--app/assets/javascripts/runner/runner_update_form_utils.js (renamed from app/assets/javascripts/runner/runner_details/runner_update_form_utils.js)0
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue32
-rw-r--r--app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue1
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js2
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue130
-rw-r--r--app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql9
-rw-r--r--app/assets/javascripts/security_configuration/index.js30
-rw-r--r--app/assets/javascripts/security_configuration/resolver.js56
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue2
-rw-r--r--app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue15
-rw-r--r--app/assets/javascripts/sidebar/mount_sidebar.js7
-rw-r--r--app/assets/javascripts/sidebar/sidebar_mediator.js52
-rw-r--r--app/assets/javascripts/sidebar/stores/sidebar_store.js4
-rw-r--r--app/assets/javascripts/tracking/index.js5
-rw-r--r--app/assets/javascripts/tree.js64
-rw-r--r--app/assets/javascripts/version_check_image.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/base.vue50
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue45
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue10
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue15
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue62
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue8
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue22
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/constants.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js173
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue20
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js12
-rw-r--r--app/assets/javascripts/vue_shared/components/actions_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue60
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue7
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js17
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/author_token.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/gitlab_version_check.vue67
-rw-r--r--app/assets/javascripts/vue_shared/components/line_numbers.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue75
-rw-r--r--app/assets/javascripts/vue_shared/components/modal_copy_button.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_create_view.vue19
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue12
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_editor.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer.vue45
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue96
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue1
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue8
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/constants.js5
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue18
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue4
-rw-r--r--app/assets/javascripts/work_items/constants.js2
-rw-r--r--app/assets/javascripts/work_items/pages/work_item_root.vue16
376 files changed, 5506 insertions, 2870 deletions
diff --git a/app/assets/javascripts/admin/users/components/user_actions.vue b/app/assets/javascripts/admin/users/components/user_actions.vue
index 567d7151847..f5d21ece138 100644
--- a/app/assets/javascripts/admin/users/components/user_actions.vue
+++ b/app/assets/javascripts/admin/users/components/user_actions.vue
@@ -109,12 +109,13 @@ export default {
<div v-if="hasDropdownActions" class="gl-p-2">
<gl-dropdown
data-testid="dropdown-toggle"
- right
:text="$options.i18n.userAdministration"
:text-sr-only="!showButtonLabels"
icon="ellipsis_h"
data-qa-selector="user_actions_dropdown_toggle"
:data-qa-username="user.username"
+ no-caret
+ right
>
<gl-dropdown-section-header>{{
$options.i18n.userAdministration
diff --git a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
index 0bdb45d35c9..b3ae671d611 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -31,7 +31,8 @@ export default {
props: {
groupId: {
type: Number,
- required: true,
+ required: false,
+ default: null,
},
groupNamespace: {
type: String,
@@ -57,6 +58,11 @@ export default {
required: false,
default: () => [],
},
+ loadingDefaultProjects: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -111,6 +117,9 @@ export default {
searchTerm() {
this.search();
},
+ defaultProjects(projects) {
+ this.selectedProjects = [...projects];
+ },
},
mounted() {
this.search();
@@ -202,6 +211,7 @@ export default {
ref="projectsDropdown"
class="dropdown dropdown-projects"
toggle-class="gl-shadow-none"
+ :loading="loadingDefaultProjects"
:show-clear-all="hasSelectedProjects"
show-highlighted-items-title
highlighted-items-title-class="gl-p-3"
@@ -209,6 +219,7 @@ export default {
@hide="onHide"
>
<template #button-content>
+ <gl-loading-icon v-if="loadingDefaultProjects" class="gl-mr-2" />
<div class="gl-display-flex gl-flex-grow-1">
<gl-avatar
v-if="isOnlyOneProjectSelected"
diff --git a/app/assets/javascripts/api/packages_api.js b/app/assets/javascripts/api/packages_api.js
index 47f51c7e80e..138843a910a 100644
--- a/app/assets/javascripts/api/packages_api.js
+++ b/app/assets/javascripts/api/packages_api.js
@@ -19,13 +19,7 @@ export function publishPackage(
status: 'default',
};
- const formData = new FormData();
- formData.append('file', files[0]);
-
- return axios.put(url, formData, {
- headers: {
- 'Content-Type': 'multipart/form-data',
- },
+ return axios.put(url, files[0], {
params: Object.assign(defaults, options),
...axiosOptions,
});
diff --git a/app/assets/javascripts/behaviors/copy_code.js b/app/assets/javascripts/behaviors/copy_code.js
index a6e203ea5a2..6d2a4c245cc 100644
--- a/app/assets/javascripts/behaviors/copy_code.js
+++ b/app/assets/javascripts/behaviors/copy_code.js
@@ -29,7 +29,8 @@ class CopyCodeButton extends HTMLElement {
}
function addCodeButton() {
- [...document.querySelectorAll('pre.code.js-syntax-highlight')]
+ [...document.querySelectorAll('pre.code.js-syntax-highlight:not(.content-editor-code-block)')]
+ .filter((el) => el.getAttribute('lang') !== 'mermaid')
.filter((el) => !el.closest('.js-markdown-code'))
.forEach((el) => {
const copyCodeEl = document.createElement('copy-code');
diff --git a/app/assets/javascripts/behaviors/copy_to_clipboard.js b/app/assets/javascripts/behaviors/copy_to_clipboard.js
index de248340738..c3c28aeafc0 100644
--- a/app/assets/javascripts/behaviors/copy_to_clipboard.js
+++ b/app/assets/javascripts/behaviors/copy_to_clipboard.js
@@ -1,7 +1,13 @@
-import Clipboard from 'clipboard';
+import ClipboardJS from 'clipboard';
import $ from 'jquery';
-import { sprintf, __ } from '~/locale';
-import { fixTitle, add, show, once } from '~/tooltips';
+
+import { parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
+import { fixTitle, add, show, hide, once } from '~/tooltips';
+
+const CLIPBOARD_SUCCESS_EVENT = 'clipboard-success';
+const CLIPBOARD_ERROR_EVENT = 'clipboard-error';
+const I18N_ERROR_MESSAGE = __('Copy failed. Please manually copy the value.');
function showTooltip(target, title) {
const { title: originalTitle } = target.dataset;
@@ -9,20 +15,31 @@ function showTooltip(target, title) {
once('hidden', (tooltip) => {
if (tooltip.target === target) {
target.setAttribute('title', originalTitle);
+ target.setAttribute('aria-label', originalTitle);
fixTitle(target);
}
});
target.setAttribute('title', title);
+ target.setAttribute('aria-label', title);
fixTitle(target);
show(target);
- setTimeout(() => target.blur(), 1000);
+ setTimeout(() => {
+ hide(target);
+ }, 1000);
}
function genericSuccess(e) {
- // Clear the selection and blur the trigger so it loses its border
+ // Clear the selection
e.clearSelection();
- showTooltip(e.trigger, __('Copied'));
+ e.trigger.focus();
+ e.trigger.dispatchEvent(new Event(CLIPBOARD_SUCCESS_EVENT));
+
+ const { clipboardHandleTooltip = true } = e.trigger.dataset;
+ if (parseBoolean(clipboardHandleTooltip)) {
+ // Update tooltip
+ showTooltip(e.trigger, __('Copied'));
+ }
}
/**
@@ -30,17 +47,16 @@ function genericSuccess(e) {
* See http://clipboardjs.com/#browser-support
*/
function genericError(e) {
- let key;
- if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;'; // Command
- } else {
- key = 'Ctrl';
+ e.trigger.dispatchEvent(new Event(CLIPBOARD_ERROR_EVENT));
+
+ const { clipboardHandleTooltip = true } = e.trigger.dataset;
+ if (parseBoolean(clipboardHandleTooltip)) {
+ showTooltip(e.trigger, I18N_ERROR_MESSAGE);
}
- showTooltip(e.trigger, sprintf(__(`Press %{key}-C to copy`), { key }));
}
export default function initCopyToClipboard() {
- const clipboard = new Clipboard('[data-clipboard-target], [data-clipboard-text]');
+ const clipboard = new ClipboardJS('[data-clipboard-target], [data-clipboard-text]');
clipboard.on('success', genericSuccess);
clipboard.on('error', genericError);
@@ -74,6 +90,8 @@ export default function initCopyToClipboard() {
clipboardData.setData('text/plain', json.text);
clipboardData.setData('text/x-gfm', json.gfm);
});
+
+ return clipboard;
}
/**
@@ -89,3 +107,5 @@ export function clickCopyToClipboardButton(btnElement) {
btnElement.click();
}
+
+export { CLIPBOARD_SUCCESS_EVENT, CLIPBOARD_ERROR_EVENT, I18N_ERROR_MESSAGE };
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index 4698fcd4d42..c4e09efe263 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -4,6 +4,7 @@ import initUserPopovers from '../../user_popovers';
import highlightCurrentUser from './highlight_current_user';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
+import renderSandboxedMermaid from './render_sandboxed_mermaid';
import renderMetrics from './render_metrics';
// Render GitLab flavoured Markdown
@@ -13,7 +14,11 @@ import renderMetrics from './render_metrics';
$.fn.renderGFM = function renderGFM() {
syntaxHighlight(this.find('.js-syntax-highlight').get());
renderMath(this.find('.js-render-math'));
- renderMermaid(this.find('.js-render-mermaid'));
+ if (gon.features?.sandboxedMermaid) {
+ renderSandboxedMermaid(this.find('.js-render-mermaid'));
+ } else {
+ renderMermaid(this.find('.js-render-mermaid'));
+ }
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.js-user-link').get());
diff --git a/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
new file mode 100644
index 00000000000..1d54a1b0c04
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/render_sandboxed_mermaid.js
@@ -0,0 +1,234 @@
+import $ from 'jquery';
+import { once, countBy } from 'lodash';
+import { __ } from '~/locale';
+import {
+ getBaseURL,
+ relativePathToAbsolute,
+ setUrlParams,
+ joinPaths,
+} from '~/lib/utils/url_utility';
+import { darkModeEnabled } from '~/lib/utils/color_utils';
+import { setAttributes } from '~/lib/utils/dom_utils';
+
+// Renders diagrams and flowcharts from text using Mermaid in any element with the
+// `js-render-mermaid` class.
+//
+// Example markup:
+//
+// <pre class="js-render-mermaid">
+// graph TD;
+// A-- > B;
+// A-- > C;
+// B-- > D;
+// C-- > D;
+// </pre>
+//
+
+const SANDBOX_FRAME_PATH = '/-/sandbox/mermaid';
+// This is an arbitrary number; Can be iterated upon when suitable.
+const MAX_CHAR_LIMIT = 2000;
+// Max # of mermaid blocks that can be rendered in a page.
+const MAX_MERMAID_BLOCK_LIMIT = 50;
+// Max # of `&` allowed in Chaining of links syntax
+const MAX_CHAINING_OF_LINKS_LIMIT = 30;
+const BUFFER_IFRAME_HEIGHT = 10;
+// Keep a map of mermaid blocks we've already rendered.
+const elsProcessingMap = new WeakMap();
+let renderedMermaidBlocks = 0;
+
+// Pages without any restrictions on mermaid rendering
+const PAGES_WITHOUT_RESTRICTIONS = [
+ // Group wiki
+ 'groups:wikis:show',
+ 'groups:wikis:edit',
+ 'groups:wikis:create',
+
+ // Project wiki
+ 'projects:wikis:show',
+ 'projects:wikis:edit',
+ 'projects:wikis:create',
+
+ // Project files
+ 'projects:show',
+ 'projects:blob:show',
+];
+
+function shouldLazyLoadMermaidBlock(source) {
+ /**
+ * If source contains `&`, which means that it might
+ * contain Chaining of links a new syntax in Mermaid.
+ */
+ if (countBy(source)['&'] > MAX_CHAINING_OF_LINKS_LIMIT) {
+ return true;
+ }
+
+ return false;
+}
+
+function fixElementSource(el) {
+ // Mermaid doesn't like `<br />` tags, so collapse all like tags into `<br>`, which is parsed correctly.
+ const source = el.textContent?.replace(/<br\s*\/>/g, '<br>');
+
+ // Remove any extra spans added by the backend syntax highlighting.
+ Object.assign(el, { textContent: source });
+
+ return { source };
+}
+
+function getSandboxFrameSrc() {
+ const path = joinPaths(gon.relative_url_root || '', SANDBOX_FRAME_PATH);
+ if (!darkModeEnabled()) {
+ return path;
+ }
+ const absoluteUrl = relativePathToAbsolute(path, getBaseURL());
+ return setUrlParams({ darkMode: darkModeEnabled() }, absoluteUrl);
+}
+
+function renderMermaidEl(el, source) {
+ const iframeEl = document.createElement('iframe');
+ setAttributes(iframeEl, {
+ src: getSandboxFrameSrc(),
+ sandbox: 'allow-scripts',
+ frameBorder: 0,
+ scrolling: 'no',
+ width: '100%',
+ });
+
+ // Add the original source into the DOM
+ // to allow Copy-as-GFM to access it.
+ const sourceEl = document.createElement('text');
+ sourceEl.textContent = source;
+ sourceEl.classList.add('gl-display-none');
+
+ const wrapper = document.createElement('div');
+ wrapper.appendChild(iframeEl);
+ wrapper.appendChild(sourceEl);
+
+ el.closest('pre').replaceWith(wrapper);
+
+ // Event Listeners
+ iframeEl.addEventListener('load', () => {
+ // Potential risk associated with '*' discussed in below thread
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/74414#note_735183398
+ iframeEl.contentWindow.postMessage(source, '*');
+ });
+
+ window.addEventListener(
+ 'message',
+ (event) => {
+ if (event.origin !== 'null' || event.source !== iframeEl.contentWindow) {
+ return;
+ }
+ const { h } = event.data;
+ iframeEl.height = `${h + BUFFER_IFRAME_HEIGHT}px`;
+ },
+ false,
+ );
+}
+
+function renderMermaids($els) {
+ if (!$els.length) return;
+
+ const pageName = document.querySelector('body').dataset.page;
+
+ // A diagram may have been truncated in search results which will cause errors, so abort the render.
+ if (pageName === 'search:show') return;
+
+ let renderedChars = 0;
+
+ $els.each((i, el) => {
+ // Skipping all the elements which we've already queued in requestIdleCallback
+ if (elsProcessingMap.has(el)) {
+ return;
+ }
+
+ const { source } = fixElementSource(el);
+ /**
+ * Restrict the rendering to a certain amount of character
+ * and mermaid blocks to prevent mermaidjs from hanging
+ * up the entire thread and causing a DoS.
+ */
+ if (
+ !PAGES_WITHOUT_RESTRICTIONS.includes(pageName) &&
+ ((source && source.length > MAX_CHAR_LIMIT) ||
+ renderedChars > MAX_CHAR_LIMIT ||
+ renderedMermaidBlocks >= MAX_MERMAID_BLOCK_LIMIT ||
+ shouldLazyLoadMermaidBlock(source))
+ ) {
+ const html = `
+ <div class="alert gl-alert gl-alert-warning alert-dismissible lazy-render-mermaid-container js-lazy-render-mermaid-container fade show" role="alert">
+ <div>
+ <div>
+ <div class="js-warning-text"></div>
+ <div class="gl-alert-actions">
+ <button type="button" class="js-lazy-render-mermaid btn gl-alert-action btn-warning btn-md gl-button">Display</button>
+ </div>
+ </div>
+ <button type="button" class="close" data-dismiss="alert" aria-label="Close">
+ <span aria-hidden="true">&times;</span>
+ </button>
+ </div>
+ </div>
+ `;
+
+ const $parent = $(el).parent();
+
+ if (!$parent.hasClass('lazy-alert-shown')) {
+ $parent.after(html);
+ $parent
+ .siblings()
+ .find('.js-warning-text')
+ .text(
+ __('Warning: Displaying this diagram might cause performance issues on this page.'),
+ );
+ $parent.addClass('lazy-alert-shown');
+ }
+
+ return;
+ }
+
+ renderedChars += source.length;
+ renderedMermaidBlocks += 1;
+
+ const requestId = window.requestIdleCallback(() => {
+ renderMermaidEl(el, source);
+ });
+
+ elsProcessingMap.set(el, requestId);
+ });
+}
+
+const hookLazyRenderMermaidEvent = once(() => {
+ $(document.body).on('click', '.js-lazy-render-mermaid', function eventHandler() {
+ const parent = $(this).closest('.js-lazy-render-mermaid-container');
+ const pre = parent.prev();
+
+ const el = pre.find('.js-render-mermaid');
+
+ parent.remove();
+
+ // sandbox update
+ const element = el.get(0);
+ const { source } = fixElementSource(element);
+
+ renderMermaidEl(element, source);
+ });
+});
+
+export default function renderMermaid($els) {
+ if (!$els.length) return;
+
+ const visibleMermaids = $els.filter(function filter() {
+ return $(this).closest('details').length === 0 && $(this).is(':visible');
+ });
+
+ renderMermaids(visibleMermaids);
+
+ $els.closest('details').one('toggle', function toggle() {
+ if (this.open) {
+ renderMermaids($(this).find('.js-render-mermaid'));
+ }
+ });
+
+ hookLazyRenderMermaidEvent();
+}
diff --git a/app/assets/javascripts/behaviors/preview_markdown.js b/app/assets/javascripts/behaviors/preview_markdown.js
index a548b283142..679940d1317 100644
--- a/app/assets/javascripts/behaviors/preview_markdown.js
+++ b/app/assets/javascripts/behaviors/preview_markdown.js
@@ -124,13 +124,6 @@ const writeButtonSelector = '.js-md-write-button';
lastTextareaPreviewed = null;
const markdownToolbar = $('.md-header-toolbar');
-$.fn.setupMarkdownPreview = function () {
- const $form = $(this);
- $form.find('textarea.markdown-area').on('input', () => {
- markdownPreview.hideReferencedUsers($form);
- });
-};
-
$(document).on('markdown-preview:show', (e, $form) => {
if (!$form) {
return;
diff --git a/app/assets/javascripts/blob/blob_line_permalink_updater.js b/app/assets/javascripts/blob/blob_line_permalink_updater.js
index 11089b299c5..a3dd241604d 100644
--- a/app/assets/javascripts/blob/blob_line_permalink_updater.js
+++ b/app/assets/javascripts/blob/blob_line_permalink_updater.js
@@ -1,6 +1,6 @@
import { getLocationHash } from '../lib/utils/url_utility';
-const lineNumberRe = /^L[0-9]+/;
+const lineNumberRe = /^(L|LC)[0-9]+/;
const updateLineNumbersOnBlobPermalinks = (linksToUpdate) => {
const hash = getLocationHash();
diff --git a/app/assets/javascripts/blob/components/blob_header.vue b/app/assets/javascripts/blob/components/blob_header.vue
index 933ad448c77..1645469a218 100644
--- a/app/assets/javascripts/blob/components/blob_header.vue
+++ b/app/assets/javascripts/blob/components/blob_header.vue
@@ -81,7 +81,7 @@ export default {
</blob-filepath>
</div>
- <div class="gl-display-none gl-sm-display-flex">
+ <div class="gl-sm-display-flex file-actions">
<viewer-switcher v-if="showViewerSwitcher" v-model="viewer" />
<slot name="actions"></slot>
diff --git a/app/assets/javascripts/blob/components/blob_header_filepath.vue b/app/assets/javascripts/blob/components/blob_header_filepath.vue
index cb441a7e491..90d01358451 100644
--- a/app/assets/javascripts/blob/components/blob_header_filepath.vue
+++ b/app/assets/javascripts/blob/components/blob_header_filepath.vue
@@ -1,4 +1,5 @@
<script>
+import { GlBadge } from '@gitlab/ui';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
@@ -7,6 +8,7 @@ export default {
components: {
FileIcon,
ClipboardButton,
+ GlBadge,
},
props: {
blob: {
@@ -21,6 +23,9 @@ export default {
gfmCopyText() {
return `\`${this.blob.path}\``;
},
+ showLfsBadge() {
+ return this.blob.storedExternally && this.blob.externalStorage === 'lfs';
+ },
},
};
</script>
@@ -37,8 +42,6 @@ export default {
>
</template>
- <small class="mr-2">{{ blobSize }}</small>
-
<clipboard-button
:text="blob.path"
:gfm="gfmCopyText"
@@ -46,5 +49,9 @@ export default {
category="tertiary"
css-class="btn-clipboard btn-transparent lh-100 position-static"
/>
+
+ <small class="mr-2">{{ blobSize }}</small>
+
+ <gl-badge v-if="showLfsBadge">{{ __('LFS') }}</gl-badge>
</div>
</template>
diff --git a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
index a5b594fbd88..b2546d47694 100644
--- a/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
+++ b/app/assets/javascripts/blob/components/blob_header_viewer_switcher.vue
@@ -53,6 +53,8 @@ export default {
icon="code"
category="primary"
variant="default"
+ class="js-blob-viewer-switch-btn"
+ data-viewer="simple"
@click="switchToViewer($options.SIMPLE_BLOB_VIEWER)"
/>
<gl-button
@@ -63,6 +65,8 @@ export default {
icon="document"
category="primary"
variant="default"
+ class="js-blob-viewer-switch-btn"
+ data-viewer="rich"
@click="switchToViewer($options.RICH_BLOB_VIEWER)"
/>
</gl-button-group>
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/blob/line_highlighter.js
index a1f59aa1b54..a1f59aa1b54 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/blob/line_highlighter.js
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index 563bed6a6b8..dc821cb9f58 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -72,7 +72,7 @@ export default {
data-qa-selector="board_card"
:class="{
'multi-select': multiSelectVisible,
- 'user-can-drag': isDraggable,
+ 'gl-cursor-grab': isDraggable,
'is-disabled': isDisabled,
'is-active': isActive,
'gl-cursor-not-allowed gl-bg-gray-10': item.isLoading,
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index f89f8e5feb8..156029b62b0 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -6,11 +6,12 @@ import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdow
import { __, sprintf } from '~/locale';
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 { ISSUABLE, INCIDENT } from '~/boards/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/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 SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
@@ -29,6 +30,7 @@ export default {
SidebarSubscriptionsWidget,
SidebarDropdownWidget,
SidebarTodoWidget,
+ SidebarSeverity,
MountingPortal,
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
@@ -69,9 +71,15 @@ export default {
isIssuableSidebar() {
return this.sidebarType === ISSUABLE;
},
+ isIncidentSidebar() {
+ return this.activeBoardItem.type === INCIDENT;
+ },
showSidebar() {
return this.isIssuableSidebar && this.isSidebarOpen;
},
+ sidebarTitle() {
+ return this.isIncidentSidebar ? __('Incident details') : __('Issue details');
+ },
fullPath() {
return this.activeBoardItem?.referencePath?.split('#')[0] || '';
},
@@ -138,7 +146,7 @@ export default {
@close="handleClose"
>
<template #title>
- <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ __('Issue details') }}</h2>
+ <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">{{ sidebarTitle }}</h2>
</template>
<template #header>
<sidebar-todo-widget
@@ -159,7 +167,7 @@ export default {
@assignees-updated="setAssignees"
/>
<sidebar-dropdown-widget
- v-if="epicFeatureAvailable"
+ v-if="epicFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
@@ -178,7 +186,7 @@ export default {
/>
<template v-if="!glFeatures.iterationCadences">
<sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
+ v-if="iterationFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
issuable-attribute="iteration"
:workspace-path="projectPathForActiveIssue"
@@ -190,7 +198,7 @@ export default {
</template>
<template v-else>
<iteration-sidebar-dropdown-widget
- v-if="iterationFeatureAvailable"
+ v-if="iterationFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
@@ -200,7 +208,7 @@ export default {
/>
</template>
</div>
- <board-sidebar-time-tracker class="swimlanes-sidebar-time-tracker" />
+ <board-sidebar-time-tracker />
<sidebar-date-widget
:iid="activeBoardItem.iid"
:full-path="fullPath"
@@ -209,7 +217,6 @@ export default {
/>
<sidebar-labels-widget
class="block labels"
- data-testid="sidebar-labels"
:iid="activeBoardItem.iid"
:full-path="projectPathForActiveIssue"
:allow-label-remove="allowLabelEdit"
@@ -227,8 +234,14 @@ export default {
>
{{ __('None') }}
</sidebar-labels-widget>
+ <sidebar-severity
+ v-if="isIncidentSidebar"
+ :iid="activeBoardItem.iid"
+ :project-path="fullPath"
+ :initial-severity="activeBoardItem.severity"
+ />
<sidebar-weight-widget
- v-if="weightFeatureAvailable"
+ v-if="weightFeatureAvailable && !isIncidentSidebar"
:iid="activeBoardItem.iid"
:full-path="fullPath"
:issuable-type="issuableType"
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 09ec385bbba..2599d1c80b8 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -6,6 +6,7 @@ import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
+import { AssigneeFilterType } from '~/boards/constants';
export default {
i18n: {
@@ -37,6 +38,7 @@ export default {
authorUsername,
labelName,
assigneeUsername,
+ assigneeId,
search,
milestoneTitle,
iterationId,
@@ -63,6 +65,13 @@ export default {
});
}
+ if (assigneeId) {
+ filteredSearchValue.push({
+ type: 'assignee',
+ value: { data: assigneeId, operator: '=' },
+ });
+ }
+
if (types) {
filteredSearchValue.push({
type: 'type',
@@ -211,6 +220,7 @@ export default {
authorUsername,
labelName,
assigneeUsername,
+ assigneeId,
search,
milestoneTitle,
types,
@@ -246,6 +256,7 @@ export default {
author_username: authorUsername,
'label_name[]': labelName,
assignee_username: assigneeUsername,
+ assignee_id: assigneeId,
milestone_title: milestoneTitle,
iteration_id: iterationId,
search,
@@ -295,7 +306,11 @@ export default {
filterParams.authorUsername = filter.value.data;
break;
case 'assignee':
- filterParams.assigneeUsername = filter.value.data;
+ if (Object.values(AssigneeFilterType).includes(filter.value.data)) {
+ filterParams.assigneeId = filter.value.data;
+ } else {
+ filterParams.assigneeUsername = filter.value.data;
+ }
break;
case 'type':
filterParams.types = filter.value.data;
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index 19004518edf..6835d83a66c 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -263,7 +263,7 @@ export default {
>
<h3
:class="{
- 'user-can-drag': userCanDrag,
+ 'gl-cursor-grab': userCanDrag,
'gl-py-3 gl-h-full': list.collapsed && !isSwimlanesHeader,
'gl-border-b-0': list.collapsed || isSwimlanesHeader,
'gl-py-2': list.collapsed && isSwimlanesHeader,
diff --git a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
index e77aadfa50e..9d19fe57e7a 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -150,7 +150,7 @@ export default {
<div class="gl-display-flex gl-w-full gl-justify-content-space-between gl-mt-5">
<gl-button
- variant="success"
+ variant="confirm"
size="small"
data-testid="submit-button"
:disabled="!title"
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index 851b5eca40d..0f290f566ba 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -50,10 +50,13 @@ export const toggleFormEventPrefix = {
issue: 'toggle-issue-form-',
};
+export const active = 'active';
+
export const inactiveId = 0;
export const ISSUABLE = 'issuable';
export const LIST = 'list';
+export const INCIDENT = 'INCIDENT';
export const flashAnimationDuration = 2000;
@@ -119,6 +122,7 @@ export const FilterFields = {
/* eslint-disable @gitlab/require-i18n-strings */
export const AssigneeFilterType = {
any: 'Any',
+ none: 'None',
};
export const MilestoneFilterType = {
diff --git a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
index 0963b3fbfaa..6fe8bb799d6 100644
--- a/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/group_board_milestones.query.graphql
@@ -1,7 +1,7 @@
-query GroupBoardMilestones($fullPath: ID!, $searchTerm: String) {
+query GroupBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
group(fullPath: $fullPath) {
id
- milestones(includeAncestors: true, searchTitle: $searchTerm) {
+ milestones(includeAncestors: true, searchTitle: $searchTerm, state: $state) {
nodes {
id
title
diff --git a/app/assets/javascripts/boards/graphql/issue.fragment.graphql b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
index 314faae89f8..53fe6fdc59e 100644
--- a/app/assets/javascripts/boards/graphql/issue.fragment.graphql
+++ b/app/assets/javascripts/boards/graphql/issue.fragment.graphql
@@ -1,35 +1,6 @@
-#import "~/graphql_shared/fragments/milestone.fragment.graphql"
-#import "~/graphql_shared/fragments/user.fragment.graphql"
+#import "~/graphql_shared/fragments/issue.fragment.graphql"
-fragment IssueNode on Issue {
+fragment Issue on Issue {
id
- iid
- title
- referencePath: reference(full: true)
- dueDate
- timeEstimate
- totalTimeSpent
- humanTimeEstimate
- humanTotalTimeSpent
- emailsDisabled
- confidential
- hidden
- webUrl
- relativePosition
- milestone {
- ...MilestoneFragment
- }
- assignees {
- nodes {
- ...User
- }
- }
- labels {
- nodes {
- id
- title
- color
- description
- }
- }
+ ...IssueNode
}
diff --git a/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql
index c1a2361a4e8..643d5dcfe4c 100644
--- a/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_create.mutation.graphql
@@ -3,7 +3,7 @@
mutation CreateIssue($input: CreateIssueInput!) {
createIssue(input: $input) {
issue {
- ...IssueNode
+ ...Issue
}
errors
}
diff --git a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
index 570731ecac6..1658cf09085 100644
--- a/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
+++ b/app/assets/javascripts/boards/graphql/issue_move_list.mutation.graphql
@@ -21,7 +21,7 @@ mutation issueMoveList(
}
) {
issue {
- ...IssueNode
+ ...Issue
}
errors
}
diff --git a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
index 105f2931caa..994ea894be3 100644
--- a/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
+++ b/app/assets/javascripts/boards/graphql/lists_issues.query.graphql
@@ -22,7 +22,7 @@ query BoardListsEE(
issues(first: $first, filters: $filters, after: $after) {
edges {
node {
- ...IssueNode
+ ...Issue
}
}
pageInfo {
@@ -46,7 +46,7 @@ query BoardListsEE(
issues(first: $first, filters: $filters, after: $after) {
edges {
node {
- ...IssueNode
+ ...Issue
}
}
pageInfo {
diff --git a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
index e456823d78a..d917c7e809d 100644
--- a/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
+++ b/app/assets/javascripts/boards/graphql/project_board_milestones.query.graphql
@@ -1,7 +1,7 @@
-query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String) {
+query ProjectBoardMilestones($fullPath: ID!, $searchTerm: String, $state: MilestoneStateEnum) {
project(fullPath: $fullPath) {
id
- milestones(searchTitle: $searchTerm, includeAncestors: true) {
+ milestones(searchTitle: $searchTerm, includeAncestors: true, state: $state) {
nodes {
id
title
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
index 1ebfcfc331b..48ca3239cfd 100644
--- a/app/assets/javascripts/boards/stores/actions.js
+++ b/app/assets/javascripts/boards/stores/actions.js
@@ -15,6 +15,7 @@ import {
FilterFields,
ListTypeTitles,
DraggableItemTypes,
+ active,
} from 'ee_else_ce/boards/constants';
import {
formatIssueInput,
@@ -209,6 +210,7 @@ export default {
const variables = {
fullPath,
searchTerm,
+ state: active,
};
let query;
diff --git a/app/assets/javascripts/branches/branches_delete_modal.js b/app/assets/javascripts/branches/branches_delete_modal.js
deleted file mode 100644
index f4c3fa185d8..00000000000
--- a/app/assets/javascripts/branches/branches_delete_modal.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import $ from 'jquery';
-
-const MODAL_SELECTOR = '#modal-delete-branch';
-
-class DeleteModal {
- constructor() {
- this.$modal = $(MODAL_SELECTOR);
- this.$toggleBtns = $(`[data-target="${MODAL_SELECTOR}"]`);
- this.$branchName = $('.js-branch-name', this.$modal);
- this.$confirmInput = $('.js-delete-branch-input', this.$modal);
- this.$deleteBtn = $('.js-delete-branch', this.$modal);
- this.$notMerged = $('.js-not-merged', this.$modal);
- this.bindEvents();
- }
-
- bindEvents() {
- this.$toggleBtns.on('click', this.setModalData.bind(this));
- this.$confirmInput.on('input', this.setDeleteDisabled.bind(this));
- this.$deleteBtn.on('click', this.setDisableDeleteButton.bind(this));
- }
-
- setModalData(e) {
- const branchData = e.currentTarget.dataset;
- this.branchName = branchData.branchName || '';
- this.deletePath = branchData.deletePath || '';
- this.isMerged = Boolean(branchData.isMerged);
- this.updateModal();
- }
-
- setDeleteDisabled(e) {
- this.$deleteBtn.attr('disabled', e.currentTarget.value !== this.branchName);
- }
-
- setDisableDeleteButton(e) {
- if (this.$deleteBtn.is('[disabled]')) {
- e.preventDefault();
- e.stopPropagation();
- return false;
- }
-
- return true;
- }
-
- updateModal() {
- this.$branchName.text(this.branchName);
- this.$confirmInput.val('');
- this.$deleteBtn.attr('href', this.deletePath);
- this.$deleteBtn.attr('disabled', true);
- this.$notMerged.toggleClass('hidden', this.isMerged);
- }
-}
-
-export default DeleteModal;
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 9109c010500..a53bba6992d 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -51,16 +51,7 @@ export default {
TokenTable,
ActivityEvents,
},
- props: {
- agentName: {
- required: true,
- type: String,
- },
- projectPath: {
- required: true,
- type: String,
- },
- },
+ inject: ['agentName', 'projectPath'],
data() {
return {
cursor: {
@@ -135,7 +126,7 @@ export default {
<activity-events :agent-name="agentName" :project-path="projectPath" />
</gl-tab>
- <slot name="ee-security-tab"></slot>
+ <slot name="ee-security-tab" :cluster-agent-id="clusterAgent.id"></slot>
<gl-tab query-param-value="tokens">
<template #title>
diff --git a/app/assets/javascripts/clusters/agents/graphql/provider.js b/app/assets/javascripts/clusters/agents/graphql/provider.js
new file mode 100644
index 00000000000..8b068fa1eee
--- /dev/null
+++ b/app/assets/javascripts/clusters/agents/graphql/provider.js
@@ -0,0 +1,26 @@
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import { vulnerabilityLocationTypes } from '~/graphql_shared/fragment_types/vulnerability_location_types';
+
+Vue.use(VueApollo);
+
+// We create a fragment matcher so that we can create a fragment from an interface
+// Without this, Apollo throws a heuristic fragment matcher warning
+const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData: vulnerabilityLocationTypes,
+});
+
+const defaultClient = createDefaultClient(
+ {},
+ {
+ cacheConfig: {
+ fragmentMatcher,
+ },
+ },
+);
+
+export default new VueApollo({
+ defaultClient,
+});
diff --git a/app/assets/javascripts/clusters/agents/index.js b/app/assets/javascripts/clusters/agents/index.js
index 5796c9e308d..6c7fae274f8 100644
--- a/app/assets/javascripts/clusters/agents/index.js
+++ b/app/assets/javascripts/clusters/agents/index.js
@@ -1,9 +1,6 @@
import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
import AgentShowPage from 'ee_else_ce/clusters/agents/components/show.vue';
-
-Vue.use(VueApollo);
+import apolloProvider from './graphql/provider';
export default () => {
const el = document.querySelector('#js-cluster-agent-details');
@@ -12,20 +9,19 @@ export default () => {
return null;
}
- const defaultClient = createDefaultClient();
- const { agentName, projectPath, activityEmptyStateImage } = el.dataset;
+ const { activityEmptyStateImage, agentName, emptyStateSvgPath, projectPath } = el.dataset;
return new Vue({
el,
- apolloProvider: new VueApollo({ defaultClient }),
- provide: { agentName, projectPath, activityEmptyStateImage },
+ apolloProvider,
+ provide: {
+ activityEmptyStateImage,
+ agentName,
+ emptyStateSvgPath,
+ projectPath,
+ },
render(createElement) {
- return createElement(AgentShowPage, {
- props: {
- agentName,
- projectPath,
- },
- });
+ return createElement(AgentShowPage);
},
});
};
diff --git a/app/assets/javascripts/clusters_list/components/agent_options.vue b/app/assets/javascripts/clusters_list/components/agent_options.vue
new file mode 100644
index 00000000000..a364122ba56
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/components/agent_options.vue
@@ -0,0 +1,200 @@
+<script>
+import {
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlSprintf,
+ GlFormGroup,
+ GlFormInput,
+} from '@gitlab/ui';
+import { s__, __, sprintf } from '~/locale';
+import { DELETE_AGENT_MODAL_ID } from '../constants';
+import deleteAgent from '../graphql/mutations/delete_agent.mutation.graphql';
+import getAgentsQuery from '../graphql/queries/get_agents.query.graphql';
+import { removeAgentFromStore } from '../graphql/cache_update';
+
+export default {
+ i18n: {
+ dropdownText: __('More options'),
+ deleteButton: s__('ClusterAgents|Delete agent'),
+ modalTitle: __('Are you sure?'),
+ modalBody: s__(
+ 'ClusterAgents|Are you sure you want to delete this agent? You cannot undo this.',
+ ),
+ modalInputLabel: s__('ClusterAgents|To delete the agent, type %{name} to confirm:'),
+ modalAction: s__('ClusterAgents|Delete'),
+ modalCancel: __('Cancel'),
+ successMessage: s__('ClusterAgents|%{name} successfully deleted'),
+ defaultError: __('An error occurred. Please try again.'),
+ },
+ components: {
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlSprintf,
+ GlFormGroup,
+ GlFormInput,
+ },
+ directives: {
+ GlModalDirective,
+ },
+ inject: ['projectPath'],
+ props: {
+ agent: {
+ required: true,
+ type: Object,
+ validator: (value) => ['id', 'name'].every((prop) => value[prop]),
+ },
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ maxAgents: {
+ default: null,
+ required: false,
+ type: Number,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ error: null,
+ deleteConfirmText: null,
+ agentName: this.agent.name,
+ };
+ },
+ computed: {
+ getAgentsQueryVariables() {
+ return {
+ defaultBranchName: this.defaultBranchName,
+ first: this.maxAgents,
+ last: null,
+ projectPath: this.projectPath,
+ };
+ },
+ modalId() {
+ return sprintf(DELETE_AGENT_MODAL_ID, {
+ agentName: this.agent.name,
+ });
+ },
+ primaryModalProps() {
+ return {
+ text: this.$options.i18n.modalAction,
+ attributes: [
+ { disabled: this.loading || this.disableModalSubmit, loading: this.loading },
+ { variant: 'danger' },
+ ],
+ };
+ },
+ cancelModalProps() {
+ return {
+ text: this.$options.i18n.modalCancel,
+ attributes: [],
+ };
+ },
+ disableModalSubmit() {
+ return this.deleteConfirmText !== this.agent.name;
+ },
+ },
+ methods: {
+ async deleteAgent() {
+ if (this.disableModalSubmit || this.loading) {
+ return;
+ }
+
+ this.loading = true;
+ this.error = null;
+
+ try {
+ const { errors } = await this.deleteAgentMutation();
+
+ if (errors.length) {
+ throw new Error(errors[0]);
+ }
+ } catch (error) {
+ this.error = error?.message || this.$options.i18n.defaultError;
+ } finally {
+ this.loading = false;
+ const successMessage = sprintf(this.$options.i18n.successMessage, { name: this.agentName });
+
+ this.$toast.show(this.error || successMessage);
+
+ this.$refs.modal.hide();
+ }
+ },
+ deleteAgentMutation() {
+ return this.$apollo
+ .mutate({
+ mutation: deleteAgent,
+ variables: {
+ input: {
+ id: this.agent.id,
+ },
+ },
+ update: (store) => {
+ const deleteClusterAgent = this.agent;
+ removeAgentFromStore(
+ store,
+ deleteClusterAgent,
+ getAgentsQuery,
+ this.getAgentsQueryVariables,
+ );
+ },
+ })
+
+ .then(({ data: { clusterAgentDelete } }) => {
+ return clusterAgentDelete;
+ });
+ },
+ hideModal() {
+ this.loading = false;
+ this.error = null;
+ this.deleteConfirmText = null;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-dropdown
+ icon="ellipsis_v"
+ right
+ :disabled="loading"
+ :text="$options.i18n.dropdownText"
+ text-sr-only
+ category="tertiary"
+ no-caret
+ >
+ <gl-dropdown-item v-gl-modal-directive="modalId">
+ {{ $options.i18n.deleteButton }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+
+ <gl-modal
+ ref="modal"
+ :modal-id="modalId"
+ :title="$options.i18n.modalTitle"
+ :action-primary="primaryModalProps"
+ :action-cancel="cancelModalProps"
+ size="sm"
+ @primary="deleteAgent"
+ @hide="hideModal"
+ >
+ <p>{{ $options.i18n.modalBody }}</p>
+
+ <gl-form-group>
+ <template #label>
+ <gl-sprintf :message="$options.i18n.modalInputLabel">
+ <template #name>
+ <code>{{ agent.name }}</code>
+ </template>
+ </gl-sprintf>
+ </template>
+ <gl-form-input v-model="deleteConfirmText" @keydown.enter="deleteAgent" />
+ </gl-form-group>
+ </gl-modal>
+ </div>
+</template>
diff --git a/app/assets/javascripts/clusters_list/components/agent_table.vue b/app/assets/javascripts/clusters_list/components/agent_table.vue
index 000730ac1ba..695e16b7b4b 100644
--- a/app/assets/javascripts/clusters_list/components/agent_table.vue
+++ b/app/assets/javascripts/clusters_list/components/agent_table.vue
@@ -1,21 +1,23 @@
<script>
-import {
- GlLink,
- GlModalDirective,
- GlTable,
- GlIcon,
- GlSprintf,
- GlTooltip,
- GlPopover,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlLink, GlTable, GlIcon, GlSprintf, GlTooltip, GlPopover } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { INSTALL_AGENT_MODAL_ID, AGENT_STATUSES } from '../constants';
+import { AGENT_STATUSES } from '../constants';
import { getAgentConfigPath } from '../clusters_util';
+import AgentOptions from './agent_options.vue';
export default {
+ i18n: {
+ nameLabel: s__('ClusterAgents|Name'),
+ statusLabel: s__('ClusterAgents|Connection status'),
+ lastContactLabel: s__('ClusterAgents|Last contact'),
+ configurationLabel: s__('ClusterAgents|Configuration'),
+ optionsLabel: __('Options'),
+ troubleshootingText: s__('ClusterAgents|Learn how to troubleshoot'),
+ neverConnectedText: s__('ClusterAgents|Never'),
+ },
components: {
GlLink,
GlTable,
@@ -24,14 +26,10 @@ export default {
GlTooltip,
GlPopover,
TimeAgoTooltip,
- },
- directives: {
- GlModalDirective,
+ AgentOptions,
},
mixins: [timeagoMixin],
- INSTALL_AGENT_MODAL_ID,
AGENT_STATUSES,
-
troubleshooting_link: helpPagePath('user/clusters/agent/index', {
anchor: 'troubleshooting',
}),
@@ -40,6 +38,16 @@ export default {
required: true,
type: Array,
},
+ defaultBranchName: {
+ default: '.noBranch',
+ required: false,
+ type: String,
+ },
+ maxAgents: {
+ default: null,
+ required: false,
+ type: Number,
+ },
},
computed: {
fields() {
@@ -47,22 +55,27 @@ export default {
return [
{
key: 'name',
- label: s__('ClusterAgents|Name'),
+ label: this.$options.i18n.nameLabel,
tdClass,
},
{
key: 'status',
- label: s__('ClusterAgents|Connection status'),
+ label: this.$options.i18n.statusLabel,
tdClass,
},
{
key: 'lastContact',
- label: s__('ClusterAgents|Last contact'),
+ label: this.$options.i18n.lastContactLabel,
tdClass,
},
{
key: 'configuration',
- label: s__('ClusterAgents|Configuration'),
+ label: this.$options.i18n.configurationLabel,
+ tdClass,
+ },
+ {
+ key: 'options',
+ label: this.$options.i18n.optionsLabel,
tdClass,
},
];
@@ -118,7 +131,7 @@ export default {
</p>
<p class="gl-mb-0">
<gl-link :href="$options.troubleshooting_link" target="_blank" class="gl-font-sm">
- {{ s__('ClusterAgents|Learn how to troubleshoot') }}</gl-link
+ {{ $options.i18n.troubleshootingText }}</gl-link
>
</p>
</gl-popover>
@@ -127,7 +140,7 @@ export default {
<template #cell(lastContact)="{ item }">
<span data-testid="cluster-agent-last-contact">
<time-ago-tooltip v-if="item.lastContact" :time="item.lastContact" />
- <span v-else>{{ s__('ClusterAgents|Never') }}</span>
+ <span v-else>{{ $options.i18n.neverConnectedText }}</span>
</span>
</template>
@@ -140,5 +153,13 @@ export default {
<span v-else>{{ getAgentConfigPath(item.name) }}</span>
</span>
</template>
+
+ <template #cell(options)="{ item }">
+ <agent-options
+ :agent="item"
+ :default-branch-name="defaultBranchName"
+ :max-agents="maxAgents"
+ />
+ </template>
</gl-table>
</template>
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index 45108a28e37..4fc421e7c31 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -151,7 +151,11 @@ export default {
<section v-else-if="agentList">
<div v-if="agentList.length">
- <agent-table :agents="agentList" />
+ <agent-table
+ :agents="agentList"
+ :default-branch-name="defaultBranchName"
+ :max-agents="cursor.first"
+ />
<div v-if="showPagination" class="gl-display-flex gl-justify-content-center gl-mt-5">
<gl-keyset-pagination v-bind="agentPageInfo" @prev="prevPage" @next="nextPage" />
diff --git a/app/assets/javascripts/clusters_list/constants.js b/app/assets/javascripts/clusters_list/constants.js
index 9b52df74fc5..380a5d0aada 100644
--- a/app/assets/javascripts/clusters_list/constants.js
+++ b/app/assets/javascripts/clusters_list/constants.js
@@ -106,7 +106,7 @@ export const I18N_AGENT_MODAL = {
empty_state: {
modalTitle: s__('ClusterAgents|Connect your cluster through the Agent'),
modalBody: s__(
- "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}What's the agent's configuration file?%{linkEnd}",
+ "ClusterAgents|To install a new agent, first add the agent's configuration file to this repository. %{linkStart}Learn more about installing GitLab Agent.%{linkEnd}",
),
enableKasText: s__(
"ClusterAgents|Your instance doesn't have the %{linkStart}GitLab Agent Server (KAS)%{linkEnd} set up. Ask a GitLab Administrator to install it.",
@@ -242,3 +242,5 @@ export const EVENT_ACTIONS_CHANGE = 'change_tab';
export const MODAL_TYPE_EMPTY = 'empty_state';
export const MODAL_TYPE_REGISTER = 'agent_registration';
+
+export const DELETE_AGENT_MODAL_ID = 'delete-agent-modal-%{agentName}';
diff --git a/app/assets/javascripts/clusters_list/graphql/cache_update.js b/app/assets/javascripts/clusters_list/graphql/cache_update.js
index 4d12bc8151c..6476b7a6c2f 100644
--- a/app/assets/javascripts/clusters_list/graphql/cache_update.js
+++ b/app/assets/javascripts/clusters_list/graphql/cache_update.js
@@ -63,3 +63,25 @@ export function addAgentConfigToStore(
});
}
}
+
+export function removeAgentFromStore(store, deleteClusterAgent, query, variables) {
+ if (!hasErrors(deleteClusterAgent)) {
+ const sourceData = store.readQuery({
+ query,
+ variables,
+ });
+
+ const data = produce(sourceData, (draftData) => {
+ draftData.project.clusterAgents.nodes = draftData.project.clusterAgents.nodes.filter(
+ ({ id }) => id !== deleteClusterAgent.id,
+ );
+ draftData.project.clusterAgents.count -= 1;
+ });
+
+ store.writeQuery({
+ query,
+ variables,
+ data,
+ });
+ }
+}
diff --git a/app/assets/javascripts/clusters_list/graphql/mutations/delete_agent.mutation.graphql b/app/assets/javascripts/clusters_list/graphql/mutations/delete_agent.mutation.graphql
new file mode 100644
index 00000000000..28387b2a36c
--- /dev/null
+++ b/app/assets/javascripts/clusters_list/graphql/mutations/delete_agent.mutation.graphql
@@ -0,0 +1,5 @@
+mutation deleteClusterAgent($input: ClusterAgentDeleteInput!) {
+ clusterAgentDelete(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/clusters_list/index.js b/app/assets/javascripts/clusters_list/index.js
index 7f1ef37814b..6148483dcb0 100644
--- a/app/assets/javascripts/clusters_list/index.js
+++ b/app/assets/javascripts/clusters_list/index.js
@@ -1,8 +1,10 @@
+import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import loadClusters from './load_clusters';
import loadMainView from './load_main_view';
+Vue.use(GlToast);
Vue.use(VueApollo);
export default () => {
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
deleted file mode 100644
index ad70d9be16f..00000000000
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import $ from 'jquery';
-import { Rails } from '~/lib/utils/rails_ujs';
-import { rstrip } from './lib/utils/common_utils';
-
-function openConfirmDangerModal($form, $modal, text) {
- const $input = $('.js-legacy-confirm-danger-input', $modal);
- $input.val('');
-
- $('.js-confirm-text', $modal).text(text || '');
- $modal.modal('show');
-
- const confirmTextMatch = $('.js-legacy-confirm-danger-match', $modal).text();
- const $submit = $('.js-legacy-confirm-danger-submit', $modal);
- $submit.disable();
- $input.focus();
-
- // eslint-disable-next-line @gitlab/no-global-event-off
- $input.off('input').on('input', function handleInput() {
- const confirmText = rstrip($(this).val());
- if (confirmText === confirmTextMatch) {
- $submit.enable();
- } else {
- $submit.disable();
- }
- });
-
- // eslint-disable-next-line @gitlab/no-global-event-off
- $('.js-legacy-confirm-danger-submit', $modal)
- .off('click')
- .on('click', () => {
- if ($form.data('remote')) {
- Rails.fire($form[0], 'submit');
- } else {
- $form.submit();
- }
- });
-}
-
-function getModal($btn) {
- const $modal = $btn.prev('.modal');
-
- if ($modal.length) {
- return $modal;
- }
-
- return $('#modal-confirm-danger');
-}
-
-export default function initConfirmDangerModal() {
- $(document).on('click', '.js-legacy-confirm-danger', (e) => {
- const $btn = $(e.target);
- const checkFieldName = $btn.data('checkFieldName');
- const checkFieldCompareValue = $btn.data('checkCompareValue');
- const checkFieldVal = parseInt($(`[name="${checkFieldName}"]`).val(), 10);
-
- if (!checkFieldName || checkFieldVal < checkFieldCompareValue) {
- e.preventDefault();
- const $form = $btn.closest('form');
- const $modal = getModal($btn);
- const text = $btn.data('confirmDangerMessage');
- openConfirmDangerModal($form, $modal, text);
- }
- });
-}
diff --git a/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue b/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
index 97b69afd12e..e8829d00986 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/frontmatter.vue
@@ -20,7 +20,7 @@ export default {
};
</script>
<template>
- <node-view-wrapper class="gl-relative code highlight" as="pre">
+ <node-view-wrapper class="content-editor-code-block gl-relative code highlight" as="pre">
<span
data-testid="frontmatter-label"
class="gl-absolute gl-top-0 gl-right-3"
diff --git a/app/assets/javascripts/content_editor/constants.js b/app/assets/javascripts/content_editor/constants.js
index 4af9dc8e405..5e56078df01 100644
--- a/app/assets/javascripts/content_editor/constants.js
+++ b/app/assets/javascripts/content_editor/constants.js
@@ -49,3 +49,10 @@ export const LOADING_ERROR_EVENT = 'loadingError';
export const PARSE_HTML_PRIORITY_LOWEST = 1;
export const PARSE_HTML_PRIORITY_DEFAULT = 50;
export const PARSE_HTML_PRIORITY_HIGHEST = 100;
+
+export const EXTENSION_PRIORITY_LOWER = 75;
+/**
+ * 100 is the default priority in Tiptap
+ * https://tiptap.dev/guide/custom-extensions/#priority
+ */
+export const EXTENSION_PRIORITY_DEFAULT = 100;
diff --git a/app/assets/javascripts/content_editor/extensions/code.js b/app/assets/javascripts/content_editor/extensions/code.js
index f93c22ad10e..53f6d9b995c 100644
--- a/app/assets/javascripts/content_editor/extensions/code.js
+++ b/app/assets/javascripts/content_editor/extensions/code.js
@@ -1 +1,12 @@
-export { Code as default } from '@tiptap/extension-code';
+import Code from '@tiptap/extension-code';
+import { EXTENSION_PRIORITY_LOWER } from '../constants';
+
+export default Code.extend({
+ excludes: null,
+ /**
+ * Reduce the rendering priority of the code mark to
+ * ensure the bold, italic, and strikethrough marks
+ * are rendered first.
+ */
+ priority: EXTENSION_PRIORITY_LOWER,
+});
diff --git a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
index ea51bee3ba9..9dc17fcd570 100644
--- a/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
+++ b/app/assets/javascripts/content_editor/extensions/code_block_highlight.js
@@ -19,7 +19,14 @@ export default CodeBlockLowlight.extend({
};
},
renderHTML({ HTMLAttributes }) {
- return ['div', ['pre', HTMLAttributes, ['code', {}, 0]]];
+ return [
+ 'pre',
+ {
+ ...HTMLAttributes,
+ class: `content-editor-code-block ${HTMLAttributes.class}`,
+ },
+ ['code', {}, 0],
+ ];
},
}).configure({
lowlight,
diff --git a/app/assets/javascripts/content_editor/extensions/frontmatter.js b/app/assets/javascripts/content_editor/extensions/frontmatter.js
index c09c10bc524..9842027e192 100644
--- a/app/assets/javascripts/content_editor/extensions/frontmatter.js
+++ b/app/assets/javascripts/content_editor/extensions/frontmatter.js
@@ -14,9 +14,20 @@ export default CodeBlockHighlight.extend({
},
];
},
+ addCommands() {
+ return {
+ setFrontmatter: (attributes) => ({ commands }) => {
+ return commands.setNode(this.name, attributes);
+ },
+ toggleFrontmatter: (attributes) => ({ commands }) => {
+ return commands.toggleNode(this.name, 'paragraph', attributes);
+ },
+ };
+ },
addNodeView() {
return new VueNodeViewRenderer(FrontmatterWrapper);
},
+
addInputRules() {
return [];
},
diff --git a/app/assets/javascripts/content_editor/extensions/image.js b/app/assets/javascripts/content_editor/extensions/image.js
index d7fb617f7ee..519f7f168ce 100644
--- a/app/assets/javascripts/content_editor/extensions/image.js
+++ b/app/assets/javascripts/content_editor/extensions/image.js
@@ -66,6 +66,17 @@ export default Image.extend({
},
];
},
+ renderHTML({ HTMLAttributes }) {
+ return [
+ 'img',
+ {
+ src: HTMLAttributes.src,
+ alt: HTMLAttributes.alt,
+ title: HTMLAttributes.title,
+ 'data-canonical-src': HTMLAttributes.canonicalSrc,
+ },
+ ];
+ },
addNodeView() {
return VueNodeViewRenderer(ImageWrapper);
},
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 278ef326c7a..d54fb7cded2 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -65,8 +65,8 @@ import {
const defaultSerializerConfig = {
marks: {
[Bold.name]: defaultMarkdownSerializer.marks.strong,
- [Code.name]: defaultMarkdownSerializer.marks.code,
[Italic.name]: { open: '_', close: '_', mixable: true, expelEnclosingWhitespace: true },
+ [Code.name]: defaultMarkdownSerializer.marks.code,
[Subscript.name]: { open: '<sub>', close: '</sub>', mixable: true },
[Superscript.name]: { open: '<sup>', close: '</sup>', mixable: true },
[InlineDiff.name]: {
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index ed5910fca18..4d5a54c0347 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -1,4 +1,4 @@
-import { uniq } from 'lodash';
+import { uniq, isString } from 'lodash';
const defaultAttrs = {
td: { colspan: 1, rowspan: 1, colwidth: null },
@@ -325,9 +325,12 @@ export function renderHardBreak(state, node, parent, index) {
export function renderImage(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})`);
+ if (isString(src) || isString(canonicalSrc)) {
+ const quotedTitle = title ? ` ${state.quote(title)}` : '';
+
+ state.write(`![${state.esc(alt || '')}](${state.esc(canonicalSrc || src)}${quotedTitle})`);
+ }
}
export function renderPlayable(state, node) {
diff --git a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
index 9b1cb76f845..eb1e4885ba6 100644
--- a/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
+++ b/app/assets/javascripts/content_editor/services/track_input_rules_and_shortcuts.js
@@ -35,31 +35,33 @@ const trackInputRule = (contentType, inputRule) => {
};
const trackInputRulesAndShortcuts = (tiptapExtension) => {
- return tiptapExtension.extend({
- addKeyboardShortcuts() {
- const shortcuts = this.parent?.() || {};
- const { name } = this;
- /**
- * We don’t want to track keyboard shortcuts
- * that are not deliberately executed to create
- * new types of content
- */
- const dotNotTrackKeys = [ENTER_KEY, BACKSPACE_KEY];
- const decorated = mapValues(shortcuts, (commandFn, shortcut) =>
- dotNotTrackKeys.includes(shortcut)
- ? commandFn
- : trackKeyboardShortcut(name, commandFn, shortcut),
- );
+ return tiptapExtension
+ .extend({
+ addKeyboardShortcuts() {
+ const shortcuts = this.parent?.() || {};
+ const { name } = this;
+ /**
+ * We don’t want to track keyboard shortcuts
+ * that are not deliberately executed to create
+ * new types of content
+ */
+ const dotNotTrackKeys = [ENTER_KEY, BACKSPACE_KEY];
+ const decorated = mapValues(shortcuts, (commandFn, shortcut) =>
+ dotNotTrackKeys.includes(shortcut)
+ ? commandFn
+ : trackKeyboardShortcut(name, commandFn, shortcut),
+ );
- return decorated;
- },
- addInputRules() {
- const inputRules = this.parent?.() || [];
- const { name } = this;
+ return decorated;
+ },
+ addInputRules() {
+ const inputRules = this.parent?.() || [];
+ const { name } = this;
- return inputRules.map((inputRule) => trackInputRule(name, inputRule));
- },
- });
+ return inputRules.map((inputRule) => trackInputRule(name, inputRule));
+ },
+ })
+ .configure(tiptapExtension.options);
};
export default trackInputRulesAndShortcuts;
diff --git a/app/assets/javascripts/crm/components/form.vue b/app/assets/javascripts/crm/components/form.vue
new file mode 100644
index 00000000000..b24de1e95e8
--- /dev/null
+++ b/app/assets/javascripts/crm/components/form.vue
@@ -0,0 +1,232 @@
+<script>
+import { GlAlert, GlButton, GlDrawer, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { get as getPropValueByPath, isEmpty } from 'lodash';
+import { produce } from 'immer';
+import { MountingPortal } from 'portal-vue';
+import { __ } from '~/locale';
+import { logError } from '~/lib/logger';
+import { getFirstPropertyValue } from '~/lib/utils/common_utils';
+import { INDEX_ROUTE_NAME } from '../constants';
+
+const MSG_SAVE_CHANGES = __('Save changes');
+const MSG_ERROR = __('Something went wrong. Please try again.');
+const MSG_OPTIONAL = __('(optional)');
+const MSG_CANCEL = __('Cancel');
+
+/**
+ * This component is a first iteration towards a general reusable Create/Update component
+ *
+ * There's some opportunity to improve cohesion of this module which we are planning
+ * to address after solidifying the abstraction's requirements.
+ *
+ * Please see https://gitlab.com/gitlab-org/gitlab/-/issues/349441
+ */
+export default {
+ components: {
+ GlAlert,
+ GlButton,
+ GlDrawer,
+ GlFormGroup,
+ GlFormInput,
+ MountingPortal,
+ },
+ props: {
+ drawerOpen: {
+ type: Boolean,
+ required: true,
+ },
+ fields: {
+ type: Array,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ successMessage: {
+ type: String,
+ required: true,
+ },
+ mutation: {
+ type: Object,
+ required: true,
+ },
+ getQuery: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ getQueryNodePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ existingModel: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ additionalCreateParams: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ buttonLabel: {
+ type: String,
+ required: false,
+ default: () => MSG_SAVE_CHANGES,
+ },
+ },
+ data() {
+ const initialModel = this.fields.reduce(
+ (map, field) =>
+ Object.assign(map, {
+ [field.name]: this.existingModel ? this.existingModel[field.name] : null,
+ }),
+ {},
+ );
+
+ return {
+ model: initialModel,
+ submitting: false,
+ errorMessages: [],
+ };
+ },
+ computed: {
+ isEditMode() {
+ return this.existingModel?.id;
+ },
+ isInvalid() {
+ const { fields, model } = this;
+
+ return fields.some((field) => {
+ return field.required && isEmpty(model[field.name]);
+ });
+ },
+ variables() {
+ const { additionalCreateParams, fields, isEditMode, model } = this;
+
+ const variables = fields.reduce(
+ (map, field) =>
+ Object.assign(map, {
+ [field.name]: this.formatValue(model, field),
+ }),
+ {},
+ );
+
+ if (isEditMode) {
+ return { input: { id: this.existingModel.id, ...variables } };
+ }
+
+ return { input: { ...additionalCreateParams, ...variables } };
+ },
+ },
+ methods: {
+ formatValue(model, field) {
+ if (!isEmpty(model[field.name]) && field.input?.type === 'number') {
+ return parseFloat(model[field.name]);
+ }
+
+ return model[field.name];
+ },
+ save() {
+ const { mutation, variables, close } = this;
+
+ this.submitting = true;
+
+ return this.$apollo
+ .mutate({
+ mutation,
+ variables,
+ update: (store, { data }) => {
+ const { errors, ...result } = getFirstPropertyValue(data);
+
+ if (errors?.length) {
+ this.errorMessages = errors;
+ } else {
+ this.updateCache(store, result);
+ close(true);
+ }
+ },
+ })
+ .catch((e) => {
+ logError(e);
+ this.errorMessages = [MSG_ERROR];
+ })
+ .finally(() => {
+ this.submitting = false;
+ });
+ },
+ close(success) {
+ if (success) {
+ // This is needed so toast perists when route is changed
+ this.$root.$toast.show(this.successMessage);
+ }
+
+ this.$router.replace({ name: this.$options.INDEX_ROUTE_NAME });
+ },
+ updateCache(store, result) {
+ const { getQuery, isEditMode, getQueryNodePath } = this;
+
+ if (isEditMode || !getQuery) return;
+
+ const sourceData = store.readQuery(getQuery);
+
+ const newData = produce(sourceData, (draftState) => {
+ getPropValueByPath(draftState, getQueryNodePath).nodes.push(getFirstPropertyValue(result));
+ });
+
+ store.writeQuery({
+ ...getQuery,
+ data: newData,
+ });
+ },
+ getFieldLabel(field) {
+ const optionalSuffix = field.required ? '' : ` ${MSG_OPTIONAL}`;
+ return field.label + optionalSuffix;
+ },
+ },
+ MSG_CANCEL,
+ INDEX_ROUTE_NAME,
+};
+</script>
+
+<template>
+ <mounting-portal mount-to="#js-crm-form-portal" append>
+ <gl-drawer class="gl-drawer-responsive gl-absolute" :open="drawerOpen" @close="close(false)">
+ <template #title>
+ <h3>{{ title }}</h3>
+ </template>
+ <gl-alert v-if="errorMessages.length" variant="danger" @dismiss="errorMessages = []">
+ <ul class="gl-mb-0! gl-ml-5">
+ <li v-for="error in errorMessages" :key="error">
+ {{ error }}
+ </li>
+ </ul>
+ </gl-alert>
+ <form @submit.prevent="save">
+ <gl-form-group
+ v-for="field in fields"
+ :key="field.name"
+ :label="getFieldLabel(field)"
+ :label-for="field.name"
+ >
+ <gl-form-input :id="field.name" v-bind="field.input" v-model="model[field.name]" />
+ </gl-form-group>
+ <span class="gl-float-right">
+ <gl-button data-testid="cancel-button" @click="close(false)">
+ {{ $options.MSG_CANCEL }}
+ </gl-button>
+ <gl-button
+ variant="confirm"
+ :disabled="isInvalid"
+ :loading="submitting"
+ data-testid="save-button"
+ type="submit"
+ >{{ buttonLabel }}</gl-button
+ >
+ </span>
+ </form>
+ </gl-drawer>
+ </mounting-portal>
+</template>
diff --git a/app/assets/javascripts/cycle_analytics/components/base.vue b/app/assets/javascripts/cycle_analytics/components/base.vue
index 36430e51dd2..bdfabb8e846 100644
--- a/app/assets/javascripts/cycle_analytics/components/base.vue
+++ b/app/assets/javascripts/cycle_analytics/components/base.vue
@@ -153,7 +153,7 @@ export default {
};
</script>
<template>
- <div class="cycle-analytics">
+ <div>
<h3>{{ $options.i18n.pageTitle }}</h3>
<div class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row">
<path-navigation
diff --git a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
index f8f89772fd6..af7334ecf2e 100644
--- a/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
+++ b/app/assets/javascripts/cycle_analytics/components/path_navigation.vue
@@ -57,7 +57,7 @@ export default {
};
</script>
<template>
- <gl-skeleton-loading v-if="loading" :lines="2" class="h-auto pt-2 pb-1" />
+ <gl-skeleton-loading v-if="loading" :lines="2" />
<gl-path v-else :key="selectedStage.id" :items="stages" @selected="onSelectStage">
<template #default="{ pathItem, pathId }">
<gl-popover
diff --git a/app/assets/javascripts/cycle_analytics/components/stage_table.vue b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
index fc4dfafb809..8f7a3f99bab 100644
--- a/app/assets/javascripts/cycle_analytics/components/stage_table.vue
+++ b/app/assets/javascripts/cycle_analytics/components/stage_table.vue
@@ -32,6 +32,9 @@ const WORKFLOW_COLUMN_TITLES = {
mergeRequests: { ...DEFAULT_WORKFLOW_TITLE_PROPERTIES, label: __('Merge requests') },
};
+const fullProjectPath = ({ namespaceFullPath = '', projectPath }) =>
+ namespaceFullPath.split('/').length > 1 ? `${namespaceFullPath}/${projectPath}` : projectPath;
+
export default {
name: 'StageTable',
components: {
@@ -89,6 +92,11 @@ export default {
required: false,
default: true,
},
+ includeProjectName: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
if (this.pagination) {
@@ -144,8 +152,15 @@ export default {
isMrLink(url = '') {
return url.includes('/merge_request');
},
- itemId({ url, iid }) {
- return this.isMrLink(url) ? `!${iid}` : `#${iid}`;
+ itemId({ iid, projectPath, namespaceFullPath = '' }, separator = '#') {
+ const prefix = this.includeProjectName
+ ? fullProjectPath({ namespaceFullPath, projectPath })
+ : '';
+ return `${prefix}${separator}${iid}`;
+ },
+ itemDisplayName(item) {
+ const separator = this.isMrLink(item.url) ? '!' : '#';
+ return this.itemId(item, separator);
},
itemTitle(item) {
return item.title || item.name;
@@ -201,8 +216,11 @@ export default {
<div data-testid="vsa-stage-event">
<div v-if="item.id" data-testid="vsa-stage-content">
<p class="gl-m-0">
- <gl-link class="gl-text-black-normal pipeline-id" :href="item.url"
- >#{{ item.id }}</gl-link
+ <gl-link
+ data-testid="vsa-stage-event-link"
+ class="gl-text-black-normal"
+ :href="item.url"
+ >{{ itemId(item.id, '#') }}</gl-link
>
<gl-icon :size="16" name="fork" />
<gl-link
@@ -240,7 +258,12 @@ export default {
<gl-link class="gl-text-black-normal" :href="item.url">{{ itemTitle(item) }}</gl-link>
</h5>
<p class="gl-m-0">
- <gl-link class="gl-text-black-normal" :href="item.url">{{ itemId(item) }}</gl-link>
+ <gl-link
+ data-testid="vsa-stage-event-link"
+ class="gl-text-black-normal"
+ :href="item.url"
+ >{{ itemDisplayName(item) }}</gl-link
+ >
<span class="gl-font-lg">&middot;</span>
<span data-testid="vsa-stage-event-date">
{{ s__('OpenedNDaysAgo|Opened') }}
diff --git a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
index 8610dfc2b03..64461797c46 100644
--- a/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
+++ b/app/assets/javascripts/cycle_analytics/components/value_stream_filters.vue
@@ -59,7 +59,9 @@ export default {
};
</script>
<template>
- <div class="gl-mt-3 gl-py-2 gl-px-3 bg-gray-light border-top border-bottom">
+ <div
+ class="gl-mt-3 gl-py-2 gl-px-3 gl-bg-gray-10 gl-border-b-1 gl-border-b-solid gl-border-t-1 gl-border-t-solid gl-border-gray-100"
+ >
<filter-bar
data-testid="vsa-filter-bar"
class="filtered-search-box gl-display-flex gl-mb-2 gl-mr-3 gl-border-none"
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 10976202d06..7fefbab977d 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
@@ -7,6 +7,7 @@ import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vu
import { updateGlobalTodoCount } from '~/vue_shared/components/sidebar/todo_toggle/utils';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import DesignNotePin from '~/vue_shared/components/design_management/design_note_pin.vue';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../../constants';
import createNoteMutation from '../../graphql/mutations/create_note.mutation.graphql';
import toggleResolveDiscussionMutation from '../../graphql/mutations/toggle_resolve_discussion.mutation.graphql';
@@ -17,6 +18,7 @@ import { hasErrors } from '../../utils/cache_update';
import { extractDesign } from '../../utils/design_management_utils';
import { ADD_DISCUSSION_COMMENT_ERROR } from '../../utils/error_messages';
import DesignNote from './design_note.vue';
+import DesignNoteSignedOut from './design_note_signed_out.vue';
import DesignReplyForm from './design_reply_form.vue';
import ToggleRepliesWidget from './toggle_replies_widget.vue';
@@ -24,6 +26,7 @@ export default {
components: {
ApolloMutation,
DesignNote,
+ DesignNoteSignedOut,
ReplyPlaceholder,
DesignReplyForm,
GlIcon,
@@ -55,6 +58,14 @@ export default {
required: false,
default: '',
},
+ registerPath: {
+ type: String,
+ required: true,
+ },
+ signInPath: {
+ type: String,
+ required: true,
+ },
resolvedDiscussionsExpanded: {
type: Boolean,
required: true,
@@ -93,6 +104,7 @@ export default {
isResolving: false,
shouldChangeResolvedStatus: false,
areRepliesCollapsed: this.discussion.resolved,
+ isLoggedIn: isLoggedIn(),
};
},
computed: {
@@ -226,7 +238,7 @@ export default {
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
>
- <template v-if="discussion.resolvable" #resolve-discussion>
+ <template v-if="isLoggedIn && discussion.resolvable" #resolve-discussion>
<button
v-gl-tooltip
:class="{ 'is-active': discussion.resolved }"
@@ -269,38 +281,47 @@ export default {
:class="{ 'gl-bg-blue-50': isDiscussionActive }"
@error="$emit('update-note-error', $event)"
/>
- <li v-show="isReplyPlaceholderVisible" class="reply-wrapper discussion-reply-holder">
- <reply-placeholder
- v-if="!isFormVisible"
- class="qa-discussion-reply"
- :placeholder-text="__('Reply…')"
- @focus="showForm"
- />
- <apollo-mutation
- v-else
- #default="{ mutate, loading }"
- :mutation="$options.createNoteMutation"
- :variables="{
- input: mutationPayload,
- }"
- @done="onDone"
- @error="onCreateNoteError"
- >
- <design-reply-form
- v-model="discussionComment"
- :is-saving="loading"
- :markdown-preview-path="markdownPreviewPath"
- @submit-form="mutate"
- @cancel-form="hideForm"
+ <li
+ v-show="isReplyPlaceholderVisible"
+ class="reply-wrapper discussion-reply-holder"
+ :class="{ 'gl-bg-gray-10': !isLoggedIn }"
+ >
+ <template v-if="!isLoggedIn">
+ <design-note-signed-out :register-path="registerPath" :sign-in-path="signInPath" />
+ </template>
+ <template v-else>
+ <reply-placeholder
+ v-if="!isFormVisible"
+ class="qa-discussion-reply"
+ :placeholder-text="__('Reply…')"
+ @focus="showForm"
+ />
+ <apollo-mutation
+ v-else
+ #default="{ mutate, loading }"
+ :mutation="$options.createNoteMutation"
+ :variables="{
+ input: mutationPayload,
+ }"
+ @done="onDone"
+ @error="onCreateNoteError"
>
- <template v-if="discussion.resolvable" #resolve-checkbox>
- <label data-testid="resolve-checkbox">
- <input v-model="shouldChangeResolvedStatus" type="checkbox" />
- {{ resolveCheckboxText }}
- </label>
- </template>
- </design-reply-form>
- </apollo-mutation>
+ <design-reply-form
+ v-model="discussionComment"
+ :is-saving="loading"
+ :markdown-preview-path="markdownPreviewPath"
+ @submit-form="mutate"
+ @cancel-form="hideForm"
+ >
+ <template v-if="discussion.resolvable" #resolve-checkbox>
+ <label data-testid="resolve-checkbox">
+ <input v-model="shouldChangeResolvedStatus" type="checkbox" />
+ {{ resolveCheckboxText }}
+ </label>
+ </template>
+ </design-reply-form>
+ </apollo-mutation>
+ </template>
</li>
</ul>
</div>
diff --git a/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
new file mode 100644
index 00000000000..f0812e62bba
--- /dev/null
+++ b/app/assets/javascripts/design_management/components/design_notes/design_note_signed_out.vue
@@ -0,0 +1,50 @@
+<script>
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlSprintf,
+ GlLink,
+ },
+ props: {
+ registerPath: {
+ type: String,
+ required: true,
+ },
+ signInPath: {
+ type: String,
+ required: true,
+ },
+ isAddDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ signedOutText() {
+ return this.isAddDiscussion
+ ? __(
+ 'Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to start a new discussion.',
+ )
+ : __(
+ 'Please %{registerLinkStart}register%{registerLinkEnd} or %{signInLinkStart}sign in%{signInLinkEnd} to reply.',
+ );
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="disabled-comment text-center">
+ <gl-sprintf :message="signedOutText">
+ <template #registerLink="{ content }">
+ <gl-link :href="registerPath">{{ content }}</gl-link>
+ </template>
+ <template #signInLink="{ content }">
+ <gl-link :href="signInPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+</template>
diff --git a/app/assets/javascripts/design_management/components/design_presentation.vue b/app/assets/javascripts/design_management/components/design_presentation.vue
index 11d2f3b2e37..5116bacefa5 100644
--- a/app/assets/javascripts/design_management/components/design_presentation.vue
+++ b/app/assets/javascripts/design_management/components/design_presentation.vue
@@ -1,5 +1,6 @@
<script>
import { throttle } from 'lodash';
+import { isLoggedIn } from '~/lib/utils/common_utils';
import DesignOverlay from './design_overlay.vue';
import DesignImage from './image.vue';
@@ -54,6 +55,7 @@ export default {
initialLoad: true,
lastDragPosition: null,
isDraggingDesign: false,
+ isLoggedIn: isLoggedIn(),
};
},
computed: {
@@ -311,7 +313,7 @@ export default {
:position="overlayPosition"
:notes="discussionStartingNotes"
:current-comment-form="currentCommentForm"
- :disable-commenting="isDraggingDesign"
+ :disable-commenting="!isLoggedIn || isDraggingDesign"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
@openCommentForm="openCommentForm"
@closeCommentForm="closeCommentForm"
diff --git a/app/assets/javascripts/design_management/components/design_sidebar.vue b/app/assets/javascripts/design_management/components/design_sidebar.vue
index ced76eb4843..6d0ed3b08a3 100644
--- a/app/assets/javascripts/design_management/components/design_sidebar.vue
+++ b/app/assets/javascripts/design_management/components/design_sidebar.vue
@@ -1,7 +1,7 @@
<script>
import { GlCollapse, GlButton, GlPopover } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import { parseBoolean } from '~/lib/utils/common_utils';
+import { parseBoolean, isLoggedIn } from '~/lib/utils/common_utils';
import { s__ } from '~/locale';
import Participants from '~/sidebar/components/participants/participants.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -9,11 +9,13 @@ import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '../constants';
import updateActiveDiscussionMutation from '../graphql/mutations/update_active_discussion.mutation.graphql';
import { extractDiscussions, extractParticipants } from '../utils/design_management_utils';
import DesignDiscussion from './design_notes/design_discussion.vue';
+import DesignNoteSignedOut from './design_notes/design_note_signed_out.vue';
import DesignTodoButton from './design_todo_button.vue';
export default {
components: {
DesignDiscussion,
+ DesignNoteSignedOut,
Participants,
GlCollapse,
GlButton,
@@ -28,6 +30,12 @@ export default {
issueIid: {
default: '',
},
+ registerPath: {
+ default: '',
+ },
+ signInPath: {
+ default: '',
+ },
},
props: {
design: {
@@ -47,6 +55,7 @@ export default {
return {
isResolvedCommentsPopoverHidden: parseBoolean(Cookies.get(this.$options.cookieKey)),
discussionWithOpenForm: '',
+ isLoggedIn: isLoggedIn(),
};
},
computed: {
@@ -134,12 +143,19 @@ export default {
class="gl-mb-4"
/>
<h2
- v-if="unresolvedDiscussions.length === 0"
+ v-if="isLoggedIn && unresolvedDiscussions.length === 0"
class="new-discussion-disclaimer gl-font-base gl-m-0 gl-mb-4"
data-testid="new-discussion-disclaimer"
>
{{ s__("DesignManagement|Click the image where you'd like to start a new discussion") }}
</h2>
+ <design-note-signed-out
+ v-if="!isLoggedIn"
+ class="gl-mb-4"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
+ :is-add-discussion="true"
+ />
<design-discussion
v-for="discussion in unresolvedDiscussions"
:key="discussion.id"
@@ -147,6 +163,8 @@ export default {
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="unresolved-discussion"
@@ -197,6 +215,8 @@ export default {
:design-id="$route.params.id"
:noteable-id="design.id"
:markdown-preview-path="markdownPreviewPath"
+ :register-path="registerPath"
+ :sign-in-path="signInPath"
:resolved-discussions-expanded="resolvedDiscussionsExpanded"
:discussion-with-open-form="discussionWithOpenForm"
data-testid="resolved-discussion"
diff --git a/app/assets/javascripts/design_management/index.js b/app/assets/javascripts/design_management/index.js
index 11666587265..4ae76050aa5 100644
--- a/app/assets/javascripts/design_management/index.js
+++ b/app/assets/javascripts/design_management/index.js
@@ -8,7 +8,7 @@ import createRouter from './router';
export default () => {
const el = document.querySelector('.js-design-management');
- const { issueIid, projectPath, issuePath } = el.dataset;
+ const { issueIid, projectPath, issuePath, registerPath, signInPath } = el.dataset;
const router = createRouter(issuePath);
apolloProvider.clients.defaultClient.cache.writeQuery({
@@ -29,6 +29,8 @@ export default () => {
provide: {
projectPath,
issueIid,
+ registerPath,
+ signInPath,
},
mounted() {
performanceMarkAndMeasure({
diff --git a/app/assets/javascripts/diffs/components/compare_versions.vue b/app/assets/javascripts/diffs/components/compare_versions.vue
index da918947cc5..442807587d5 100644
--- a/app/assets/javascripts/diffs/components/compare_versions.vue
+++ b/app/assets/javascripts/diffs/components/compare_versions.vue
@@ -182,7 +182,7 @@ export default {
class="gl-mr-3"
@click="expandAllFiles"
>
- {{ __('Expand all') }}
+ {{ __('Expand all files') }}
</gl-button>
<settings-dropdown />
</div>
diff --git a/app/assets/javascripts/diffs/components/image_diff_overlay.vue b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
index 5572338908f..eede8e52292 100644
--- a/app/assets/javascripts/diffs/components/image_diff_overlay.vue
+++ b/app/assets/javascripts/diffs/components/image_diff_overlay.vue
@@ -4,8 +4,8 @@ import { isArray } from 'lodash';
import { mapActions, mapGetters } from 'vuex';
import imageDiffMixin from 'ee_else_ce/diffs/mixins/image_diff';
-function calcPercent(pos, size, renderedSize) {
- return (((pos / size) * 100) / ((renderedSize / size) * 100)) * 100;
+function calcPercent(pos, renderedSize) {
+ return (100 * pos) / renderedSize;
}
export default {
@@ -65,8 +65,8 @@ export default {
...mapActions('diffs', ['openDiffFileCommentForm']),
getImageDimensions() {
return {
- width: this.$parent.width,
- height: this.$parent.height,
+ width: Math.round(this.$parent.width),
+ height: Math.round(this.$parent.height),
};
},
getPositionForObject(meta) {
@@ -87,15 +87,15 @@ export default {
},
clickedImage(x, y) {
const { width, height } = this.getImageDimensions();
- const xPercent = calcPercent(x, width, this.renderedWidth);
- const yPercent = calcPercent(y, height, this.renderedHeight);
+ const xPercent = calcPercent(x, this.renderedWidth);
+ const yPercent = calcPercent(y, this.renderedHeight);
this.openDiffFileCommentForm({
fileHash: this.fileHash,
width,
height,
- x: width * (xPercent / 100),
- y: height * (yPercent / 100),
+ x: Math.round(width * (xPercent / 100)),
+ y: Math.round(height * (yPercent / 100)),
xPercent,
yPercent,
});
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 7c7127dfa44..491c2ced358 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -51,7 +51,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
// Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area');
- form.setupMarkdownPreview();
const $formDropzone = form.find('.div-dropzone');
$formDropzone.parent().addClass('div-dropzone-wrapper');
$formDropzone.append(divHover);
diff --git a/app/assets/javascripts/editor/source_editor.js b/app/assets/javascripts/editor/source_editor.js
index 57e2b0da565..fa749112ab5 100644
--- a/app/assets/javascripts/editor/source_editor.js
+++ b/app/assets/javascripts/editor/source_editor.js
@@ -149,7 +149,7 @@ export default class SourceEditor {
});
this.instances.push(instance);
- el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { instance }));
+ el.dispatchEvent(new CustomEvent(EDITOR_READY_EVENT, { detail: { instance } }));
return instance;
}
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index dc3eac0cd0c..686b5ffff9e 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -28,6 +28,16 @@ export default {
required: false,
default: () => [],
},
+ right: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ boundary: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -62,7 +72,7 @@ export default {
addToFrequentlyUsed(name);
},
getBoundaryElement() {
- return document.querySelector('.content-wrapper') || 'scrollParent';
+ return this.boundary || document.querySelector('.content-wrapper') || 'scrollParent';
},
onSearchInput() {
this.$refs.virtualScoller.setScrollTop(0);
@@ -87,7 +97,7 @@ export default {
menu-class="dropdown-extended-height"
category="secondary"
no-flip
- right
+ :right="right"
lazy
@shown="$emit('shown')"
@hidden="$emit('hidden')"
@@ -115,7 +125,7 @@ export default {
:aria-label="category.name"
@click="scrollToCategory(category.name)"
>
- <gl-icon :name="category.icon" :size="12" />
+ <gl-icon :name="category.icon" />
</button>
</div>
<emoji-list :search-value="searchValue">
diff --git a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
index 0e556f093e2..ce919f73858 100644
--- a/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
+++ b/app/assets/javascripts/environments/components/confirm_rollback_modal.vue
@@ -99,8 +99,7 @@ export default {
};
},
isLastDeployment() {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return this.environment?.isLastDeployment || this.environment?.lastDeployment?.['last?'];
+ return this.environment?.isLastDeployment || this.environment?.lastDeployment?.isLast;
},
},
methods: {
diff --git a/app/assets/javascripts/environments/components/deployment.vue b/app/assets/javascripts/environments/components/deployment.vue
new file mode 100644
index 00000000000..ef43ca6bc33
--- /dev/null
+++ b/app/assets/javascripts/environments/components/deployment.vue
@@ -0,0 +1,25 @@
+<script>
+import DeploymentStatusBadge from './deployment_status_badge.vue';
+
+export default {
+ components: {
+ DeploymentStatusBadge,
+ },
+ props: {
+ deployment: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ status() {
+ return this.deployment?.status;
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <deployment-status-badge v-if="status" :status="status" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/deployment_status_badge.vue b/app/assets/javascripts/environments/components/deployment_status_badge.vue
new file mode 100644
index 00000000000..5a026911766
--- /dev/null
+++ b/app/assets/javascripts/environments/components/deployment_status_badge.vue
@@ -0,0 +1,60 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+const STATUS_TEXT = {
+ created: s__('Deployment|Created'),
+ running: s__('Deployment|Running'),
+ success: s__('Deployment|Success'),
+ failed: s__('Deployment|Failed'),
+ canceled: s__('Deployment|Cancelled'),
+ skipped: s__('Deployment|Skipped'),
+ blocked: s__('Deployment|Waiting'),
+};
+
+const STATUS_VARIANT = {
+ success: 'success',
+ running: 'info',
+ failed: 'danger',
+ created: 'neutral',
+ canceled: 'neutral',
+ skipped: 'neutral',
+ blocked: 'neutral',
+};
+
+const STATUS_ICON = {
+ success: 'status_success',
+ running: 'status_running',
+ failed: 'status_failed',
+ created: 'status_created',
+ canceled: 'status_canceled',
+ skipped: 'status_skipped',
+ blocked: 'status_manual',
+};
+
+export default {
+ components: {
+ GlBadge,
+ },
+ props: {
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ icon() {
+ return STATUS_ICON[this.status];
+ },
+ text() {
+ return STATUS_TEXT[this.status];
+ },
+ variant() {
+ return STATUS_VARIANT[this.status];
+ },
+ },
+};
+</script>
+<template>
+ <gl-badge v-if="status" :icon="icon" :variant="variant">{{ text }}</gl-badge>
+</template>
diff --git a/app/assets/javascripts/environments/components/environment_actions.vue b/app/assets/javascripts/environments/components/environment_actions.vue
index 2d98f00433a..98c95507168 100644
--- a/app/assets/javascripts/environments/components/environment_actions.vue
+++ b/app/assets/javascripts/environments/components/environment_actions.vue
@@ -1,8 +1,9 @@
<script>
-import { GlDropdown, GlDropdownItem, GlIcon, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
import eventHub from '../event_hub';
+import actionMutation from '../graphql/mutations/action.mutation.graphql';
export default {
directives: {
@@ -12,7 +13,6 @@ export default {
GlDropdown,
GlDropdownItem,
GlIcon,
- GlLoadingIcon,
},
props: {
actions: {
@@ -20,6 +20,11 @@ export default {
required: false,
default: () => [],
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -49,7 +54,11 @@ export default {
this.isLoading = true;
- eventHub.$emit('postAction', { endpoint: action.playPath });
+ if (this.graphql) {
+ this.$apollo.mutate({ mutation: actionMutation, variables: { action } });
+ } else {
+ eventHub.$emit('postAction', { endpoint: action.playPath });
+ }
},
isActionDisabled(action) {
@@ -70,18 +79,16 @@ export default {
<template>
<gl-dropdown
v-gl-tooltip
+ :text="title"
:title="title"
+ :loading="isLoading"
:aria-label="title"
- :disabled="isLoading"
+ icon="play"
+ text-sr-only
right
data-container="body"
data-testid="environment-actions-button"
>
- <template #button-content>
- <gl-icon name="play" />
- <gl-icon name="chevron-down" />
- <gl-loading-icon v-if="isLoading" size="sm" />
- </template>
<gl-dropdown-item
v-for="(action, i) in actions"
:key="i"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index be9bfb50de5..cfe35d26b94 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlDropdown, GlTooltipDirective, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
+import { GlDropdown, GlTooltipDirective, GlIcon, GlLink, GlSprintf, GlBadge } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
@@ -30,6 +30,7 @@ export default {
CommitComponent,
ExternalUrlComponent,
GlDropdown,
+ GlBadge,
GlIcon,
GlLink,
GlSprintf,
@@ -621,9 +622,9 @@ export default {
<span v-if="model.size === 1">{{ model.name }}</span>
<span v-else>{{ model.name_without_type }}</span>
</a>
- <span v-if="isProtected" class="badge badge-success">
- {{ s__('Environments|protected') }}
- </span>
+ <gl-badge v-if="isProtected" variant="success">{{
+ s__('Environments|protected')
+ }}</gl-badge>
</span>
<span
v-else
@@ -639,7 +640,7 @@ export default {
<span> {{ model.folderName }} </span>
- <span class="badge badge-pill"> {{ model.size }} </span>
+ <gl-badge>{{ model.size }}</gl-badge>
</span>
</div>
diff --git a/app/assets/javascripts/environments/components/environment_stop.vue b/app/assets/javascripts/environments/components/environment_stop.vue
index 0d4a1e76eb8..17a70fd0c34 100644
--- a/app/assets/javascripts/environments/components/environment_stop.vue
+++ b/app/assets/javascripts/environments/components/environment_stop.vue
@@ -8,6 +8,8 @@ import { GlTooltipDirective, GlButton, GlModalDirective } from '@gitlab/ui';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import eventHub from '../event_hub';
+import setEnvironmentToStopMutation from '../graphql/mutations/set_environment_to_stop.mutation.graphql';
+import isEnvironmentStoppingQuery from '../graphql/queries/is_environment_stopping.query.graphql';
export default {
components: {
@@ -22,6 +24,19 @@ export default {
type: Object,
required: true,
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ apollo: {
+ isEnvironmentStopping: {
+ query: isEnvironmentStoppingQuery,
+ variables() {
+ return { environment: this.environment };
+ },
+ },
},
i18n: {
title: s__('Environments|Stop environment'),
@@ -30,6 +45,7 @@ export default {
data() {
return {
isLoading: false,
+ isEnvironmentStopping: false,
};
},
mounted() {
@@ -41,7 +57,14 @@ export default {
methods: {
onClick() {
this.$root.$emit(BV_HIDE_TOOLTIP, this.$options.stopEnvironmentTooltipId);
- eventHub.$emit('requestStopEnvironment', this.environment);
+ if (this.graphql) {
+ this.$apollo.mutate({
+ mutation: setEnvironmentToStopMutation,
+ variables: { environment: this.environment },
+ });
+ } else {
+ eventHub.$emit('requestStopEnvironment', this.environment);
+ }
},
onStopEnvironment(environment) {
if (this.environment.id === environment.id) {
@@ -56,7 +79,7 @@ export default {
<gl-button
v-gl-tooltip="{ id: $options.stopEnvironmentTooltipId }"
v-gl-modal-directive="'stop-environment-modal'"
- :loading="isLoading"
+ :loading="isLoading || isEnvironmentStopping"
:title="$options.i18n.title"
:aria-label="$options.i18n.title"
icon="stop"
diff --git a/app/assets/javascripts/environments/components/new_environment_folder.vue b/app/assets/javascripts/environments/components/new_environment_folder.vue
index fe3d6f1e8ca..0d3867a4d74 100644
--- a/app/assets/javascripts/environments/components/new_environment_folder.vue
+++ b/app/assets/javascripts/environments/components/new_environment_folder.vue
@@ -2,9 +2,11 @@
import { GlButton, GlCollapse, GlIcon, GlBadge, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import folderQuery from '../graphql/queries/folder.query.graphql';
+import EnvironmentItem from './new_environment_item.vue';
export default {
components: {
+ EnvironmentItem,
GlButton,
GlCollapse,
GlIcon,
@@ -51,17 +53,26 @@ export default {
folderPath() {
return this.nestedEnvironment.latest.folderPath;
},
+ environments() {
+ return this.folder?.environments;
+ },
},
methods: {
toggleCollapse() {
this.visible = !this.visible;
},
+ isFirstEnvironment(index) {
+ return index === 0;
+ },
},
};
</script>
<template>
- <div class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-px-3 gl-pt-3 gl-pb-5">
- <div class="gl-w-full gl-display-flex gl-align-items-center">
+ <div
+ :class="{ 'gl-pb-5': !visible }"
+ class="gl-border-b-solid gl-border-gray-100 gl-border-1 gl-pt-3"
+ >
+ <div class="gl-w-full gl-display-flex gl-align-items-center gl-px-3">
<gl-button
class="gl-mr-4 gl-fill-current-color gl-text-gray-500"
:aria-label="label"
@@ -77,6 +88,15 @@ export default {
<gl-badge size="sm" class="gl-mr-auto">{{ count }}</gl-badge>
<gl-link v-if="visible" :href="folderPath">{{ $options.i18n.link }}</gl-link>
</div>
- <gl-collapse :visible="visible" />
+ <gl-collapse :visible="visible">
+ <environment-item
+ v-for="(environment, index) in environments"
+ :key="environment.name"
+ :environment="environment"
+ :class="{ 'gl-mt-5': isFirstEnvironment(index) }"
+ class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3"
+ in-folder
+ />
+ </gl-collapse>
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
new file mode 100644
index 00000000000..d3624103c13
--- /dev/null
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -0,0 +1,265 @@
+<script>
+import {
+ GlCollapse,
+ GlDropdown,
+ GlButton,
+ GlLink,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+import { truncate } from '~/lib/utils/text_utility';
+import isLastDeployment from '../graphql/queries/is_last_deployment.query.graphql';
+import ExternalUrl from './environment_external_url.vue';
+import Actions from './environment_actions.vue';
+import StopComponent from './environment_stop.vue';
+import Rollback from './environment_rollback.vue';
+import Pin from './environment_pin.vue';
+import Monitoring from './environment_monitoring.vue';
+import Terminal from './environment_terminal_button.vue';
+import Delete from './environment_delete.vue';
+import Deployment from './deployment.vue';
+
+export default {
+ components: {
+ GlCollapse,
+ GlDropdown,
+ GlButton,
+ GlLink,
+ Actions,
+ Deployment,
+ ExternalUrl,
+ StopComponent,
+ Rollback,
+ Monitoring,
+ Pin,
+ Terminal,
+ Delete,
+ },
+ directives: {
+ GlTooltip,
+ },
+ props: {
+ environment: {
+ required: true,
+ type: Object,
+ },
+ inFolder: {
+ required: false,
+ default: false,
+ type: Boolean,
+ },
+ },
+ apollo: {
+ isLastDeployment: {
+ query: isLastDeployment,
+ variables() {
+ return { environment: this.environment };
+ },
+ },
+ },
+ i18n: {
+ collapse: __('Collapse'),
+ expand: __('Expand'),
+ },
+ data() {
+ return { visible: false };
+ },
+ computed: {
+ icon() {
+ return this.visible ? 'angle-down' : 'angle-right';
+ },
+ externalUrl() {
+ return this.environment.externalUrl;
+ },
+ name() {
+ return this.inFolder ? this.environment.nameWithoutType : this.environment.name;
+ },
+ label() {
+ return this.visible ? this.$options.i18n.collapse : this.$options.i18n.expand;
+ },
+ lastDeployment() {
+ return this.environment?.lastDeployment;
+ },
+ upcomingDeployment() {
+ return this.environment?.upcomingDeployment;
+ },
+ actions() {
+ if (!this.lastDeployment) {
+ return [];
+ }
+ const { manualActions = [], scheduledActions = [] } = this.lastDeployment;
+ const combinedActions = [...manualActions, ...scheduledActions];
+ return combinedActions.map((action) => ({
+ ...action,
+ }));
+ },
+ canStop() {
+ return this.environment?.canStop;
+ },
+ retryPath() {
+ return this.lastDeployment?.deployable?.retryPath;
+ },
+ hasExtraActions() {
+ return Boolean(
+ this.retryPath ||
+ this.canShowAutoStopDate ||
+ this.metricsPath ||
+ this.terminalPath ||
+ this.canDeleteEnvironment,
+ );
+ },
+ canShowAutoStopDate() {
+ if (!this.environment?.autoStopAt) {
+ return false;
+ }
+
+ const autoStopDate = new Date(this.environment?.autoStopAt);
+ const now = new Date();
+
+ return now < autoStopDate;
+ },
+ autoStopPath() {
+ return this.environment?.cancelAutoStopPath ?? '';
+ },
+ metricsPath() {
+ return this.environment?.metricsPath ?? '';
+ },
+ terminalPath() {
+ return this.environment?.terminalPath ?? '';
+ },
+ canDeleteEnvironment() {
+ return Boolean(this.environment?.canDelete && this.environment?.deletePath);
+ },
+ displayName() {
+ return truncate(this.name, 80);
+ },
+ },
+ methods: {
+ toggleCollapse() {
+ this.visible = !this.visible;
+ },
+ },
+ deploymentClasses: [
+ 'gl-border-gray-100',
+ 'gl-border-t-solid',
+ 'gl-border-1',
+ 'gl-py-5',
+ 'gl-pl-7',
+ 'gl-bg-gray-10',
+ ],
+};
+</script>
+<template>
+ <div>
+ <div
+ class="gl-px-3 gl-pt-3 gl-pb-5 gl-display-flex gl-justify-content-space-between gl-align-items-center"
+ >
+ <div
+ :class="{ 'gl-ml-7': inFolder }"
+ class="gl-min-w-0 gl-mr-4 gl-display-flex gl-align-items-center"
+ >
+ <gl-button
+ class="gl-mr-4 gl-min-w-fit-content"
+ :icon="icon"
+ :aria-label="label"
+ size="small"
+ category="tertiary"
+ @click="toggleCollapse"
+ />
+ <gl-link
+ v-gl-tooltip
+ :href="environment.environmentPath"
+ class="gl-text-blue-500 gl-text-truncate"
+ :class="{ 'gl-font-weight-bold': visible }"
+ :title="name"
+ >
+ {{ displayName }}
+ </gl-link>
+ </div>
+ <div>
+ <div class="btn-group table-action-buttons" role="group">
+ <external-url
+ v-if="externalUrl"
+ :external-url="externalUrl"
+ data-track-action="click_button"
+ data-track-label="environment_url"
+ />
+
+ <actions
+ v-if="actions.length > 0"
+ :actions="actions"
+ data-track-action="click_dropdown"
+ data-track-label="environment_actions"
+ graphql
+ />
+
+ <stop-component
+ v-if="canStop"
+ :environment="environment"
+ class="gl-z-index-2"
+ data-track-action="click_button"
+ data-track-label="environment_stop"
+ graphql
+ />
+
+ <gl-dropdown
+ v-if="hasExtraActions"
+ icon="ellipsis_v"
+ text-sr-only
+ :text="__('More actions')"
+ category="secondary"
+ no-caret
+ right
+ >
+ <rollback
+ v-if="retryPath"
+ :environment="environment"
+ :is-last-deployment="isLastDeployment"
+ :retry-url="retryPath"
+ graphql
+ data-track-action="click_button"
+ data-track-label="environment_rollback"
+ />
+
+ <pin
+ v-if="canShowAutoStopDate"
+ :auto-stop-url="autoStopPath"
+ data-track-action="click_button"
+ data-track-label="environment_pin"
+ />
+
+ <monitoring
+ v-if="metricsPath"
+ :monitoring-url="metricsPath"
+ data-track-action="click_button"
+ data-track-label="environment_monitoring"
+ />
+
+ <terminal
+ v-if="terminalPath"
+ :terminal-path="terminalPath"
+ data-track-action="click_button"
+ data-track-label="environment_terminal"
+ />
+
+ <delete
+ v-if="canDeleteEnvironment"
+ :environment="environment"
+ data-track-action="click_button"
+ data-track-label="environment_delete"
+ graphql
+ />
+ </gl-dropdown>
+ </div>
+ </div>
+ </div>
+ <gl-collapse :visible="visible">
+ <div v-if="lastDeployment" :class="$options.deploymentClasses">
+ <deployment :deployment="lastDeployment" :class="{ 'gl-ml-7': inFolder }" />
+ </div>
+ <div v-if="upcomingDeployment" :class="$options.deploymentClasses">
+ <deployment :deployment="upcomingDeployment" :class="{ 'gl-ml-7': inFolder }" />
+ </div>
+ </gl-collapse>
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/components/new_environments_app.vue b/app/assets/javascripts/environments/components/new_environments_app.vue
index 8d94e7021ca..cb36e226d0e 100644
--- a/app/assets/javascripts/environments/components/new_environments_app.vue
+++ b/app/assets/javascripts/environments/components/new_environments_app.vue
@@ -5,13 +5,24 @@ import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_util
import environmentAppQuery from '../graphql/queries/environment_app.query.graphql';
import pollIntervalQuery from '../graphql/queries/poll_interval.query.graphql';
import pageInfoQuery from '../graphql/queries/page_info.query.graphql';
+import environmentToDeleteQuery from '../graphql/queries/environment_to_delete.query.graphql';
+import environmentToRollbackQuery from '../graphql/queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from '../graphql/queries/environment_to_stop.query.graphql';
import EnvironmentFolder from './new_environment_folder.vue';
import EnableReviewAppModal from './enable_review_app_modal.vue';
+import StopEnvironmentModal from './stop_environment_modal.vue';
+import EnvironmentItem from './new_environment_item.vue';
+import ConfirmRollbackModal from './confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from './delete_environment_modal.vue';
export default {
components: {
+ DeleteEnvironmentModal,
+ ConfirmRollbackModal,
EnvironmentFolder,
EnableReviewAppModal,
+ EnvironmentItem,
+ StopEnvironmentModal,
GlBadge,
GlPagination,
GlTab,
@@ -36,6 +47,15 @@ export default {
pageInfo: {
query: pageInfoQuery,
},
+ environmentToDelete: {
+ query: environmentToDeleteQuery,
+ },
+ environmentToRollback: {
+ query: environmentToRollbackQuery,
+ },
+ environmentToStop: {
+ query: environmentToStopQuery,
+ },
},
inject: ['newEnvironmentPath', 'canCreateEnvironment'],
i18n: {
@@ -57,6 +77,9 @@ export default {
isReviewAppModalVisible: false,
page: parseInt(page, 10),
scope,
+ environmentToDelete: {},
+ environmentToRollback: {},
+ environmentToStop: {},
};
},
computed: {
@@ -64,7 +87,10 @@ export default {
return this.environmentApp?.reviewApp?.canSetupReviewApp;
},
folders() {
- return this.environmentApp?.environments.filter((e) => e.size > 1) ?? [];
+ return this.environmentApp?.environments?.filter((e) => e.size > 1) ?? [];
+ },
+ environments() {
+ return this.environmentApp?.environments?.filter((e) => e.size === 1) ?? [];
},
availableCount() {
return this.environmentApp?.availableCount;
@@ -119,7 +145,7 @@ export default {
},
setScope(scope) {
this.scope = scope;
- this.resetPolling();
+ this.moveToPage(1);
},
movePage(direction) {
this.moveToPage(this.pageInfo[`${direction}Page`]);
@@ -157,6 +183,9 @@ export default {
:modal-id="$options.modalId"
data-testid="enable-review-app-modal"
/>
+ <delete-environment-modal :environment="environmentToDelete" graphql />
+ <stop-environment-modal :environment="environmentToStop" graphql />
+ <confirm-rollback-modal :environment="environmentToRollback" graphql />
<gl-tabs
:action-secondary="addEnvironment"
:action-primary="openReviewAppModal"
@@ -187,6 +216,12 @@ export default {
class="gl-mb-3"
:nested-environment="folder"
/>
+ <environment-item
+ v-for="environment in environments"
+ :key="environment.name"
+ class="gl-mb-3 gl-border-gray-100 gl-border-1 gl-border-b-solid"
+ :environment="environment.latest"
+ />
<gl-pagination
align="center"
:total-items="totalItems"
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index 7a9233048a9..162ad598c8c 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -2,6 +2,7 @@
import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import eventHub from '../event_hub';
+import stopEnvironmentMutation from '../graphql/mutations/stop_environment.mutation.graphql';
export default {
id: 'stop-environment-modal',
@@ -21,6 +22,11 @@ export default {
type: Object,
required: true,
},
+ graphql: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
computed: {
@@ -39,7 +45,14 @@ export default {
methods: {
onSubmit() {
- eventHub.$emit('stopEnvironment', this.environment);
+ if (this.graphql) {
+ this.$apollo.mutate({
+ mutation: stopEnvironmentMutation,
+ variables: { environment: this.environment },
+ });
+ } else {
+ eventHub.$emit('stopEnvironment', this.environment);
+ }
},
},
};
diff --git a/app/assets/javascripts/environments/graphql/mutations/action.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/action.mutation.graphql
new file mode 100644
index 00000000000..bc2c9b33367
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/action.mutation.graphql
@@ -0,0 +1,5 @@
+mutation action($action: LocalAction) {
+ action(action: $action) @client {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql
new file mode 100644
index 00000000000..2891f4c5101
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/mutations/set_environment_to_stop.mutation.graphql
@@ -0,0 +1,3 @@
+mutation SetEnvironmentToStop($environment: LocalEnvironmentInput) {
+ setEnvironmentToStop(environment: $environment) @client
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql b/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql
new file mode 100644
index 00000000000..128846145e8
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/environment_to_stop.query.graphql
@@ -0,0 +1,3 @@
+query environmentToStop {
+ environmentToStop @client
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql b/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql
new file mode 100644
index 00000000000..ad05e252e6f
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/is_environment_stopping.query.graphql
@@ -0,0 +1,3 @@
+query isEnvironmentStopping($environment: LocalEnvironment) {
+ isEnvironmentStopping(environment: $environment) @client
+}
diff --git a/app/assets/javascripts/environments/graphql/queries/is_last_deployment.query.graphql b/app/assets/javascripts/environments/graphql/queries/is_last_deployment.query.graphql
new file mode 100644
index 00000000000..5eda2f18567
--- /dev/null
+++ b/app/assets/javascripts/environments/graphql/queries/is_last_deployment.query.graphql
@@ -0,0 +1,3 @@
+query isLastDeployment($environment: LocalEnvironment) {
+ isLastDeployment(environment: $environment) @client
+}
diff --git a/app/assets/javascripts/environments/graphql/resolvers.js b/app/assets/javascripts/environments/graphql/resolvers.js
index 9ebbc0ad1f8..812fa0c81f0 100644
--- a/app/assets/javascripts/environments/graphql/resolvers.js
+++ b/app/assets/javascripts/environments/graphql/resolvers.js
@@ -8,6 +8,7 @@ import {
import pollIntervalQuery from './queries/poll_interval.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
+import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
import pageInfoQuery from './queries/page_info.query.graphql';
@@ -65,8 +66,7 @@ export const resolvers = (endpoint) => ({
}));
},
isLastDeployment(_, { environment }) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- return environment?.lastDeployment?.['last?'];
+ return environment?.lastDeployment?.isLast;
},
},
Mutation: {
@@ -108,6 +108,20 @@ export const resolvers = (endpoint) => ({
]);
});
},
+ setEnvironmentToStop(_, { environment }, { client }) {
+ client.writeQuery({
+ query: environmentToStopQuery,
+ data: { environmentToStop: environment },
+ });
+ },
+ action(_, { action: { playPath } }) {
+ return axios
+ .post(playPath)
+ .then(() => buildErrors())
+ .catch(() =>
+ buildErrors([s__('Environments|An error occurred while making the request.')]),
+ );
+ },
setEnvironmentToDelete(_, { environment }, { client }) {
client.writeQuery({
query: environmentToDeleteQuery,
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index 4a3abb0e89f..c02f6b2838a 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -68,7 +68,9 @@ extend type Query {
environmentToDelete: LocalEnvironment
pageInfo: LocalPageInfo
environmentToRollback: LocalEnvironment
- isLastDeployment: Boolean
+ environmentToStop: LocalEnvironment
+ isEnvironmentStopping(environment: LocalEnvironmentInput): Boolean
+ isLastDeployment(environment: LocalEnvironmentInput): Boolean
}
extend type Mutation {
@@ -78,4 +80,6 @@ extend type Mutation {
cancelAutoStop(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToDelete(environment: LocalEnvironmentInput): LocalErrors
setEnvironmentToRollback(environment: LocalEnvironmentInput): LocalErrors
+ setEnvironmentToStop(environment: LocalEnvironmentInput): LocalErrors
+ action(environment: LocalEnvironmentInput): LocalErrors
}
diff --git a/app/assets/javascripts/experimental_flags.js b/app/assets/javascripts/experimental_flags.js
deleted file mode 100644
index 1d60847147b..00000000000
--- a/app/assets/javascripts/experimental_flags.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import $ from 'jquery';
-import Cookies from 'js-cookie';
-
-export default () => {
- $('.js-experiment-feature-toggle').on('change', (e) => {
- const el = e.target;
-
- Cookies.set(el.name, el.value, {
- expires: 365 * 10,
- });
-
- document.body.scrollTop = 0;
- window.location.reload();
- });
-};
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index f0ef55f73eb..d9c2e55cffe 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,5 +1,8 @@
import * as Sentry from '@sentry/browser';
import { escape } from 'lodash';
+import Vue from 'vue';
+import { GlAlert } from '@gitlab/ui';
+import { __ } from '~/locale';
import { spriteIcon } from './lib/utils/common_utils';
const FLASH_TYPES = {
@@ -9,6 +12,12 @@ const FLASH_TYPES = {
WARNING: 'warning',
};
+const VARIANT_SUCCESS = 'success';
+const VARIANT_WARNING = 'warning';
+const VARIANT_DANGER = 'danger';
+const VARIANT_INFO = 'info';
+const VARIANT_TIP = 'tip';
+
const FLASH_CLOSED_EVENT = 'flashClosed';
const getCloseEl = (flashEl) => {
@@ -68,6 +77,126 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
getCloseEl(flashEl)?.addEventListener('click', () => hideFlash(flashEl, fadeTransition));
};
+/**
+ * Render an alert at the top of the page, or, optionally an
+ * arbitrary existing container.
+ *
+ * This alert is always dismissible.
+ *
+ * Usage:
+ *
+ * 1. Render a new alert
+ *
+ * import { createAlert, ALERT_VARIANTS } from '~/flash';
+ *
+ * createAlert({ message: 'My error message' });
+ * createAlert({ message: 'My warning message', variant: ALERT_VARIANTS.WARNING });
+ *
+ * 2. Dismiss this alert programmatically
+ *
+ * const alert = createAlert({ message: 'Message' });
+ *
+ * // ...
+ *
+ * alert.dismiss();
+ *
+ * 3. Respond to the alert being dismissed
+ *
+ * createAlert({ message: 'Message', onDismiss: () => { ... }});
+ *
+ * @param {Object} options Options to control the flash message
+ * @param {String} options.message Alert message text
+ * @param {String?} options.variant Which GlAlert variant to use, should be VARIANT_SUCCESS, VARIANT_WARNING, VARIANT_DANGER, VARIANT_INFO or VARIANT_TIP. Defaults to VARIANT_DANGER.
+ * @param {Object?} options.parent Reference to parent element under which alert needs to appear. Defaults to `document`.
+ * @param {Function?} options.onDismiss Handler to call when this alert is dismissed.
+ * @param {Object?} options.containerSelector Selector for the container of the alert
+ * @param {Object?} options.primaryButton Object describing primary button of alert
+ * @param {String?} link Href of primary button
+ * @param {String?} text Text of primary button
+ * @param {Function?} clickHandler Handler to call when primary button is clicked on. The click event is sent as an argument.
+ * @param {Object?} options.secondaryButton Object describing secondary button of alert
+ * @param {String?} link Href of secondary button
+ * @param {String?} text Text of secondary button
+ * @param {Function?} clickHandler Handler to call when secondary button is clicked on. The click event is sent as an argument.
+ * @param {Boolean?} options.captureError Whether to send error to Sentry
+ * @param {Object} options.error Error to be captured in Sentry
+ * @returns
+ */
+const createAlert = function createAlert({
+ message,
+ variant = VARIANT_DANGER,
+ parent = document,
+ containerSelector = '.flash-container',
+ primaryButton = null,
+ secondaryButton = null,
+ onDismiss = null,
+ captureError = false,
+ error = null,
+}) {
+ if (captureError && error) Sentry.captureException(error);
+
+ const alertContainer = parent.querySelector(containerSelector);
+ if (!alertContainer) return null;
+
+ const el = document.createElement('div');
+ alertContainer.appendChild(el);
+
+ return new Vue({
+ el,
+ components: {
+ GlAlert,
+ },
+ methods: {
+ /**
+ * Public method to dismiss this alert and removes
+ * this Vue instance.
+ */
+ dismiss() {
+ if (onDismiss) {
+ onDismiss();
+ }
+ this.$destroy();
+ this.$el.parentNode.removeChild(this.$el);
+ },
+ },
+ render(h) {
+ const on = {};
+
+ on.dismiss = () => {
+ this.dismiss();
+ };
+
+ if (primaryButton?.clickHandler) {
+ on.primaryAction = (e) => {
+ primaryButton.clickHandler(e);
+ };
+ }
+ if (secondaryButton?.clickHandler) {
+ on.secondaryAction = (e) => {
+ secondaryButton.clickHandler(e);
+ };
+ }
+
+ return h(
+ GlAlert,
+ {
+ props: {
+ dismissible: true,
+ dismissLabel: __('Dismiss'),
+ variant,
+ primaryButtonLink: primaryButton?.link,
+ primaryButtonText: primaryButton?.text,
+ secondaryButtonLink: secondaryButton?.link,
+ secondaryButtonText: secondaryButton?.text,
+ },
+ on,
+ },
+ message,
+ );
+ },
+ });
+};
+
/*
* Flash banner supports different types of Flash configurations
* along with ability to provide actionConfig which can be used to show
@@ -82,8 +211,8 @@ const addDismissFlashClickListener = (flashEl, fadeTransition) => {
* @param {String} title Title of action
* @param {Function} clickHandler Method to call when action is clicked on
* @param {Boolean} options.fadeTransition Boolean to determine whether to fade the alert out
- * @param {Boolean} options.captureError Boolean to determine whether to send error to sentry
- * @param {Object} options.error Error to be captured in sentry
+ * @param {Boolean} options.captureError Boolean to determine whether to send error to Sentry
+ * @param {Object} options.error Error to be captured in Sentry
*/
const createFlash = function createFlash({
message,
@@ -134,4 +263,10 @@ export {
addDismissFlashClickListener,
FLASH_TYPES,
FLASH_CLOSED_EVENT,
+ createAlert,
+ VARIANT_SUCCESS,
+ VARIANT_WARNING,
+ VARIANT_DANGER,
+ VARIANT_INFO,
+ VARIANT_TIP,
};
diff --git a/app/assets/javascripts/gitlab_version_check.js b/app/assets/javascripts/gitlab_version_check.js
new file mode 100644
index 00000000000..2892aded7c5
--- /dev/null
+++ b/app/assets/javascripts/gitlab_version_check.js
@@ -0,0 +1,20 @@
+import Vue from 'vue';
+import GitlabVersionCheck from '~/vue_shared/components/gitlab_version_check.vue';
+
+const mountGitlabVersionCheck = (el) => {
+ const { size } = el.dataset;
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(GitlabVersionCheck, {
+ props: {
+ size,
+ },
+ });
+ },
+ });
+};
+
+export default () =>
+ [...document.querySelectorAll('.js-gitlab-version-check')].map(mountGitlabVersionCheck);
diff --git a/app/assets/javascripts/google_cloud/components/deployments_service_table.vue b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue
new file mode 100644
index 00000000000..7d27d7cf6b2
--- /dev/null
+++ b/app/assets/javascripts/google_cloud/components/deployments_service_table.vue
@@ -0,0 +1,61 @@
+<script>
+import { GlButton, GlTable } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+const i18n = {
+ cloudRun: __('Cloud Run'),
+ cloudRunDescription: __('Deploy container based web apps on Google managed clusters'),
+ cloudStorage: __('Cloud Storage'),
+ cloudStorageDescription: __('Deploy static assets and resources to Google managed CDN'),
+ deployments: __('Deployments'),
+ deploymentsDescription: __(
+ 'Configure pipelines to deploy web apps, backend services, APIs and static resources to Google Cloud',
+ ),
+ configureViaMergeRequest: __('Configure via Merge Request'),
+ service: __('Service'),
+ description: __('Description'),
+};
+
+export default {
+ components: { GlButton, GlTable },
+ props: {
+ cloudRunUrl: {
+ type: String,
+ required: true,
+ },
+ cloudStorageUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ fields: [
+ { key: 'title', label: i18n.service },
+ { key: 'description', label: i18n.description },
+ { key: 'action', label: '' },
+ ],
+ items: [
+ {
+ title: i18n.cloudRun,
+ description: i18n.cloudRunDescription,
+ action: { title: i18n.configureViaMergeRequest, disabled: true },
+ },
+ {
+ title: i18n.cloudStorage,
+ description: i18n.cloudStorageDescription,
+ action: { title: i18n.configureViaMergeRequest, disabled: true },
+ },
+ ],
+ i18n,
+};
+</script>
+<template>
+ <div class="gl-mx-3">
+ <h2 class="gl-font-size-h2">{{ $options.i18n.deployments }}</h2>
+ <p>{{ $options.i18n.deploymentsDescription }}</p>
+ <gl-table :fields="$options.fields" :items="$options.items">
+ <template #cell(action)="{ value }">
+ <gl-button :disabled="value.disabled">{{ value.title }}</gl-button>
+ </template>
+ </gl-table>
+ </div>
+</template>
diff --git a/app/assets/javascripts/google_cloud/components/home.vue b/app/assets/javascripts/google_cloud/components/home.vue
index 05f39de66ee..8ef110dcf22 100644
--- a/app/assets/javascripts/google_cloud/components/home.vue
+++ b/app/assets/javascripts/google_cloud/components/home.vue
@@ -1,11 +1,13 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
+import DeploymentsServiceTable from './deployments_service_table.vue';
import ServiceAccountsList from './service_accounts_list.vue';
export default {
components: {
GlTabs,
GlTab,
+ DeploymentsServiceTable,
ServiceAccountsList,
},
props: {
@@ -21,6 +23,14 @@ export default {
type: String,
required: true,
},
+ deploymentsCloudRunUrl: {
+ type: String,
+ required: true,
+ },
+ deploymentsCloudStorageUrl: {
+ type: String,
+ required: true,
+ },
},
};
</script>
@@ -35,7 +45,12 @@ export default {
:empty-illustration-url="emptyIllustrationUrl"
/>
</gl-tab>
- <gl-tab :title="__('Deployments')" disabled />
+ <gl-tab :title="__('Deployments')">
+ <deployments-service-table
+ :cloud-run-url="deploymentsCloudRunUrl"
+ :cloud-storage-url="deploymentsCloudStorageUrl"
+ />
+ </gl-tab>
<gl-tab :title="__('Services')" disabled />
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/google_tag_manager/index.js b/app/assets/javascripts/google_tag_manager/index.js
new file mode 100644
index 00000000000..ab80e15c2ec
--- /dev/null
+++ b/app/assets/javascripts/google_tag_manager/index.js
@@ -0,0 +1,122 @@
+import { logError } from '~/lib/logger';
+
+const isSupported = () => Boolean(window.dataLayer) && gon.features?.gitlabGtmDatalayer;
+
+const pushEvent = (event, args = {}) => {
+ if (!window.dataLayer) {
+ return;
+ }
+
+ try {
+ window.dataLayer.push({
+ event,
+ ...args,
+ });
+ } catch (e) {
+ logError('Unexpected error while pushing to dataLayer', e);
+ }
+};
+
+const pushAccountSubmit = (accountType, accountMethod) =>
+ pushEvent('accountSubmit', { accountType, accountMethod });
+
+const trackFormSubmission = (accountType) => {
+ const form = document.getElementById('new_new_user');
+ form.addEventListener('submit', () => {
+ pushAccountSubmit(accountType, 'form');
+ });
+};
+
+const trackOmniAuthSubmission = (accountType) => {
+ const links = document.querySelectorAll('.js-oauth-login');
+ links.forEach((link) => {
+ const { provider } = link.dataset;
+ link.addEventListener('click', () => {
+ pushAccountSubmit(accountType, provider);
+ });
+ });
+};
+
+export const trackFreeTrialAccountSubmissions = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ trackFormSubmission('freeThirtyDayTrial');
+ trackOmniAuthSubmission('freeThirtyDayTrial');
+};
+
+export const trackNewRegistrations = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ trackFormSubmission('standardSignUp');
+ trackOmniAuthSubmission('standardSignUp');
+};
+
+export const trackSaasTrialSubmit = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ pushEvent('saasTrialSubmit');
+};
+
+export const trackSaasTrialSkip = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const skipLink = document.querySelector('.js-skip-trial');
+ skipLink.addEventListener('click', () => {
+ pushEvent('saasTrialSkip');
+ });
+};
+
+export const trackSaasTrialGroup = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const form = document.querySelector('.js-saas-trial-group');
+ form.addEventListener('submit', () => {
+ pushEvent('saasTrialGroup');
+ });
+};
+
+export const trackSaasTrialProject = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const form = document.getElementById('new_project');
+ form.addEventListener('submit', () => {
+ pushEvent('saasTrialProject');
+ });
+};
+
+export const trackSaasTrialProjectImport = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const importButtons = document.querySelectorAll('.js-import-project-btn');
+ importButtons.forEach((button) => {
+ button.addEventListener('click', () => {
+ const { platform } = button.dataset;
+ pushEvent('saasTrialProjectImport', { saasProjectImport: platform });
+ });
+ });
+};
+
+export const trackSaasTrialGetStarted = () => {
+ if (!isSupported()) {
+ return;
+ }
+
+ const getStartedButton = document.querySelector('.js-get-started-btn');
+ getStartedButton.addEventListener('click', () => {
+ pushEvent('saasTrialGetStarted');
+ });
+};
diff --git a/app/assets/javascripts/graphql_shared/fragment_types/vulnerability_location_types.js b/app/assets/javascripts/graphql_shared/fragment_types/vulnerability_location_types.js
new file mode 100644
index 00000000000..30888e20a46
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragment_types/vulnerability_location_types.js
@@ -0,0 +1,17 @@
+export const vulnerabilityLocationTypes = {
+ __schema: {
+ types: [
+ {
+ kind: 'UNION',
+ name: 'VulnerabilityLocation',
+ possibleTypes: [
+ { name: 'VulnerabilityLocationContainerScanning' },
+ { name: 'VulnerabilityLocationDast' },
+ { name: 'VulnerabilityLocationDependencyScanning' },
+ { name: 'VulnerabilityLocationSast' },
+ { name: 'VulnerabilityLocationSecretDetection' },
+ ],
+ },
+ ],
+ },
+};
diff --git a/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
new file mode 100644
index 00000000000..85a28fe1f71
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/fragments/issue.fragment.graphql
@@ -0,0 +1,37 @@
+#import "~/graphql_shared/fragments/milestone.fragment.graphql"
+#import "~/graphql_shared/fragments/user.fragment.graphql"
+
+fragment IssueNode on Issue {
+ id
+ iid
+ title
+ referencePath: reference(full: true)
+ dueDate
+ timeEstimate
+ totalTimeSpent
+ humanTimeEstimate
+ humanTotalTimeSpent
+ emailsDisabled
+ confidential
+ hidden
+ webUrl
+ relativePosition
+ type
+ severity
+ milestone {
+ ...MilestoneFragment
+ }
+ assignees {
+ nodes {
+ ...User
+ }
+ }
+ labels {
+ nodes {
+ id
+ title
+ color
+ description
+ }
+ }
+}
diff --git a/app/assets/javascripts/group.js b/app/assets/javascripts/group.js
index f255f8a084c..b6a6720e7a1 100644
--- a/app/assets/javascripts/group.js
+++ b/app/assets/javascripts/group.js
@@ -13,11 +13,8 @@ export default class Group {
this.updateGroupPathSlugHandler = this.updateGroupPathSlug.bind(this);
this.groupNames.forEach((groupName) => {
- if (groupName.value === '') {
- groupName.addEventListener('keyup', this.updateHandler);
-
- groupName.addEventListener('keyup', this.updateGroupPathSlugHandler);
- }
+ groupName.addEventListener('keyup', this.updateHandler);
+ groupName.addEventListener('keyup', this.updateGroupPathSlugHandler);
});
this.groupPaths.forEach((groupPath) => {
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index 46e9d2bec99..c24eeed9f03 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -83,7 +83,7 @@ export default {
<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" />
+ <time-ago-tooltip :time="item.lastActivityAt" tooltip-placement="bottom" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/groups_list.js b/app/assets/javascripts/groups/groups_list.js
index 56a8cbf6d03..866dd7a61ff 100644
--- a/app/assets/javascripts/groups_list.js
+++ b/app/assets/javascripts/groups/groups_list.js
@@ -1,4 +1,4 @@
-import FilterableList from './filterable_list';
+import FilterableList from '~/filterable_list';
/**
* Makes search request for groups when user types a value in the search input.
diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/groups/landing.js
index bfb4d9ce67b..bfb4d9ce67b 100644
--- a/app/assets/javascripts/landing.js
+++ b/app/assets/javascripts/groups/landing.js
diff --git a/app/assets/javascripts/groups/store/groups_store.js b/app/assets/javascripts/groups/store/groups_store.js
index 93fbd8be47d..d3600bd223a 100644
--- a/app/assets/javascripts/groups/store/groups_store.js
+++ b/app/assets/javascripts/groups/store/groups_store.js
@@ -98,6 +98,9 @@ export default class GroupsStore {
updatedAt: rawGroupItem.updated_at,
pendingRemoval: rawGroupItem.marked_for_deletion,
microdata: this.showSchemaMarkup ? getGroupItemMicrodata(rawGroupItem) : {},
+ lastActivityAt: rawGroupItem.last_activity_at
+ ? rawGroupItem.last_activity_at
+ : rawGroupItem.updated_at,
};
if (!isEmpty(rawGroupItem.compliance_management_framework)) {
diff --git a/app/assets/javascripts/transfer_edit.js b/app/assets/javascripts/groups/transfer_edit.js
index bb15e11fd4c..bb15e11fd4c 100644
--- a/app/assets/javascripts/transfer_edit.js
+++ b/app/assets/javascripts/groups/transfer_edit.js
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
index edc6573a489..36fc48a2ba8 100644
--- a/app/assets/javascripts/header_search/components/app.vue
+++ b/app/assets/javascripts/header_search/components/app.vue
@@ -19,8 +19,7 @@ import HeaderSearchScopedItems from './header_search_scoped_items.vue';
export default {
name: 'HeaderSearchApp',
i18n: {
- searchPlaceholder: s__('GlobalSearch|Search or jump to...'),
- searchAria: s__('GlobalSearch|Search GitLab'),
+ searchGitlab: s__('GlobalSearch|Search GitLab'),
searchInputDescribeByNoDropdown: s__(
'GlobalSearch|Type and press the enter key to submit search.',
),
@@ -136,7 +135,7 @@ export default {
<form
v-outside="closeDropdown"
role="search"
- :aria-label="$options.i18n.searchAria"
+ :aria-label="$options.i18n.searchGitlab"
class="header-search gl-relative"
>
<gl-search-box-by-type
@@ -145,7 +144,7 @@ export default {
role="searchbox"
class="gl-z-index-1"
autocomplete="off"
- :placeholder="$options.i18n.searchPlaceholder"
+ :placeholder="$options.i18n.searchGitlab"
:aria-activedescendant="currentFocusedId"
:aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
@focus="openDropdown"
diff --git a/app/assets/javascripts/helpers/event_hub_factory.js b/app/assets/javascripts/helpers/event_hub_factory.js
index 62af67d3ef3..ddfae7e9de3 100644
--- a/app/assets/javascripts/helpers/event_hub_factory.js
+++ b/app/assets/javascripts/helpers/event_hub_factory.js
@@ -1,16 +1,12 @@
/**
* An event hub with a Vue instance like API
*
- * NOTE: There's an [issue open][4] to eventually remove this when some
- * coupling in our codebase has been fixed.
- *
* NOTE: This is a derivative work from [mitt][1] v1.2.0 which is licensed by
* [MIT License][2] © [Jason Miller][3]
*
* [1]: https://github.com/developit/mitt
* [2]: https://opensource.org/licenses/MIT
* [3]: https://jasonformat.com/
- * [4]: https://gitlab.com/gitlab-org/gitlab/-/issues/223864
*/
class EventHub {
constructor() {
@@ -91,9 +87,6 @@ class EventHub {
* - $once
* - $emit
*
- * Please note, this was once implemented with `mitt`, but since then has been reverted
- * because of some API issues. https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35074
- *
* We'd like to shy away from using a full fledged Vue instance from this in the future.
*/
export default () => {
diff --git a/app/assets/javascripts/ide/components/jobs/stage.vue b/app/assets/javascripts/ide/components/jobs/stage.vue
index 938385f0b81..796ca1349c5 100644
--- a/app/assets/javascripts/ide/components/jobs/stage.vue
+++ b/app/assets/javascripts/ide/components/jobs/stage.vue
@@ -1,5 +1,5 @@
<script>
-import { GlLoadingIcon, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import { GlLoadingIcon, GlIcon, GlTooltipDirective, GlBadge } from '@gitlab/ui';
import CiIcon from '../../../vue_shared/components/ci_icon.vue';
import Item from './item.vue';
@@ -9,6 +9,7 @@ export default {
},
components: {
GlIcon,
+ GlBadge,
CiIcon,
Item,
GlLoadingIcon,
@@ -74,7 +75,7 @@ export default {
{{ stage.name }}
</strong>
<div v-if="!stage.isLoading || stage.jobs.length" class="gl-mr-3 gl-ml-2">
- <span class="badge badge-pill"> {{ jobsCount }} </span>
+ <gl-badge>{{ jobsCount }}</gl-badge>
</div>
<gl-icon :name="collapseIcon" class="ide-stage-collapse-icon" />
</div>
diff --git a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
index 9cf8d5a360e..51872993f16 100644
--- a/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
+++ b/app/assets/javascripts/ide/stores/modules/pipelines/actions.js
@@ -53,9 +53,15 @@ export const receiveLatestPipelineSuccess = ({ rootGetters, commit }, { pipeline
commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, lastCommitPipeline);
};
-export const fetchLatestPipeline = ({ dispatch, rootGetters }) => {
+export const fetchLatestPipeline = ({ commit, dispatch, rootGetters }) => {
if (eTagPoll) return;
+ if (!rootGetters.lastCommit) {
+ commit(types.RECEIVE_LASTEST_PIPELINE_SUCCESS, null);
+ dispatch('stopPipelinePolling');
+ return;
+ }
+
dispatch('requestLatestPipeline');
eTagPoll = new Poll({
diff --git a/app/assets/javascripts/init_confirm_danger.js b/app/assets/javascripts/init_confirm_danger.js
index a8833a17467..98bfa48740c 100644
--- a/app/assets/javascripts/init_confirm_danger.js
+++ b/app/assets/javascripts/init_confirm_danger.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { pickBy } from 'lodash';
import { parseBoolean } from './lib/utils/common_utils';
import ConfirmDanger from './vue_shared/components/confirm_danger/confirm_danger.vue';
@@ -12,21 +13,32 @@ export default () => {
buttonText,
buttonClass = '',
buttonTestid = null,
+ buttonVariant = null,
confirmDangerMessage,
+ confirmButtonText = null,
disabled = false,
+ additionalInformation,
+ htmlConfirmationMessage,
} = el.dataset;
return new Vue({
el,
- provide: {
- confirmDangerMessage,
- },
+ provide: pickBy(
+ {
+ htmlConfirmationMessage,
+ confirmDangerMessage,
+ additionalInformation,
+ confirmButtonText,
+ },
+ (v) => Boolean(v),
+ ),
render: (createElement) =>
createElement(ConfirmDanger, {
props: {
phrase,
buttonText,
buttonClass,
+ buttonVariant,
buttonTestid,
disabled: parseBoolean(disabled),
},
diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js
index 84656bd41bb..b90658fb13c 100644
--- a/app/assets/javascripts/integrations/constants.js
+++ b/app/assets/javascripts/integrations/constants.js
@@ -23,3 +23,8 @@ export const I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE = s__(
);
export const I18N_DEFAULT_ERROR_MESSAGE = __('Something went wrong on our end.');
export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection successful.');
+
+export const settingsTabTitle = __('Settings');
+export const overridesTabTitle = s__('Integrations|Projects using custom settings');
+
+export const INTEGRATION_FORM_SELECTOR = '.js-integration-settings-form';
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index 258cd1bf365..4b0579a5beb 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -153,7 +153,7 @@ export default {
:invalid-feedback="__('This field is required.')"
:state="valid"
>
- <template #description>
+ <template v-if="!isCheckbox" #description>
<span v-safe-html:[$options.helpHtmlConfig]="help"></span>
</template>
@@ -161,6 +161,9 @@ export default {
<input :name="fieldName" type="hidden" :value="model || false" />
<gl-form-checkbox :id="fieldId" v-model="model" :disabled="isInheriting">
{{ checkboxLabel || humanizedTitle }}
+ <template #help>
+ <span v-safe-html:[$options.helpHtmlConfig]="help"></span>
+ </template>
</gl-form-checkbox>
</template>
<template v-else-if="isSelect">
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index e570a468944..c3cc35adfa5 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -1,5 +1,6 @@
<script>
-import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui';
+import { GlButton, GlModalDirective, GlSafeHtmlDirective as SafeHtml, GlForm } from '@gitlab/ui';
+import axios from 'axios';
import * as Sentry from '@sentry/browser';
import { mapState, mapActions, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -8,8 +9,11 @@ import {
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
+ INTEGRATION_FORM_SELECTOR,
integrationLevels,
} from '~/integrations/constants';
+import { refreshCurrentPage } from '~/lib/utils/url_utility';
+import csrf from '~/lib/utils/csrf';
import eventHub from '../event_hub';
import { testIntegrationSettings } from '../api';
import ActiveCheckbox from './active_checkbox.vue';
@@ -33,6 +37,7 @@ export default {
ConfirmationModal,
ResetConfirmationModal,
GlButton,
+ GlForm,
},
directives: {
GlModal: GlModalDirective,
@@ -40,10 +45,6 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
- formSelector: {
- type: String,
- required: true,
- },
helpHtml: {
type: String,
required: false,
@@ -55,11 +56,12 @@ export default {
integrationActive: false,
isTesting: false,
isSaving: false,
+ isResetting: false,
};
},
computed: {
...mapGetters(['currentKey', 'propsSource']),
- ...mapState(['defaultState', 'customState', 'override', 'isResetting']),
+ ...mapState(['defaultState', 'customState', 'override']),
isEditable() {
return this.propsSource.editable;
},
@@ -81,10 +83,28 @@ export default {
disableButtons() {
return Boolean(this.isSaving || this.isResetting || this.isTesting);
},
+ useVueForm() {
+ return this.glFeatures?.vueIntegrationForm;
+ },
+ formContainerProps() {
+ return this.useVueForm
+ ? {
+ ref: 'integrationForm',
+ method: 'post',
+ class: 'gl-mb-3 gl-show-field-errors integration-settings-form',
+ action: this.propsSource.formPath,
+ novalidate: !this.integrationActive,
+ }
+ : {};
+ },
+ formContainer() {
+ return this.useVueForm ? GlForm : 'div';
+ },
},
mounted() {
- // this form element is defined in Haml
- this.form = document.querySelector(this.formSelector);
+ this.form = this.useVueForm
+ ? this.$refs.integrationForm.$el
+ : document.querySelector(INTEGRATION_FORM_SELECTOR);
},
methods: {
...mapActions(['setOverride', 'fetchResetIntegration', 'requestJiraIssueTypes']),
@@ -126,7 +146,20 @@ export default {
});
},
onResetClick() {
- this.fetchResetIntegration();
+ this.isResetting = true;
+
+ return axios
+ .post(this.propsSource.resetPath)
+ .then(() => {
+ refreshCurrentPage();
+ })
+ .catch((error) => {
+ this.$toast.show(I18N_DEFAULT_ERROR_MESSAGE);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.isResetting = false;
+ });
},
onRequestJiraIssueTypes() {
this.requestJiraIssueTypes(this.getFormData());
@@ -136,7 +169,7 @@ export default {
},
onToggleIntegrationState(integrationActive) {
this.integrationActive = integrationActive;
- if (!this.form) {
+ if (!this.form || this.useVueForm) {
return;
}
@@ -153,11 +186,23 @@ export default {
ADD_TAGS: ['use'], // to support icon SVGs
FORBID_ATTR: [], // This is trusted input so we can override the default config to allow data-* attributes
},
+ csrf,
};
</script>
<template>
- <div class="gl-mb-3">
+ <component :is="formContainer" v-bind="formContainerProps">
+ <template v-if="useVueForm">
+ <input type="hidden" name="_method" value="put" />
+ <input type="hidden" name="authenticity_token" :value="$options.csrf.token" />
+ <input
+ type="hidden"
+ name="redirect_to"
+ :value="propsSource.redirectTo"
+ data-testid="redirect-to-field"
+ />
+ </template>
+
<override-dropdown
v-if="defaultState !== null"
:inherit-from-id="defaultState.id"
@@ -200,63 +245,71 @@ export default {
v-bind="propsSource.jiraIssuesProps"
@request-jira-issue-types="onRequestJiraIssueTypes"
/>
- <div v-if="isEditable" class="footer-block row-content-block">
- <template v-if="isInstanceOrGroupLevel">
+
+ <div
+ v-if="isEditable"
+ class="footer-block row-content-block gl-display-flex gl-justify-content-space-between"
+ >
+ <div>
+ <template v-if="isInstanceOrGroupLevel">
+ <gl-button
+ v-gl-modal.confirmSaveIntegration
+ category="primary"
+ variant="confirm"
+ :loading="isSaving"
+ :disabled="disableButtons"
+ data-testid="save-button-instance-group"
+ data-qa-selector="save_changes_button"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ <confirmation-modal @submit="onSaveClick" />
+ </template>
<gl-button
- v-gl-modal.confirmSaveIntegration
+ v-else
category="primary"
variant="confirm"
+ type="submit"
:loading="isSaving"
:disabled="disableButtons"
+ data-testid="save-button"
data-qa-selector="save_changes_button"
+ @click.prevent="onSaveClick"
>
{{ __('Save changes') }}
</gl-button>
- <confirmation-modal @submit="onSaveClick" />
- </template>
- <gl-button
- v-else
- category="primary"
- variant="confirm"
- type="submit"
- :loading="isSaving"
- :disabled="disableButtons"
- data-testid="save-button"
- data-qa-selector="save_changes_button"
- @click.prevent="onSaveClick"
- >
- {{ __('Save changes') }}
- </gl-button>
- <gl-button
- v-if="showTestButton"
- category="secondary"
- variant="confirm"
- :loading="isTesting"
- :disabled="disableButtons"
- data-testid="test-button"
- @click.prevent="onTestClick"
- >
- {{ __('Test settings') }}
- </gl-button>
+ <gl-button
+ v-if="showTestButton"
+ category="secondary"
+ variant="confirm"
+ :loading="isTesting"
+ :disabled="disableButtons"
+ data-testid="test-button"
+ @click.prevent="onTestClick"
+ >
+ {{ __('Test settings') }}
+ </gl-button>
+
+ <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
+ </div>
<template v-if="showResetButton">
<gl-button
v-gl-modal.confirmResetIntegration
- category="secondary"
- variant="confirm"
+ category="tertiary"
+ variant="danger"
:loading="isResetting"
:disabled="disableButtons"
data-testid="reset-button"
>
{{ __('Reset') }}
</gl-button>
+
<reset-confirmation-modal @reset="onResetClick" />
</template>
-
- <gl-button :href="propsSource.cancelPath">{{ __('Cancel') }}</gl-button>
</div>
</div>
</div>
- </div>
+ </component>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
index 5a445235219..403bad3db11 100644
--- a/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
+++ b/app/assets/javascripts/integrations/edit/components/reset_confirmation_modal.vue
@@ -11,7 +11,7 @@ export default {
primaryProps() {
return {
text: __('Reset'),
- attributes: [{ variant: 'warning' }, { category: 'primary' }],
+ attributes: [{ variant: 'danger' }, { category: 'primary' }],
};
},
cancelProps() {
diff --git a/app/assets/javascripts/integrations/edit/index.js b/app/assets/javascripts/integrations/edit/index.js
index 9c9e3edbeb8..fbda8c1e3d0 100644
--- a/app/assets/javascripts/integrations/edit/index.js
+++ b/app/assets/javascripts/integrations/edit/index.js
@@ -28,9 +28,11 @@ function parseDatasetToProps(data) {
cancelPath,
testPath,
resetPath,
+ formPath,
vulnerabilitiesIssuetype,
jiraIssueTransitionAutomatic,
jiraIssueTransitionId,
+ redirectTo,
...booleanAttributes
} = data;
const {
@@ -57,6 +59,7 @@ function parseDatasetToProps(data) {
canTest,
testPath,
resetPath,
+ formPath,
triggerFieldsProps: {
initialTriggerCommit: commitEvents,
initialTriggerMergeRequest: mergeRequestEvents,
@@ -82,10 +85,11 @@ function parseDatasetToProps(data) {
inheritFromId: parseInt(inheritFromId, 10),
integrationLevel,
id: parseInt(id, 10),
+ redirectTo,
};
}
-export default function initIntegrationSettingsForm(formSelector) {
+export default function initIntegrationSettingsForm() {
const customSettingsEl = document.querySelector('.js-vue-integration-settings');
const defaultSettingsEl = document.querySelector('.js-vue-default-integration-settings');
@@ -115,7 +119,6 @@ export default function initIntegrationSettingsForm(formSelector) {
return createElement(IntegrationForm, {
props: {
helpHtml,
- formSelector,
},
});
},
diff --git a/app/assets/javascripts/integrations/edit/store/actions.js b/app/assets/javascripts/integrations/edit/store/actions.js
index 97565a3a69c..1398b710d1d 100644
--- a/app/assets/javascripts/integrations/edit/store/actions.js
+++ b/app/assets/javascripts/integrations/edit/store/actions.js
@@ -1,5 +1,3 @@
-import axios from 'axios';
-import { refreshCurrentPage } from '~/lib/utils/url_utility';
import {
VALIDATE_INTEGRATION_FORM_EVENT,
I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE,
@@ -10,27 +8,6 @@ import eventHub from '../event_hub';
import * as types from './mutation_types';
export const setOverride = ({ commit }, override) => commit(types.SET_OVERRIDE, override);
-export const setIsResetting = ({ commit }, isResetting) =>
- commit(types.SET_IS_RESETTING, isResetting);
-
-export const requestResetIntegration = ({ commit }) => {
- commit(types.REQUEST_RESET_INTEGRATION);
-};
-export const receiveResetIntegrationSuccess = () => {
- refreshCurrentPage();
-};
-export const receiveResetIntegrationError = ({ commit }) => {
- commit(types.RECEIVE_RESET_INTEGRATION_ERROR);
-};
-
-export const fetchResetIntegration = ({ dispatch, getters }) => {
- dispatch('requestResetIntegration');
-
- return axios
- .post(getters.propsSource.resetPath, { params: { format: 'json' } })
- .then(() => dispatch('receiveResetIntegrationSuccess'))
- .catch(() => dispatch('receiveResetIntegrationError'));
-};
export const requestJiraIssueTypes = ({ commit, dispatch, getters }, formData) => {
commit(types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, '');
diff --git a/app/assets/javascripts/integrations/edit/store/mutations.js b/app/assets/javascripts/integrations/edit/store/mutations.js
index e7e312ce650..6ca644f8821 100644
--- a/app/assets/javascripts/integrations/edit/store/mutations.js
+++ b/app/assets/javascripts/integrations/edit/store/mutations.js
@@ -4,15 +4,6 @@ export default {
[types.SET_OVERRIDE](state, override) {
state.override = override;
},
- [types.SET_IS_RESETTING](state, isResetting) {
- state.isResetting = isResetting;
- },
- [types.REQUEST_RESET_INTEGRATION](state) {
- state.isResetting = true;
- },
- [types.RECEIVE_RESET_INTEGRATION_ERROR](state) {
- state.isResetting = false;
- },
[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes) {
state.jiraIssueTypes = jiraIssueTypes;
},
diff --git a/app/assets/javascripts/integrations/edit/store/state.js b/app/assets/javascripts/integrations/edit/store/state.js
index 3d40d1b90d5..088476b2b37 100644
--- a/app/assets/javascripts/integrations/edit/store/state.js
+++ b/app/assets/javascripts/integrations/edit/store/state.js
@@ -5,8 +5,6 @@ export default ({ defaultState = null, customState = {} } = {}) => {
override,
defaultState,
customState,
- isSaving: false,
- isResetting: false,
isLoadingJiraIssueTypes: false,
loadingJiraIssueTypesErrorMessage: '',
jiraIssueTypes: [],
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
index 3fc554c5371..f2d3e6489ee 100644
--- a/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
+++ b/app/assets/javascripts/integrations/overrides/components/integration_overrides.vue
@@ -11,6 +11,8 @@ import { __, s__ } from '~/locale';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
+import IntegrationTabs from './integration_tabs.vue';
+
const DEFAULT_PAGE = 1;
export default {
@@ -23,6 +25,7 @@ export default {
GlAlert,
ProjectAvatar,
UrlSync,
+ IntegrationTabs,
},
props: {
overridesPath: {
@@ -46,6 +49,9 @@ export default {
};
},
computed: {
+ overridesCount() {
+ return this.isLoading ? null : this.totalItems;
+ },
showPagination() {
return this.totalItems > this.$options.DEFAULT_PER_PAGE && this.overrides.length > 0;
},
@@ -100,6 +106,7 @@ export default {
<template>
<div>
+ <integration-tabs :project-overrides-count="overridesCount" />
<gl-alert v-if="errorMessage" variant="danger" :dismissible="false">
{{ errorMessage }}
</gl-alert>
diff --git a/app/assets/javascripts/integrations/overrides/components/integration_tabs.vue b/app/assets/javascripts/integrations/overrides/components/integration_tabs.vue
new file mode 100644
index 00000000000..3f67c987231
--- /dev/null
+++ b/app/assets/javascripts/integrations/overrides/components/integration_tabs.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlBadge, GlNavItem, GlTabs, GlTab } from '@gitlab/ui';
+import { settingsTabTitle, overridesTabTitle } from '~/integrations/constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlNavItem,
+ GlTabs,
+ GlTab,
+ },
+ inject: {
+ editPath: {
+ default: '',
+ },
+ },
+ props: {
+ projectOverridesCount: {
+ type: [Number, String],
+ required: false,
+ default: null,
+ },
+ },
+ i18n: {
+ settingsTabTitle,
+ overridesTabTitle,
+ },
+};
+</script>
+
+<template>
+ <gl-tabs>
+ <template #tabs-start>
+ <gl-nav-item role="presentation" link-classes="gl-tab-nav-item" :href="editPath">{{
+ $options.i18n.settingsTabTitle
+ }}</gl-nav-item>
+ </template>
+
+ <gl-tab active>
+ <template #title>
+ {{ $options.i18n.overridesTabTitle }}
+ <gl-badge
+ v-if="projectOverridesCount !== null"
+ variant="muted"
+ size="sm"
+ class="gl-tab-counter-badge"
+ >{{ projectOverridesCount }}</gl-badge
+ >
+ </template>
+ </gl-tab>
+ </gl-tabs>
+</template>
diff --git a/app/assets/javascripts/integrations/overrides/index.js b/app/assets/javascripts/integrations/overrides/index.js
index 0f03b23ba21..f289a2d3d1a 100644
--- a/app/assets/javascripts/integrations/overrides/index.js
+++ b/app/assets/javascripts/integrations/overrides/index.js
@@ -8,10 +8,13 @@ export default () => {
return null;
}
- const { overridesPath } = el.dataset;
+ const { editPath, overridesPath } = el.dataset;
return new Vue({
el,
+ provide: {
+ editPath,
+ },
render(createElement) {
return createElement(IntegrationOverrides, {
props: {
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/index.js b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
new file mode 100644
index 00000000000..dca606556d0
--- /dev/null
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/index.js
@@ -0,0 +1,28 @@
+import Vue from 'vue';
+import StatusSelect from './components/status_select.vue';
+import issuableBulkUpdateActions from './issuable_bulk_update_actions';
+import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
+
+export function initBulkUpdateSidebar(prefixId) {
+ const el = document.querySelector('.issues-bulk-update');
+
+ if (!el) {
+ return;
+ }
+
+ issuableBulkUpdateActions.init({ prefixId });
+ new IssuableBulkUpdateSidebar(); // eslint-disable-line no-new
+}
+
+export function initIssueStatusSelect() {
+ const el = document.querySelector('.js-issue-status');
+
+ if (!el) {
+ return null;
+ }
+
+ return new Vue({
+ el,
+ render: (createElement) => createElement(StatusSelect),
+ });
+}
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
deleted file mode 100644
index 43179a86d70..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/init_issue_status_select.js
+++ /dev/null
@@ -1,17 +0,0 @@
-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_sidebar/issuable_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
index 1eb3ffc9808..d46354e240a 100644
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
+++ b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_bulk_update_sidebar.js
@@ -1,12 +1,9 @@
/* eslint-disable class-methods-use-this, no-new */
import $ from 'jquery';
-import { property } from 'lodash';
-
-import issuableEventHub from '~/issues_list/eventhub';
+import issuableEventHub from '~/issues/list/eventhub';
import LabelsSelect from '~/labels/labels_select';
import MilestoneSelect from '~/milestones/milestone_select';
-import initIssueStatusSelect from './init_issue_status_select';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
import subscriptionSelect from './subscription_select';
@@ -17,8 +14,6 @@ const SIDEBAR_COLLAPSED_CLASS = 'right-sidebar-collapsed issuable-bulk-update-si
export default class IssuableBulkUpdateSidebar {
constructor() {
- this.vueIssuablesListFeature = property(['gon', 'features', 'vueIssuablesList'])(window);
-
this.initDomElements();
this.bindEvents();
this.initDropdowns();
@@ -57,7 +52,6 @@ export default class IssuableBulkUpdateSidebar {
initDropdowns() {
new LabelsSelect();
new MilestoneSelect();
- initIssueStatusSelect();
subscriptionSelect();
if (IS_EE) {
@@ -145,7 +139,7 @@ export default class IssuableBulkUpdateSidebar {
}
toggleCheckboxDisplay(show) {
- this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show || this.vueIssuablesListFeature);
+ this.$checkAllContainer.toggleClass(HIDDEN_CLASS, !show);
this.$issueChecks.toggleClass(HIDDEN_CLASS, !show);
}
diff --git a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js b/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
deleted file mode 100644
index 179c2b83c6c..00000000000
--- a/app/assets/javascripts/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import issuableBulkUpdateActions from './issuable_bulk_update_actions';
-import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
-
-export default {
- bulkUpdateSidebar: null,
-
- init(prefixId) {
- const bulkUpdateEl = document.querySelector('.issues-bulk-update');
- const alreadyInitialized = Boolean(this.bulkUpdateSidebar);
-
- if (bulkUpdateEl && !alreadyInitialized) {
- issuableBulkUpdateActions.init({ prefixId });
-
- this.bulkUpdateSidebar = new IssuableBulkUpdateSidebar();
- }
-
- return this.bulkUpdateSidebar;
- },
-};
diff --git a/app/assets/javascripts/issuable/components/csv_import_modal.vue b/app/assets/javascripts/issuable/components/csv_import_modal.vue
index b72abe14ee1..7e2cbf03801 100644
--- a/app/assets/javascripts/issuable/components/csv_import_modal.vue
+++ b/app/assets/javascripts/issuable/components/csv_import_modal.vue
@@ -60,7 +60,11 @@ export default {
<form ref="form" :action="importCsvIssuesPath" enctype="multipart/form-data" method="post">
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
<p>{{ $options.i18n.mainText }}</p>
- <gl-form-group :label="$options.i18n.uploadCsvFileText" label-for="file">
+ <gl-form-group
+ :label="$options.i18n.uploadCsvFileText"
+ class="gl-text-truncate"
+ label-for="file"
+ >
<input id="file" type="file" name="file" accept=".csv,text/csv" />
</gl-form-group>
<p class="text-secondary">
diff --git a/app/assets/javascripts/issuable/index.js b/app/assets/javascripts/issuable/index.js
index 072422944f5..57bad5182e7 100644
--- a/app/assets/javascripts/issuable/index.js
+++ b/app/assets/javascripts/issuable/index.js
@@ -11,7 +11,9 @@ import IssuableHeaderWarnings from './components/issuable_header_warnings.vue';
export function initCsvImportExportButtons() {
const el = document.querySelector('.js-csv-import-export-buttons');
- if (!el) return null;
+ if (!el) {
+ return null;
+ }
const {
showExportButton,
@@ -42,23 +44,24 @@ export function initCsvImportExportButtons() {
maxAttachmentSize,
showLabel,
},
- render(h) {
- return h(CsvImportExportButtons, {
+ render: (createElement) =>
+ createElement(CsvImportExportButtons, {
props: {
exportCsvPath,
issuableCount: parseInt(issuableCount, 10),
},
- });
- },
+ }),
});
}
export function initIssuableByEmail() {
- Vue.use(GlToast);
-
const el = document.querySelector('.js-issuable-by-email');
- if (!el) return null;
+ if (!el) {
+ return null;
+ }
+
+ Vue.use(GlToast);
const {
initialEmail,
@@ -79,9 +82,7 @@ export function initIssuableByEmail() {
markdownHelpPath,
resetPath,
},
- render(h) {
- return h(IssuableByEmail);
- },
+ render: (createElement) => createElement(IssuableByEmail),
});
}
@@ -89,7 +90,7 @@ export function initIssuableHeaderWarnings(store) {
const el = document.getElementById('js-issuable-header-warnings');
if (!el) {
- return false;
+ return null;
}
const { hidden } = el.dataset;
@@ -98,18 +99,18 @@ export function initIssuableHeaderWarnings(store) {
el,
store,
provide: { hidden: parseBoolean(hidden) },
- render(createElement) {
- return createElement(IssuableHeaderWarnings);
- },
+ render: (createElement) => createElement(IssuableHeaderWarnings),
});
}
export function initIssuableSidebar() {
- const sidebarOptEl = document.querySelector('.js-sidebar-options');
+ const el = document.querySelector('.js-sidebar-options');
- if (!sidebarOptEl) return;
+ if (!el) {
+ return;
+ }
- const sidebarOptions = getSidebarOptions(sidebarOptEl);
+ const sidebarOptions = getSidebarOptions(el);
new IssuableContext(sidebarOptions.currentUser); // eslint-disable-line no-new
Sidebar.initialize();
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index b7b123dfd5f..4b9a42da178 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -19,6 +19,12 @@ export const IssuableType = {
Alert: 'alert',
};
+export const IssueType = {
+ Issue: 'issue',
+ Incident: 'incident',
+ TestCase: 'test_case',
+};
+
export const WorkspaceType = {
project: 'project',
group: 'group',
diff --git a/app/assets/javascripts/create_merge_request_dropdown.js b/app/assets/javascripts/issues/create_merge_request_dropdown.js
index ae6e6bf02e4..5d36396bc6e 100644
--- a/app/assets/javascripts/create_merge_request_dropdown.js
+++ b/app/assets/javascripts/issues/create_merge_request_dropdown.js
@@ -3,13 +3,13 @@ import {
init as initConfidentialMergeRequest,
isConfidentialIssue,
canCreateConfidentialMergeRequest,
-} from './confidential_merge_request';
-import confidentialMergeRequestState from './confidential_merge_request/state';
-import DropLab from './filtered_search/droplab/drop_lab_deprecated';
-import ISetter from './filtered_search/droplab/plugins/input_setter';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { __, sprintf } from './locale';
+} from '~/confidential_merge_request';
+import confidentialMergeRequestState from '~/confidential_merge_request/state';
+import DropLab from '~/filtered_search/droplab/drop_lab_deprecated';
+import ISetter from '~/filtered_search/droplab/plugins/input_setter';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { __, sprintf } from '~/locale';
// Todo: Remove this when fixing issue in input_setter plugin
const InputSetter = { ...ISetter };
diff --git a/app/assets/javascripts/issues/form.js b/app/assets/javascripts/issues/form.js
deleted file mode 100644
index 33371d065f9..00000000000
--- a/app/assets/javascripts/issues/form.js
+++ /dev/null
@@ -1,24 +0,0 @@
-/* eslint-disable no-new */
-
-import $ from 'jquery';
-import IssuableForm from 'ee_else_ce/issuable/issuable_form';
-import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import GLForm from '~/gl_form';
-import { initTitleSuggestions, initTypePopover } from '~/issues/new';
-import LabelsSelect from '~/labels/labels_select';
-import MilestoneSelect from '~/milestones/milestone_select';
-import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
-
-export default () => {
- new ShortcutsNavigation();
- new GLForm($('.issue-form'));
- new IssuableForm($('.issue-form'));
- new LabelsSelect();
- new MilestoneSelect();
- new IssuableTemplateSelectors({
- warnTemplateOverride: true,
- });
-
- initTitleSuggestions();
- initTypePopover();
-};
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
new file mode 100644
index 00000000000..2ee9ac2a682
--- /dev/null
+++ b/app/assets/javascripts/issues/index.js
@@ -0,0 +1,88 @@
+import $ from 'jquery';
+import IssuableForm from 'ee_else_ce/issuable/issuable_form';
+import loadAwardsHandler from '~/awards_handler';
+import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
+import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
+import GLForm from '~/gl_form';
+import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
+import IssuableTemplateSelectors from '~/issuable/issuable_template_selectors';
+import { IssueType } from '~/issues/constants';
+import Issue from '~/issues/issue';
+import { initTitleSuggestions, initTypePopover } from '~/issues/new';
+import { initRelatedMergeRequests } from '~/issues/related_merge_requests';
+import {
+ initHeaderActions,
+ initIncidentApp,
+ initIssueApp,
+ initSentryErrorStackTrace,
+} from '~/issues/show';
+import { parseIssuableData } from '~/issues/show/utils/parse_data';
+import LabelsSelect from '~/labels/labels_select';
+import MilestoneSelect from '~/milestones/milestone_select';
+import initNotesApp from '~/notes';
+import { store } from '~/notes/stores';
+import ZenMode from '~/zen_mode';
+import FilteredSearchServiceDesk from './filtered_search_service_desk';
+
+export function initFilteredSearchServiceDesk() {
+ if (document.querySelector('.filtered-search')) {
+ const supportBotData = JSON.parse(
+ document.querySelector('.js-service-desk-issues').dataset.supportBot,
+ );
+ const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
+ filteredSearchManager.setup();
+ }
+}
+
+export function initForm() {
+ new GLForm($('.issue-form')); // eslint-disable-line no-new
+ new IssuableForm($('.issue-form')); // eslint-disable-line no-new
+ new IssuableTemplateSelectors({ warnTemplateOverride: true }); // eslint-disable-line no-new
+ new LabelsSelect(); // eslint-disable-line no-new
+ new MilestoneSelect(); // eslint-disable-line no-new
+ new ShortcutsNavigation(); // eslint-disable-line no-new
+
+ initTitleSuggestions();
+ initTypePopover();
+}
+
+export function initShow() {
+ const el = document.getElementById('js-issuable-app');
+
+ if (!el) {
+ return;
+ }
+
+ const { issueType, ...issuableData } = parseIssuableData(el);
+
+ if (issueType === IssueType.Incident) {
+ initIncidentApp(issuableData);
+ initHeaderActions(store, IssueType.Incident);
+ } else {
+ initIssueApp(issuableData, store);
+ initHeaderActions(store);
+ }
+
+ new Issue(); // eslint-disable-line no-new
+ new ShortcutsIssuable(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+ initIssuableHeaderWarnings(store);
+ initIssuableSidebar();
+ initNotesApp();
+ initRelatedMergeRequests();
+ initSentryErrorStackTrace();
+
+ const awardEmojiEl = document.getElementById('js-vue-awards-block');
+
+ if (awardEmojiEl) {
+ import('~/emoji/awards_app')
+ .then((m) => m.default(awardEmojiEl))
+ .catch(() => {});
+ } else {
+ loadAwardsHandler();
+ }
+
+ import(/* webpackChunkName: 'design_management' */ '~/design_management')
+ .then((module) => module.default())
+ .catch(() => {});
+}
diff --git a/app/assets/javascripts/issues/init_filtered_search_service_desk.js b/app/assets/javascripts/issues/init_filtered_search_service_desk.js
deleted file mode 100644
index 1901802c11c..00000000000
--- a/app/assets/javascripts/issues/init_filtered_search_service_desk.js
+++ /dev/null
@@ -1,11 +0,0 @@
-import FilteredSearchServiceDesk from './filtered_search_service_desk';
-
-export function initFilteredSearchServiceDesk() {
- if (document.querySelector('.filtered-search')) {
- const supportBotData = JSON.parse(
- document.querySelector('.js-service-desk-issues').dataset.supportBot,
- );
- const filteredSearchManager = new FilteredSearchServiceDesk(supportBotData);
- filteredSearchManager.setup();
- }
-}
diff --git a/app/assets/javascripts/issues/issue.js b/app/assets/javascripts/issues/issue.js
index c471875654b..8e27f547b5c 100644
--- a/app/assets/javascripts/issues/issue.js
+++ b/app/assets/javascripts/issues/issue.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import { joinPaths } from '~/lib/utils/url_utility';
-import CreateMergeRequestDropdown from '~/create_merge_request_dropdown';
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';
import { __ } from '~/locale';
+import CreateMergeRequestDropdown from './create_merge_request_dropdown';
export default class Issue {
constructor() {
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 aece7372182..aece7372182 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
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 6ced1080b71..8b15e801f02 100644
--- a/app/assets/javascripts/issues_list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -11,9 +11,9 @@ import {
import * as Sentry from '@sentry/browser';
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { orderBy } from 'lodash';
-import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import getIssuesCountsQuery from 'ee_else_ce/issues_list/queries/get_issues_counts.query.graphql';
-import IssueCardTimeInfo from 'ee_else_ce/issues_list/components/issue_card_time_info.vue';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import getIssuesCountsQuery from 'ee_else_ce/issues/list/queries/get_issues_counts.query.graphql';
+import IssueCardTimeInfo from 'ee_else_ce/issues/list/components/issue_card_time_info.vue';
import createFlash, { FLASH_TYPES } from '~/flash';
import { TYPE_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -41,7 +41,7 @@ import {
TOKEN_TYPE_TYPE,
UPDATED_DESC,
urlSortParams,
-} from '~/issues_list/constants';
+} from '~/issues/list/constants';
import {
convertToApiParams,
convertToSearchQuery,
@@ -51,7 +51,7 @@ import {
getInitialPageParams,
getSortKey,
getSortOptions,
-} from '~/issues_list/utils';
+} from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { getParameterByName, joinPaths } from '~/lib/utils/url_utility';
@@ -517,10 +517,9 @@ export default {
},
async handleBulkUpdateClick() {
if (!this.hasInitBulkEdit) {
- const initBulkUpdateSidebar = await import(
- '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar'
- );
- initBulkUpdateSidebar.default.init('issuable_');
+ const bulkUpdateSidebar = await import('~/issuable/bulk_update_sidebar');
+ bulkUpdateSidebar.initBulkUpdateSidebar('issuable_');
+ bulkUpdateSidebar.initIssueStatusSelect();
const usersSelect = await import('~/users_select');
const UsersSelect = usersSelect.default;
diff --git a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue b/app/assets/javascripts/issues/list/components/jira_issues_import_status_app.vue
index fb1dbef666c..fb1dbef666c 100644
--- a/app/assets/javascripts/issues_list/components/jira_issues_import_status_app.vue
+++ b/app/assets/javascripts/issues/list/components/jira_issues_import_status_app.vue
diff --git a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
index e749579af80..71f84050ba8 100644
--- a/app/assets/javascripts/issues_list/components/new_issue_dropdown.vue
+++ b/app/assets/javascripts/issues/list/components/new_issue_dropdown.vue
@@ -7,7 +7,7 @@ import {
GlSearchBoxByType,
} from '@gitlab/ui';
import createFlash from '~/flash';
-import searchProjectsQuery from '~/issues_list/queries/search_projects.query.graphql';
+import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
diff --git a/app/assets/javascripts/issues_list/constants.js b/app/assets/javascripts/issues/list/constants.js
index c9eaf0b9908..4a380848b4f 100644
--- a/app/assets/javascripts/issues_list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -9,62 +9,6 @@ import {
OPERATOR_IS_NOT,
} from '~/vue_shared/components/filtered_search_bar/constants';
-// Maps sort order as it appears in the URL query to API `order_by` and `sort` params.
-const PRIORITY = 'priority';
-const ASC = 'asc';
-const DESC = 'desc';
-const CREATED_AT = 'created_at';
-const UPDATED_AT = 'updated_at';
-const DUE_DATE = 'due_date';
-const MILESTONE_DUE = 'milestone_due';
-const POPULARITY = 'popularity';
-const WEIGHT = 'weight';
-const LABEL_PRIORITY = 'label_priority';
-const TITLE = 'title';
-export const RELATIVE_POSITION = 'relative_position';
-export const LOADING_LIST_ITEMS_LENGTH = 8;
-export const PAGE_SIZE = 20;
-export const PAGE_SIZE_MANUAL = 100;
-
-export const sortOrderMap = {
- priority: { order_by: PRIORITY, sort: ASC }, // asc and desc are flipped for some reason
- created_date: { order_by: CREATED_AT, sort: DESC },
- created_asc: { order_by: CREATED_AT, sort: ASC },
- updated_desc: { order_by: UPDATED_AT, sort: DESC },
- updated_asc: { order_by: UPDATED_AT, sort: ASC },
- milestone_due_desc: { order_by: MILESTONE_DUE, sort: DESC },
- milestone: { order_by: MILESTONE_DUE, sort: ASC },
- due_date_desc: { order_by: DUE_DATE, sort: DESC },
- due_date: { order_by: DUE_DATE, sort: ASC },
- popularity: { order_by: POPULARITY, sort: DESC },
- popularity_asc: { order_by: POPULARITY, sort: ASC },
- label_priority: { order_by: LABEL_PRIORITY, sort: ASC }, // asc and desc are flipped
- relative_position: { order_by: RELATIVE_POSITION, sort: ASC },
- weight_desc: { order_by: WEIGHT, sort: DESC },
- weight: { order_by: WEIGHT, sort: ASC },
- title: { order_by: TITLE, sort: ASC },
- title_desc: { order_by: TITLE, sort: DESC },
-};
-
-export const availableSortOptionsJira = [
- {
- id: 1,
- title: __('Created date'),
- sortDirection: {
- descending: 'created_desc',
- ascending: 'created_asc',
- },
- },
- {
- id: 2,
- title: __('Last updated'),
- sortDirection: {
- descending: 'updated_desc',
- ascending: 'updated_asc',
- },
- },
-];
-
export const i18n = {
anonymousSearchingMessage: __('You must sign in to search for specific terms.'),
calendarLabel: __('Subscribe to calendar'),
@@ -108,11 +52,13 @@ export const i18n = {
upvotes: __('Upvotes'),
};
-export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
-
+export const MAX_LIST_SIZE = 10;
+export const PAGE_SIZE = 20;
+export const PAGE_SIZE_MANUAL = 100;
export const PARAM_DUE_DATE = 'due_date';
export const PARAM_SORT = 'sort';
export const PARAM_STATE = 'state';
+export const RELATIVE_POSITION = 'relative_position';
export const defaultPageSizeParams = {
firstPageSize: PAGE_SIZE,
@@ -183,8 +129,6 @@ export const urlSortParams = {
[TITLE_DESC]: 'title_desc',
};
-export const MAX_LIST_SIZE = 10;
-
export const API_PARAM = 'apiParam';
export const URL_PARAM = 'urlParam';
export const NORMAL_FILTER = 'normalFilter';
diff --git a/app/assets/javascripts/issues_list/eventhub.js b/app/assets/javascripts/issues/list/eventhub.js
index e31806ad199..e31806ad199 100644
--- a/app/assets/javascripts/issues_list/eventhub.js
+++ b/app/assets/javascripts/issues/list/eventhub.js
diff --git a/app/assets/javascripts/issues_list/index.js b/app/assets/javascripts/issues/list/index.js
index 9d2ec8b32d2..01cc82ed8fd 100644
--- a/app/assets/javascripts/issues_list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -1,11 +1,10 @@
import produce from 'immer';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import getIssuesQuery from 'ee_else_ce/issues_list/queries/get_issues.query.graphql';
-import IssuesListApp from 'ee_else_ce/issues_list/components/issues_list_app.vue';
+import getIssuesQuery from 'ee_else_ce/issues/list/queries/get_issues.query.graphql';
+import IssuesListApp from 'ee_else_ce/issues/list/components/issues_list_app.vue';
import createDefaultClient from '~/lib/graphql';
-import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import IssuablesListApp from './components/issuables_list_app.vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import JiraIssuesImportStatusRoot from './components/jira_issues_import_status_app.vue';
export function mountJiraIssuesListApp() {
@@ -45,35 +44,6 @@ export function mountJiraIssuesListApp() {
});
}
-export function mountIssuablesListApp() {
- if (!gon.features?.vueIssuablesList) {
- return;
- }
-
- document.querySelectorAll('.js-issuables-list').forEach((el) => {
- const { canBulkEdit, emptyStateMeta = {}, scopedLabelsAvailable, ...data } = el.dataset;
-
- return new Vue({
- el,
- provide: {
- scopedLabelsAvailable: parseBoolean(scopedLabelsAvailable),
- },
- render(createElement) {
- return createElement(IssuablesListApp, {
- props: {
- ...data,
- emptyStateMeta:
- Object.keys(emptyStateMeta).length !== 0
- ? convertObjectPropsToCamelCase(JSON.parse(emptyStateMeta))
- : {},
- canBulkEdit: Boolean(canBulkEdit),
- },
- });
- },
- });
- });
-}
-
export function mountIssuesListApp() {
const el = document.querySelector('.js-issues-list');
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 be8deb3fe97..be8deb3fe97 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
index 1a345fd2877..1a345fd2877 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_counts.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_counts.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql b/app/assets/javascripts/issues/list/queries/get_issues_list_details.query.graphql
index a53dba8c7c8..a53dba8c7c8 100644
--- a/app/assets/javascripts/issues_list/queries/get_issues_list_details.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/get_issues_list_details.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
index 07dae3fd756..07dae3fd756 100644
--- a/app/assets/javascripts/issues_list/queries/issue.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/issue.fragment.graphql
diff --git a/app/assets/javascripts/issues_list/queries/label.fragment.graphql b/app/assets/javascripts/issues/list/queries/label.fragment.graphql
index bb1d8f1ac9b..bb1d8f1ac9b 100644
--- a/app/assets/javascripts/issues_list/queries/label.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/label.fragment.graphql
diff --git a/app/assets/javascripts/issues_list/queries/milestone.fragment.graphql b/app/assets/javascripts/issues/list/queries/milestone.fragment.graphql
index 3cdf69bf585..3cdf69bf585 100644
--- a/app/assets/javascripts/issues_list/queries/milestone.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/milestone.fragment.graphql
diff --git a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql b/app/assets/javascripts/issues/list/queries/reorder_issues.mutation.graphql
index 160026a4742..160026a4742 100644
--- a/app/assets/javascripts/issues_list/queries/reorder_issues.mutation.graphql
+++ b/app/assets/javascripts/issues/list/queries/reorder_issues.mutation.graphql
diff --git a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql b/app/assets/javascripts/issues/list/queries/search_labels.query.graphql
index 44b57317161..44b57317161 100644
--- a/app/assets/javascripts/issues_list/queries/search_labels.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_labels.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
index e7eb08104a6..e7eb08104a6 100644
--- a/app/assets/javascripts/issues_list/queries/search_milestones.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_milestones.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql b/app/assets/javascripts/issues/list/queries/search_projects.query.graphql
index bd2f9bc2340..bd2f9bc2340 100644
--- a/app/assets/javascripts/issues_list/queries/search_projects.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_projects.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/search_users.query.graphql b/app/assets/javascripts/issues/list/queries/search_users.query.graphql
index 92517ad35d0..92517ad35d0 100644
--- a/app/assets/javascripts/issues_list/queries/search_users.query.graphql
+++ b/app/assets/javascripts/issues/list/queries/search_users.query.graphql
diff --git a/app/assets/javascripts/issues_list/queries/user.fragment.graphql b/app/assets/javascripts/issues/list/queries/user.fragment.graphql
index 3e5bc0f7b93..3e5bc0f7b93 100644
--- a/app/assets/javascripts/issues_list/queries/user.fragment.graphql
+++ b/app/assets/javascripts/issues/list/queries/user.fragment.graphql
diff --git a/app/assets/javascripts/issues_list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 99946e4e851..2919bbbfef8 100644
--- a/app/assets/javascripts/issues_list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -36,7 +36,7 @@ import {
urlSortParams,
WEIGHT_ASC,
WEIGHT_DESC,
-} from '~/issues_list/constants';
+} from '~/issues/list/constants';
import { isPositiveInteger } from '~/lib/utils/number_utils';
import { __ } from '~/locale';
import {
@@ -72,7 +72,7 @@ export const getSortOptions = (hasIssueWeightsFeature, hasBlockedIssuesFeature)
},
{
id: 3,
- title: __('Last updated'),
+ title: __('Updated date'),
sortDirection: {
ascending: UPDATED_ASC,
descending: UPDATED_DESC,
diff --git a/app/assets/javascripts/issues/manual_ordering.js b/app/assets/javascripts/issues/manual_ordering.js
index 9613246d6a6..c78505d0610 100644
--- a/app/assets/javascripts/issues/manual_ordering.js
+++ b/app/assets/javascripts/issues/manual_ordering.js
@@ -20,7 +20,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
});
});
-const initManualOrdering = (draggableSelector = 'li.issue') => {
+const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering');
if (!issueList || !(gon.current_user_id > 0)) {
@@ -37,14 +37,14 @@ const initManualOrdering = (draggableSelector = 'li.issue') => {
group: {
name: 'issues',
},
- draggable: draggableSelector,
+ draggable: 'li.issue',
onStart: () => {
sortableStart();
},
onUpdate: (event) => {
const el = event.item;
- const url = el.getAttribute('url') || el.dataset.url;
+ const url = el.getAttribute('url');
const prev = el.previousElementSibling;
const next = el.nextElementSibling;
diff --git a/app/assets/javascripts/issues/new/index.js b/app/assets/javascripts/issues/new/index.js
index 59a7cbec627..f96cacf2595 100644
--- a/app/assets/javascripts/issues/new/index.js
+++ b/app/assets/javascripts/issues/new/index.js
@@ -5,8 +5,6 @@ import TitleSuggestions from './components/title_suggestions.vue';
import TypePopover from './components/type_popover.vue';
export function initTitleSuggestions() {
- Vue.use(VueApollo);
-
const el = document.getElementById('js-suggestions');
const issueTitle = document.getElementById('issue_title');
@@ -14,6 +12,8 @@ export function initTitleSuggestions() {
return undefined;
}
+ Vue.use(VueApollo);
+
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
diff --git a/app/assets/javascripts/issues/related_merge_requests/index.js b/app/assets/javascripts/issues/related_merge_requests/index.js
index ce33cf7df1d..5045f7e1a2a 100644
--- a/app/assets/javascripts/issues/related_merge_requests/index.js
+++ b/app/assets/javascripts/issues/related_merge_requests/index.js
@@ -2,23 +2,21 @@ import Vue from 'vue';
import RelatedMergeRequests from './components/related_merge_requests.vue';
import createStore from './store';
-export default function initRelatedMergeRequests() {
- const relatedMergeRequestsElement = document.querySelector('#js-related-merge-requests');
+export function initRelatedMergeRequests() {
+ const el = document.querySelector('#js-related-merge-requests');
- if (relatedMergeRequestsElement) {
- const { endpoint, projectPath, projectNamespace } = relatedMergeRequestsElement.dataset;
-
- // eslint-disable-next-line no-new
- new Vue({
- el: relatedMergeRequestsElement,
- components: {
- RelatedMergeRequests,
- },
- store: createStore(),
- render: (createElement) =>
- createElement('related-merge-requests', {
- props: { endpoint, projectNamespace, projectPath },
- }),
- });
+ if (!el) {
+ return undefined;
}
+
+ const { endpoint, projectPath, projectNamespace } = el.dataset;
+
+ return new Vue({
+ el,
+ store: createStore(),
+ render: (createElement) =>
+ createElement(RelatedMergeRequests, {
+ props: { endpoint, projectNamespace, projectPath },
+ }),
+ });
}
diff --git a/app/assets/javascripts/issues/sentry_error_stack_trace/index.js b/app/assets/javascripts/issues/sentry_error_stack_trace/index.js
deleted file mode 100644
index 8e9ee25e7a8..00000000000
--- a/app/assets/javascripts/issues/sentry_error_stack_trace/index.js
+++ /dev/null
@@ -1,22 +0,0 @@
-import Vue from 'vue';
-import store from '~/error_tracking/store';
-import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
-
-export default function initSentryErrorStacktrace() {
- const sentryErrorStackTraceEl = document.querySelector('#js-sentry-error-stack-trace');
- if (sentryErrorStackTraceEl) {
- const { issueStackTracePath } = sentryErrorStackTraceEl.dataset;
- // eslint-disable-next-line no-new
- new Vue({
- el: sentryErrorStackTraceEl,
- components: {
- SentryErrorStackTrace,
- },
- store,
- render: (createElement) =>
- createElement('sentry-error-stack-trace', {
- props: { issueStackTracePath },
- }),
- });
- }
-}
diff --git a/app/assets/javascripts/issues/show.js b/app/assets/javascripts/issues/show.js
deleted file mode 100644
index e43e56d7b4e..00000000000
--- a/app/assets/javascripts/issues/show.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import loadAwardsHandler from '~/awards_handler';
-import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
-import { initIssuableHeaderWarnings, initIssuableSidebar } from '~/issuable';
-import { IssuableType } from '~/vue_shared/issuable/show/constants';
-import Issue from '~/issues/issue';
-import { initIncidentApp, initIncidentHeaderActions } from '~/issues/show/incident';
-import { initIssuableApp, initIssueHeaderActions } from '~/issues/show/issue';
-import { parseIssuableData } from '~/issues/show/utils/parse_data';
-import initNotesApp from '~/notes';
-import { store } from '~/notes/stores';
-import initRelatedMergeRequestsApp from '~/issues/related_merge_requests';
-import initSentryErrorStackTraceApp from '~/issues/sentry_error_stack_trace';
-import ZenMode from '~/zen_mode';
-
-export default function initShowIssue() {
- initNotesApp();
-
- const initialDataEl = document.getElementById('js-issuable-app');
- const { issueType, ...issuableData } = parseIssuableData(initialDataEl);
-
- switch (issueType) {
- case IssuableType.Incident:
- initIncidentApp(issuableData);
- initIncidentHeaderActions(store);
- break;
- case IssuableType.Issue:
- initIssuableApp(issuableData, store);
- initIssueHeaderActions(store);
- break;
- default:
- initIssueHeaderActions(store);
- break;
- }
-
- initIssuableHeaderWarnings(store);
- initSentryErrorStackTraceApp();
- initRelatedMergeRequestsApp();
-
- import(/* webpackChunkName: 'design_management' */ '~/design_management')
- .then((module) => module.default())
- .catch(() => {});
-
- new ZenMode(); // eslint-disable-line no-new
-
- if (issueType !== IssuableType.TestCase) {
- const awardEmojiEl = document.getElementById('js-vue-awards-block');
-
- new Issue(); // eslint-disable-line no-new
- new ShortcutsIssuable(); // eslint-disable-line no-new
- initIssuableSidebar();
- if (awardEmojiEl) {
- import('~/emoji/awards_app')
- .then((m) => m.default(awardEmojiEl))
- .catch(() => {});
- } else {
- loadAwardsHandler();
- }
- }
-}
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index eeaf865a35f..0490728c6bc 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -6,7 +6,7 @@ import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/const
import Poll from '~/lib/utils/poll';
import { visitUrl } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
-import { IssueTypePath, IncidentTypePath, IncidentType, POLLING_DELAY } from '../constants';
+import { ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH, INCIDENT_TYPE, POLLING_DELAY } from '../constants';
import eventHub from '../event_hub';
import getIssueStateQuery from '../queries/get_issue_state.query.graphql';
import Service from '../services/index';
@@ -378,15 +378,15 @@ export default {
.then((data) => {
if (
!window.location.pathname.includes(data.web_url) &&
- issueState.issueType !== IncidentType
+ issueState.issueType !== INCIDENT_TYPE
) {
visitUrl(data.web_url);
}
if (issueState.isDirty) {
const URI =
- issueState.issueType === IncidentType
- ? data.web_url.replace(IssueTypePath, IncidentTypePath)
+ issueState.issueType === INCIDENT_TYPE
+ ? data.web_url.replace(ISSUE_TYPE_PATH, INCIDENT_TYPE_PATH)
: data.web_url;
visitUrl(URI);
}
diff --git a/app/assets/javascripts/issues/show/components/fields/type.vue b/app/assets/javascripts/issues/show/components/fields/type.vue
index 9110a6924b4..75d0b9e5e76 100644
--- a/app/assets/javascripts/issues/show/components/fields/type.vue
+++ b/app/assets/javascripts/issues/show/components/fields/type.vue
@@ -2,7 +2,7 @@
import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
import { capitalize } from 'lodash';
import { __ } from '~/locale';
-import { IssuableTypes, IncidentType } from '../../constants';
+import { issuableTypes, INCIDENT_TYPE } from '../../constants';
import getIssueStateQuery from '../../queries/get_issue_state.query.graphql';
import updateIssueStateMutation from '../../queries/update_issue_state.mutation.graphql';
@@ -12,7 +12,7 @@ export const i18n = {
export default {
i18n,
- IssuableTypes,
+ issuableTypes,
components: {
GlFormGroup,
GlIcon,
@@ -45,7 +45,7 @@ export default {
return capitalize(issueType);
},
shouldShowIncident() {
- return this.issueType === IncidentType || this.canCreateIncident;
+ return this.issueType === INCIDENT_TYPE || this.canCreateIncident;
},
},
methods: {
@@ -59,7 +59,7 @@ export default {
});
},
isShown(type) {
- return type.value !== IncidentType || this.shouldShowIncident;
+ return type.value !== INCIDENT_TYPE || this.shouldShowIncident;
},
},
};
@@ -81,7 +81,7 @@ export default {
toggle-class="dropdown-menu-toggle"
>
<gl-dropdown-item
- v-for="type in $options.IssuableTypes"
+ v-for="type in $options.issuableTypes"
v-show="isShown(type)"
:key="type.value"
:is-checked="issueState.issueType === type.value"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 700ef92a0f3..8ba08472ea0 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -11,9 +11,8 @@ import {
import { mapActions, mapGetters, mapState } from 'vuex';
import createFlash, { FLASH_TYPES } from '~/flash';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
-import { IssuableType } from '~/vue_shared/issuable/show/constants';
-import { IssuableStatus } from '~/issues/constants';
-import { IssueStateEvent } from '~/issues/show/constants';
+import { IssuableStatus, IssueType } from '~/issues/constants';
+import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl } from '~/lib/utils/url_utility';
import { s__, __, sprintf } from '~/locale';
@@ -83,7 +82,7 @@ export default {
default: '',
},
issueType: {
- default: IssuableType.Issue,
+ default: IssueType.Issue,
},
newIssuePath: {
default: '',
@@ -106,8 +105,8 @@ export default {
},
issueTypeText() {
const issueTypeTexts = {
- [IssuableType.Issue]: s__('HeaderAction|issue'),
- [IssuableType.Incident]: s__('HeaderAction|incident'),
+ [IssueType.Issue]: s__('HeaderAction|issue'),
+ [IssueType.Incident]: s__('HeaderAction|incident'),
};
return issueTypeTexts[this.issueType] ?? this.issueType;
@@ -163,7 +162,7 @@ export default {
input: {
iid: this.iid.toString(),
projectPath: this.projectPath,
- stateEvent: this.isClosed ? IssueStateEvent.Reopen : IssueStateEvent.Close,
+ stateEvent: this.isClosed ? ISSUE_STATE_EVENT_REOPEN : ISSUE_STATE_EVENT_CLOSE,
},
},
})
diff --git a/app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue b/app/assets/javascripts/issues/show/components/sentry_error_stack_trace.vue
index 1530e9a15b5..1530e9a15b5 100644
--- a/app/assets/javascripts/issues/sentry_error_stack_trace/components/sentry_error_stack_trace.vue
+++ b/app/assets/javascripts/issues/show/components/sentry_error_stack_trace.vue
diff --git a/app/assets/javascripts/issues/show/constants.js b/app/assets/javascripts/issues/show/constants.js
index 35f3bcdad70..a100aaf88ad 100644
--- a/app/assets/javascripts/issues/show/constants.js
+++ b/app/assets/javascripts/issues/show/constants.js
@@ -1,22 +1,20 @@
import { __ } from '~/locale';
-export const IssueStateEvent = {
- Close: 'CLOSE',
- Reopen: 'REOPEN',
-};
-
-export const STATUS_PAGE_PUBLISHED = __('Published on status page');
+export const INCIDENT_TYPE = 'incident';
+export const INCIDENT_TYPE_PATH = 'issues/incident';
+export const ISSUE_STATE_EVENT_CLOSE = 'CLOSE';
+export const ISSUE_STATE_EVENT_REOPEN = 'REOPEN';
+export const ISSUE_TYPE_PATH = 'issues';
export const JOIN_ZOOM_MEETING = __('Join Zoom meeting');
+export const POLLING_DELAY = 2000;
+export const STATUS_PAGE_PUBLISHED = __('Published on status page');
-export const IssuableTypes = [
+export const issuableTypes = [
{ value: 'issue', text: __('Issue'), icon: 'issue-type-issue' },
{ value: 'incident', text: __('Incident'), icon: 'issue-type-incident' },
];
-export const IssueTypePath = 'issues';
-export const IncidentTypePath = 'issues/incident';
-export const IncidentType = 'incident';
-
-export const issueState = { issueType: undefined, isDirty: false };
-
-export const POLLING_DELAY = 2000;
+export const issueState = {
+ issueType: undefined,
+ isDirty: false,
+};
diff --git a/app/assets/javascripts/issues/show/incident.js b/app/assets/javascripts/issues/show/index.js
index a260c31e1da..7f5a0e32f72 100644
--- a/app/assets/javascripts/issues/show/incident.js
+++ b/app/assets/javascripts/issues/show/index.js
@@ -1,11 +1,15 @@
import Vue from 'vue';
+import { mapGetters } from 'vuex';
+import errorTrackingStore from '~/error_tracking/store';
import { parseBoolean } from '~/lib/utils/common_utils';
-import issuableApp from './components/app.vue';
-import incidentTabs from './components/incidents/incident_tabs.vue';
-import { issueState, IncidentType } from './constants';
+import { scrollToTargetOnResize } from '~/lib/utils/resize_observer';
+import IssueApp from './components/app.vue';
+import HeaderActions from './components/header_actions.vue';
+import IncidentTabs from './components/incidents/incident_tabs.vue';
+import SentryErrorStackTrace from './components/sentry_error_stack_trace.vue';
+import { INCIDENT_TYPE, issueState } from './constants';
import apolloProvider from './graphql';
import getIssueStateQuery from './queries/get_issue_state.query.graphql';
-import HeaderActions from './components/header_actions.vue';
const bootstrapApollo = (state = {}) => {
return apolloProvider.clients.defaultClient.cache.writeQuery({
@@ -16,7 +20,7 @@ const bootstrapApollo = (state = {}) => {
});
};
-export function initIncidentApp(issuableData = {}) {
+export function initIncidentApp(issueData = {}) {
const el = document.getElementById('js-issuable-app');
if (!el) {
@@ -34,18 +38,15 @@ export function initIncidentApp(issuableData = {}) {
projectId,
slaFeatureAvailable,
uploadMetricsFeatureAvailable,
- } = issuableData;
+ } = issueData;
const fullPath = `${projectNamespace}/${projectPath}`;
return new Vue({
el,
apolloProvider,
- components: {
- issuableApp,
- },
provide: {
- issueType: IncidentType,
+ issueType: INCIDENT_TYPE,
canCreateIncident,
canUpdate,
fullPath,
@@ -55,10 +56,10 @@ export function initIncidentApp(issuableData = {}) {
uploadMetricsFeatureAvailable: parseBoolean(uploadMetricsFeatureAvailable),
},
render(createElement) {
- return createElement('issuable-app', {
+ return createElement(IssueApp, {
props: {
- ...issuableData,
- descriptionComponent: incidentTabs,
+ ...issueData,
+ descriptionComponent: IncidentTabs,
showTitleBorder: false,
},
});
@@ -66,7 +67,46 @@ export function initIncidentApp(issuableData = {}) {
});
}
-export function initIncidentHeaderActions(store) {
+export function initIssueApp(issueData, store) {
+ const el = document.getElementById('js-issuable-app');
+
+ if (!el) {
+ return undefined;
+ }
+
+ if (gon?.features?.fixCommentScroll) {
+ scrollToTargetOnResize();
+ }
+
+ bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+
+ const { canCreateIncident, ...issueProps } = issueData;
+
+ return new Vue({
+ el,
+ apolloProvider,
+ store,
+ provide: {
+ canCreateIncident,
+ },
+ computed: {
+ ...mapGetters(['getNoteableData']),
+ },
+ render(createElement) {
+ return createElement(IssueApp, {
+ props: {
+ ...issueProps,
+ isConfidential: this.getNoteableData?.confidential,
+ isLocked: this.getNoteableData?.discussion_locked,
+ issuableStatus: this.getNoteableData?.state,
+ id: this.getNoteableData?.id,
+ },
+ });
+ },
+ });
+}
+
+export function initHeaderActions(store, type = '') {
const el = document.querySelector('.js-issue-header-actions');
if (!el) {
@@ -75,12 +115,15 @@ export function initIncidentHeaderActions(store) {
bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
+ const canCreate =
+ type === INCIDENT_TYPE ? el.dataset.canCreateIncident : el.dataset.canCreateIssue;
+
return new Vue({
el,
apolloProvider,
store,
provide: {
- canCreateIssue: parseBoolean(el.dataset.canCreateIncident),
+ canCreateIssue: parseBoolean(canCreate),
canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
@@ -99,3 +142,20 @@ export function initIncidentHeaderActions(store) {
render: (createElement) => createElement(HeaderActions),
});
}
+
+export function initSentryErrorStackTrace() {
+ const el = document.querySelector('#js-sentry-error-stack-trace');
+
+ if (!el) {
+ return undefined;
+ }
+
+ const { issueStackTracePath } = el.dataset;
+
+ return new Vue({
+ el,
+ store: errorTrackingStore,
+ render: (createElement) =>
+ createElement(SentryErrorStackTrace, { props: { issueStackTracePath } }),
+ });
+}
diff --git a/app/assets/javascripts/issues/show/issue.js b/app/assets/javascripts/issues/show/issue.js
deleted file mode 100644
index 60e90934af8..00000000000
--- a/app/assets/javascripts/issues/show/issue.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import Vue from 'vue';
-import { mapGetters } from 'vuex';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import IssuableApp from './components/app.vue';
-import HeaderActions from './components/header_actions.vue';
-import { issueState } from './constants';
-import apolloProvider from './graphql';
-import getIssueStateQuery from './queries/get_issue_state.query.graphql';
-
-const bootstrapApollo = (state = {}) => {
- return apolloProvider.clients.defaultClient.cache.writeQuery({
- query: getIssueStateQuery,
- data: {
- issueState: state,
- },
- });
-};
-
-export function initIssuableApp(issuableData, store) {
- const el = document.getElementById('js-issuable-app');
-
- if (!el) {
- return undefined;
- }
-
- bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
-
- const { canCreateIncident, ...issuableProps } = issuableData;
-
- return new Vue({
- el,
- apolloProvider,
- store,
- provide: {
- canCreateIncident,
- },
- computed: {
- ...mapGetters(['getNoteableData']),
- },
- render(createElement) {
- return createElement(IssuableApp, {
- props: {
- ...issuableProps,
- isConfidential: this.getNoteableData?.confidential,
- isLocked: this.getNoteableData?.discussion_locked,
- issuableStatus: this.getNoteableData?.state,
- id: this.getNoteableData?.id,
- },
- });
- },
- });
-}
-
-export function initIssueHeaderActions(store) {
- const el = document.querySelector('.js-issue-header-actions');
-
- if (!el) {
- return undefined;
- }
-
- bootstrapApollo({ ...issueState, issueType: el.dataset.issueType });
-
- return new Vue({
- el,
- apolloProvider,
- store,
- provide: {
- canCreateIssue: parseBoolean(el.dataset.canCreateIssue),
- canDestroyIssue: parseBoolean(el.dataset.canDestroyIssue),
- canPromoteToEpic: parseBoolean(el.dataset.canPromoteToEpic),
- canReopenIssue: parseBoolean(el.dataset.canReopenIssue),
- canReportSpam: parseBoolean(el.dataset.canReportSpam),
- canUpdateIssue: parseBoolean(el.dataset.canUpdateIssue),
- iid: el.dataset.iid,
- isIssueAuthor: parseBoolean(el.dataset.isIssueAuthor),
- issuePath: el.dataset.issuePath,
- issueType: el.dataset.issueType,
- newIssuePath: el.dataset.newIssuePath,
- projectPath: el.dataset.projectPath,
- projectId: el.dataset.projectId,
- reportAbusePath: el.dataset.reportAbusePath,
- submitAsSpamPath: el.dataset.submitAsSpamPath,
- },
- render: (createElement) => createElement(HeaderActions),
- });
-}
diff --git a/app/assets/javascripts/issues_list/components/issuable.vue b/app/assets/javascripts/issues_list/components/issuable.vue
deleted file mode 100644
index 6476d5be38c..00000000000
--- a/app/assets/javascripts/issues_list/components/issuable.vue
+++ /dev/null
@@ -1,441 +0,0 @@
-<script>
-/*
- * This is tightly coupled to projects/issues/_issue.html.haml,
- * any changes done to the haml need to be reflected here.
- */
-
-// TODO: need to move this component to graphql - https://gitlab.com/gitlab-org/gitlab/-/issues/221246
-import jiraLogo from '@gitlab/svgs/dist/illustrations/logos/jira.svg';
-import {
- GlLink,
- GlTooltipDirective as GlTooltip,
- GlSprintf,
- GlLabel,
- GlIcon,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import { escape, isNumber } from 'lodash';
-import { isScopedLabel } from '~/lib/utils/common_utils';
-import {
- dateInWords,
- formatDate,
- getDayDifference,
- getTimeago,
- timeFor,
- newDateAsLocaleTime,
-} from '~/lib/utils/datetime_utility';
-import { convertToCamelCase } from '~/lib/utils/text_utility';
-import { mergeUrlParams, setUrlFragment, isExternal } from '~/lib/utils/url_utility';
-import { sprintf, __ } from '~/locale';
-import initUserPopovers from '~/user_popovers';
-import IssueAssignees from '~/issuable/components/issue_assignees.vue';
-
-export default {
- i18n: {
- openedAgo: __('created %{timeAgoString} by %{user}'),
- openedAgoJira: __('created %{timeAgoString} by %{user} in Jira'),
- openedAgoServiceDesk: __('created %{timeAgoString} by %{email} via %{user}'),
- },
- components: {
- IssueAssignees,
- GlLink,
- GlLabel,
- GlIcon,
- GlSprintf,
- IssueHealthStatus: () =>
- import('ee_component/related_items_tree/components/issue_health_status.vue'),
- },
- directives: {
- GlTooltip,
- SafeHtml,
- },
- inject: ['scopedLabelsAvailable'],
- props: {
- issuable: {
- type: Object,
- required: true,
- },
- isBulkEditing: {
- type: Boolean,
- required: false,
- default: false,
- },
- selected: {
- type: Boolean,
- required: false,
- default: false,
- },
- baseUrl: {
- type: String,
- required: false,
- default() {
- return window.location.href;
- },
- },
- },
- data() {
- return {
- jiraLogo,
- };
- },
- computed: {
- milestoneLink() {
- const { title } = this.issuable.milestone;
-
- return this.issuableLink({ milestone_title: title });
- },
- hasWeight() {
- return isNumber(this.issuable.weight);
- },
- dueDate() {
- return this.issuable.due_date ? newDateAsLocaleTime(this.issuable.due_date) : undefined;
- },
- dueDateWords() {
- return this.dueDate ? dateInWords(this.dueDate, true) : undefined;
- },
- isOverdue() {
- return this.dueDate ? this.dueDate < new Date() : false;
- },
- isClosed() {
- return this.issuable.state === 'closed';
- },
- isJiraIssue() {
- return this.issuable.external_tracker === 'jira';
- },
- webUrl() {
- return this.issuable.gitlab_web_url || this.issuable.web_url;
- },
- isIssuableUrlExternal() {
- return isExternal(this.webUrl);
- },
- linkTarget() {
- return this.isIssuableUrlExternal ? '_blank' : null;
- },
- issueCreatedToday() {
- return getDayDifference(new Date(this.issuable.created_at), new Date()) < 1;
- },
- labelIdsString() {
- return JSON.stringify(this.issuable.labels.map((l) => l.id));
- },
- milestoneDueDate() {
- const { due_date: dueDate } = this.issuable.milestone || {};
-
- return dueDate ? newDateAsLocaleTime(dueDate) : undefined;
- },
- milestoneTooltipText() {
- if (this.milestoneDueDate) {
- return sprintf(__('%{primary} (%{secondary})'), {
- primary: formatDate(this.milestoneDueDate, 'mmm d, yyyy'),
- secondary: timeFor(this.milestoneDueDate),
- });
- }
- return __('Milestone');
- },
- issuableAuthor() {
- return this.issuable.author;
- },
- issuableCreatedAt() {
- return getTimeago().format(this.issuable.created_at);
- },
- popoverDataAttrs() {
- const { id, username, name, avatar_url } = this.issuableAuthor;
-
- return {
- 'data-user-id': id,
- 'data-username': username,
- 'data-name': name,
- 'data-avatar-url': avatar_url,
- };
- },
- referencePath() {
- return this.issuable.references.relative;
- },
- updatedDateString() {
- return formatDate(new Date(this.issuable.updated_at), 'mmm d, yyyy h:MMtt');
- },
- updatedDateAgo() {
- // snake_case because it's the same i18n string as the HAML view
- return sprintf(__('updated %{time_ago}'), {
- time_ago: escape(getTimeago().format(this.issuable.updated_at)),
- });
- },
- issuableMeta() {
- return [
- {
- key: 'merge-requests',
- visible: this.issuable.merge_requests_count > 0,
- value: this.issuable.merge_requests_count,
- title: __('Related merge requests'),
- dataTestId: 'merge-requests',
- class: 'js-merge-requests',
- icon: 'merge-request',
- },
- {
- key: 'upvotes',
- visible: this.issuable.upvotes > 0,
- value: this.issuable.upvotes,
- title: __('Upvotes'),
- dataTestId: 'upvotes',
- class: 'js-upvotes issuable-upvotes',
- icon: 'thumb-up',
- },
- {
- key: 'downvotes',
- visible: this.issuable.downvotes > 0,
- value: this.issuable.downvotes,
- title: __('Downvotes'),
- dataTestId: 'downvotes',
- class: 'js-downvotes issuable-downvotes',
- icon: 'thumb-down',
- },
- {
- key: 'blocking-issues',
- visible: this.issuable.blocking_issues_count > 0,
- value: this.issuable.blocking_issues_count,
- title: __('Blocking issues'),
- dataTestId: 'blocking-issues',
- href: setUrlFragment(this.webUrl, 'related-issues'),
- icon: 'issue-block',
- },
- {
- key: 'comments-count',
- visible: !this.isJiraIssue,
- value: this.issuable.user_notes_count,
- title: __('Comments'),
- dataTestId: 'notes-count',
- href: setUrlFragment(this.webUrl, 'notes'),
- class: { 'no-comments': !this.issuable.user_notes_count, 'issuable-comments': true },
- icon: 'comments',
- },
- ];
- },
- healthStatus() {
- return convertToCamelCase(this.issuable.health_status);
- },
- openedMessage() {
- if (this.isJiraIssue) return this.$options.i18n.openedAgoJira;
- if (this.issuable.service_desk_reply_to) return this.$options.i18n.openedAgoServiceDesk;
- return this.$options.i18n.openedAgo;
- },
- },
- mounted() {
- // TODO: Refactor user popover to use its own component instead of
- // spawning event listeners on Vue-rendered elements.
- initUserPopovers([this.$refs.openedAgoByContainer.$el]);
- },
- methods: {
- issuableLink(params) {
- return mergeUrlParams(params, this.baseUrl);
- },
- isScoped({ name }) {
- return isScopedLabel({ title: name }) && this.scopedLabelsAvailable;
- },
- labelHref({ name }) {
- if (this.isJiraIssue) {
- return this.issuableLink({ 'labels[]': name });
- }
-
- return this.issuableLink({ 'label_name[]': name });
- },
- onSelect(ev) {
- this.$emit('select', {
- issuable: this.issuable,
- selected: ev.target.checked,
- });
- },
- issuableMetaComponent(href) {
- return href ? 'gl-link' : 'span';
- },
- },
-
- confidentialTooltipText: __('Confidential'),
-};
-</script>
-<template>
- <li
- :id="`issue_${issuable.id}`"
- class="issue"
- :class="{ today: issueCreatedToday, closed: isClosed }"
- :data-id="issuable.id"
- :data-labels="labelIdsString"
- :data-url="webUrl"
- data-qa-selector="issue_container"
- :data-qa-issue-title="issuable.title"
- >
- <div class="gl-display-flex">
- <!-- Bulk edit checkbox -->
- <div v-if="isBulkEditing" class="gl-mr-3">
- <input
- :id="`selected_issue_${issuable.id}`"
- :checked="selected"
- class="selected-issuable"
- type="checkbox"
- :data-id="issuable.id"
- @input="onSelect"
- />
- </div>
-
- <!-- Issuable info container -->
- <!-- Issuable main info -->
- <div class="gl-flex-grow-1">
- <div class="title">
- <span class="issue-title-text">
- <gl-icon
- v-if="issuable.confidential"
- v-gl-tooltip
- name="eye-slash"
- class="gl-vertical-align-text-bottom"
- :size="16"
- :title="$options.confidentialTooltipText"
- :aria-label="$options.confidentialTooltipText"
- />
- <gl-link
- :href="webUrl"
- :target="linkTarget"
- data-testid="issuable-title"
- data-qa-selector="issue_link"
- >
- {{ issuable.title }}
- <gl-icon
- v-if="isIssuableUrlExternal"
- name="external-link"
- class="gl-vertical-align-text-bottom gl-ml-2"
- />
- </gl-link>
- </span>
- <span
- v-if="issuable.has_tasks"
- class="gl-ml-2 task-status gl-display-none d-sm-inline-block"
- >{{ issuable.task_status }}</span
- >
- </div>
-
- <div class="issuable-info">
- <span class="js-ref-path gl-mr-4 mr-sm-0">
- <span
- v-if="isJiraIssue"
- v-safe-html="jiraLogo"
- class="svg-container logo-container"
- data-testid="jira-logo"
- ></span>
- {{ referencePath }}
- </span>
-
- <span data-testid="openedByMessage" class="gl-display-none d-sm-inline-block gl-mr-4">
- &middot;
- <gl-sprintf :message="openedMessage">
- <template #timeAgoString>
- <span>{{ issuableCreatedAt }}</span>
- </template>
- <template #user>
- <gl-link
- ref="openedAgoByContainer"
- v-bind="popoverDataAttrs"
- :href="issuableAuthor.web_url"
- :target="linkTarget"
- >{{ issuableAuthor.name }}</gl-link
- >
- </template>
- <template #email>
- <span>{{ issuable.service_desk_reply_to }}</span>
- </template>
- </gl-sprintf>
- </span>
-
- <gl-link
- v-if="issuable.milestone"
- v-gl-tooltip
- class="gl-display-none d-sm-inline-block gl-mr-4 js-milestone milestone"
- :href="milestoneLink"
- :title="milestoneTooltipText"
- >
- <gl-icon name="clock" class="s16 gl-vertical-align-text-bottom" />
- {{ issuable.milestone.title }}
- </gl-link>
-
- <span
- v-if="dueDate"
- v-gl-tooltip
- class="gl-display-none d-sm-inline-block gl-mr-4 js-due-date"
- :class="{ cred: isOverdue }"
- :title="__('Due date')"
- >
- <gl-icon name="calendar" />
- {{ dueDateWords }}
- </span>
-
- <span
- v-if="hasWeight"
- v-gl-tooltip
- :title="__('Weight')"
- class="gl-display-none d-sm-inline-block gl-mr-4"
- data-testid="weight"
- data-qa-selector="issuable_weight_content"
- >
- <gl-icon name="weight" class="align-text-bottom" />
- {{ issuable.weight }}
- </span>
-
- <issue-health-status
- v-if="issuable.health_status"
- :health-status="healthStatus"
- class="gl-mr-4 issuable-tag-valign"
- />
-
- <gl-label
- v-for="label in issuable.labels"
- :key="label.id"
- data-qa-selector="issuable-label"
- :target="labelHref(label)"
- :background-color="label.color"
- :description="label.description"
- :color="label.text_color"
- :title="label.name"
- :scoped="isScoped(label)"
- size="sm"
- class="gl-mr-2 issuable-tag-valign"
- >{{ label.name }}</gl-label
- >
- </div>
- </div>
-
- <!-- Issuable meta -->
- <div
- class="gl-flex-shrink-0 gl-display-flex gl-flex-direction-column align-items-end gl-justify-content-center"
- >
- <div class="controls gl-display-flex">
- <span v-if="isJiraIssue" data-testid="issuable-status">{{ issuable.status }}</span>
- <span v-else-if="isClosed" class="issuable-status">{{ __('CLOSED') }}</span>
-
- <issue-assignees
- :assignees="issuable.assignees"
- class="gl-align-items-center gl-display-flex gl-ml-3"
- :icon-size="16"
- img-css-classes="gl-mr-2!"
- :max-visible="4"
- />
-
- <template v-for="meta in issuableMeta">
- <span
- v-if="meta.visible"
- :key="meta.key"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-flex gl-align-items-center gl-ml-3"
- :class="meta.class"
- :data-testid="meta.dataTestId"
- :title="meta.title"
- >
- <component :is="issuableMetaComponent(meta.href)" :href="meta.href">
- <gl-icon v-if="meta.icon" :name="meta.icon" />
- {{ meta.value }}
- </component>
- </span>
- </template>
- </div>
- <div v-gl-tooltip class="issuable-updated-at" :title="updatedDateString">
- {{ updatedDateAgo }}
- </div>
- </div>
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/issues_list/components/issuables_list_app.vue b/app/assets/javascripts/issues_list/components/issuables_list_app.vue
deleted file mode 100644
index 71136bf0159..00000000000
--- a/app/assets/javascripts/issues_list/components/issuables_list_app.vue
+++ /dev/null
@@ -1,426 +0,0 @@
-<script>
-import {
- GlEmptyState,
- GlPagination,
- GlDeprecatedSkeletonLoading as GlSkeletonLoading,
- GlSafeHtmlDirective as SafeHtml,
-} from '@gitlab/ui';
-import { toNumber, omit } from 'lodash';
-import createFlash from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import { scrollToElement, historyPushState } from '~/lib/utils/common_utils';
-import { setUrlParams, queryToObject, getParameterByName } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
-import initManualOrdering from '~/issues/manual_ordering';
-import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import {
- sortOrderMap,
- availableSortOptionsJira,
- RELATIVE_POSITION,
- PAGE_SIZE,
- PAGE_SIZE_MANUAL,
- LOADING_LIST_ITEMS_LENGTH,
-} from '../constants';
-import issuableEventHub from '../eventhub';
-import { emptyStateHelper } from '../service_desk_helper';
-import Issuable from './issuable.vue';
-
-/**
- * @deprecated Use app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue instead
- */
-export default {
- LOADING_LIST_ITEMS_LENGTH,
- directives: {
- SafeHtml,
- },
- components: {
- GlEmptyState,
- GlPagination,
- GlSkeletonLoading,
- Issuable,
- FilteredSearchBar,
- },
- props: {
- canBulkEdit: {
- type: Boolean,
- required: false,
- default: false,
- },
- emptyStateMeta: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- projectPath: {
- type: String,
- required: false,
- default: '',
- },
- sortKey: {
- type: String,
- required: false,
- default: '',
- },
- type: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- availableSortOptionsJira,
- filters: {},
- isBulkEditing: false,
- issuables: [],
- loading: false,
- page: getParameterByName('page') !== null ? toNumber(getParameterByName('page')) : 1,
- selection: {},
- totalItems: 0,
- };
- },
- computed: {
- allIssuablesSelected() {
- // WARNING: Because we are only keeping track of selected values
- // this works, we will need to rethink this if we start tracking
- // [id]: false for not selected values.
- return this.issuables.length === Object.keys(this.selection).length;
- },
- emptyState() {
- if (this.issuables.length) {
- return {}; // Empty state shouldn't be shown here
- }
-
- if (this.isServiceDesk) {
- return emptyStateHelper(this.emptyStateMeta);
- }
-
- if (this.hasFilters) {
- return {
- title: __('Sorry, your filter produced no results'),
- svgPath: this.emptyStateMeta.svgPath,
- description: __('To widen your search, change or remove filters above'),
- primaryLink: this.emptyStateMeta.createIssuePath,
- primaryText: __('New issue'),
- };
- }
-
- if (this.filters.state === 'opened') {
- return {
- title: __('There are no open issues'),
- svgPath: this.emptyStateMeta.svgPath,
- description: __('To keep this project going, create a new issue'),
- primaryLink: this.emptyStateMeta.createIssuePath,
- primaryText: __('New issue'),
- };
- } else if (this.filters.state === 'closed') {
- return {
- title: __('There are no closed issues'),
- svgPath: this.emptyStateMeta.svgPath,
- };
- }
-
- return {
- title: __('There are no issues to show'),
- svgPath: this.emptyStateMeta.svgPath,
- description: __(
- 'The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project.',
- ),
- };
- },
- hasFilters() {
- const ignored = ['utf8', 'state', 'scope', 'order_by', 'sort'];
- return Object.keys(omit(this.filters, ignored)).length > 0;
- },
- isManualOrdering() {
- return this.sortKey === RELATIVE_POSITION;
- },
- itemsPerPage() {
- return this.isManualOrdering ? PAGE_SIZE_MANUAL : PAGE_SIZE;
- },
- baseUrl() {
- return window.location.href.replace(/(\?.*)?(#.*)?$/, '');
- },
- paginationNext() {
- return this.page + 1;
- },
- paginationPrev() {
- return this.page - 1;
- },
- paginationProps() {
- const paginationProps = { value: this.page };
-
- if (this.totalItems) {
- return {
- ...paginationProps,
- perPage: this.itemsPerPage,
- totalItems: this.totalItems,
- };
- }
-
- return {
- ...paginationProps,
- prevPage: this.paginationPrev,
- nextPage: this.paginationNext,
- };
- },
- isServiceDesk() {
- return this.type === 'service_desk';
- },
- isJira() {
- return this.type === 'jira';
- },
- initialFilterValue() {
- const value = [];
- const { search } = this.getQueryObject();
-
- if (search) {
- value.push(search);
- }
- return value;
- },
- initialSortBy() {
- const { sort } = this.getQueryObject();
- return sort || 'created_desc';
- },
- },
- watch: {
- selection() {
- // We need to call nextTick here to wait for all of the boxes to be checked and rendered
- // before we query the dom in issuable_bulk_update_actions.js.
- this.$nextTick(() => {
- issuableEventHub.$emit('issuables:updateBulkEdit');
- });
- },
- issuables() {
- this.$nextTick(() => {
- initManualOrdering();
- });
- },
- },
- mounted() {
- if (this.canBulkEdit) {
- this.unsubscribeToggleBulkEdit = issuableEventHub.$on('issuables:toggleBulkEdit', (val) => {
- this.isBulkEditing = val;
- });
- }
- this.fetchIssuables();
- },
- beforeDestroy() {
- // eslint-disable-next-line @gitlab/no-global-event-off
- issuableEventHub.$off('issuables:toggleBulkEdit');
- },
- methods: {
- isSelected(issuableId) {
- return Boolean(this.selection[issuableId]);
- },
- setSelection(ids) {
- ids.forEach((id) => {
- this.select(id, true);
- });
- },
- clearSelection() {
- this.selection = {};
- },
- select(id, isSelect = true) {
- if (isSelect) {
- this.$set(this.selection, id, true);
- } else {
- this.$delete(this.selection, id);
- }
- },
- fetchIssuables(pageToFetch) {
- this.loading = true;
-
- this.clearSelection();
-
- this.setFilters();
-
- return axios
- .get(this.endpoint, {
- params: {
- ...this.filters,
-
- with_labels_details: true,
- page: pageToFetch || this.page,
- per_page: this.itemsPerPage,
- },
- })
- .then((response) => {
- this.loading = false;
- this.issuables = response.data;
- this.totalItems = Number(response.headers['x-total']);
- this.page = Number(response.headers['x-page']);
- })
- .catch(() => {
- this.loading = false;
- return createFlash({
- message: __('An error occurred while loading issues'),
- });
- });
- },
- getQueryObject() {
- return queryToObject(window.location.search, { gatherArrays: true });
- },
- onPaginate(newPage) {
- if (newPage === this.page) return;
-
- scrollToElement('#content-body');
-
- // NOTE: This allows for the params to be updated on pagination
- historyPushState(
- setUrlParams({ ...this.filters, page: newPage }, window.location.href, true),
- );
-
- this.fetchIssuables(newPage);
- },
- onSelectAll() {
- if (this.allIssuablesSelected) {
- this.selection = {};
- } else {
- this.setSelection(this.issuables.map(({ id }) => id));
- }
- },
- onSelectIssuable({ issuable, selected }) {
- if (!this.canBulkEdit) return;
-
- this.select(issuable.id, selected);
- },
- setFilters() {
- const {
- label_name: labels,
- milestone_title: milestoneTitle,
- 'not[label_name]': excludedLabels,
- 'not[milestone_title]': excludedMilestone,
- ...filters
- } = this.getQueryObject();
-
- // TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/227880
-
- if (milestoneTitle) {
- filters.milestone = milestoneTitle;
- }
- if (Array.isArray(labels)) {
- filters.labels = labels.join(',');
- }
- if (!filters.state) {
- filters.state = 'opened';
- }
-
- if (excludedLabels) {
- filters['not[labels]'] = excludedLabels;
- }
-
- if (excludedMilestone) {
- filters['not[milestone]'] = excludedMilestone;
- }
-
- Object.assign(filters, sortOrderMap[this.sortKey]);
-
- this.filters = filters;
- },
- refetchIssuables() {
- const ignored = ['utf8'];
- const params = omit(this.filters, ignored);
-
- historyPushState(setUrlParams(params, window.location.href, true, true));
- this.fetchIssuables();
- },
- handleFilter(filters) {
- const searchTokens = [];
-
- filters.forEach((filter) => {
- if (filter.type === 'filtered-search-term') {
- if (filter.value.data) {
- searchTokens.push(filter.value.data);
- }
- }
- });
-
- if (searchTokens.length) {
- this.filters.search = searchTokens.join(' ');
- }
- this.page = 1;
-
- this.refetchIssuables();
- },
- handleSort(sort) {
- this.filters.sort = sort;
- this.page = 1;
-
- this.refetchIssuables();
- },
- },
-};
-</script>
-
-<template>
- <div>
- <filtered-search-bar
- v-if="isJira"
- :namespace="projectPath"
- :search-input-placeholder="__('Search Jira issues')"
- :tokens="[]"
- :sort-options="availableSortOptionsJira"
- :initial-filter-value="initialFilterValue"
- :initial-sort-by="initialSortBy"
- class="row-content-block"
- @onFilter="handleFilter"
- @onSort="handleSort"
- />
- <ul v-if="loading" class="content-list">
- <li v-for="n in $options.LOADING_LIST_ITEMS_LENGTH" :key="n" class="issue gl-px-5! gl-py-5!">
- <gl-skeleton-loading />
- </li>
- </ul>
- <div v-else-if="issuables.length">
- <div v-if="isBulkEditing" class="issue px-3 py-3 border-bottom border-light">
- <input
- id="check-all-issues"
- type="checkbox"
- :checked="allIssuablesSelected"
- class="mr-2"
- @click="onSelectAll"
- />
- <strong>{{ __('Select all') }}</strong>
- </div>
- <ul
- class="content-list issuable-list issues-list"
- :class="{ 'manual-ordering': isManualOrdering }"
- >
- <issuable
- v-for="issuable in issuables"
- :key="issuable.id"
- class="pr-3"
- :class="{ 'user-can-drag': isManualOrdering }"
- :issuable="issuable"
- :is-bulk-editing="isBulkEditing"
- :selected="isSelected(issuable.id)"
- :base-url="baseUrl"
- @select="onSelectIssuable"
- />
- </ul>
- <div class="mt-3">
- <gl-pagination
- v-bind="paginationProps"
- class="gl-justify-content-center"
- @input="onPaginate"
- />
- </div>
- </div>
- <gl-empty-state
- v-else
- :title="emptyState.title"
- :svg-path="emptyState.svgPath"
- :primary-button-link="emptyState.primaryLink"
- :primary-button-text="emptyState.primaryText"
- >
- <template #description>
- <div v-safe-html="emptyState.description"></div>
- </template>
- </gl-empty-state>
- </div>
-</template>
diff --git a/app/assets/javascripts/issues_list/service_desk_helper.js b/app/assets/javascripts/issues_list/service_desk_helper.js
deleted file mode 100644
index 815f338f1a0..00000000000
--- a/app/assets/javascripts/issues_list/service_desk_helper.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import { __, s__ } from '~/locale';
-
-/**
- * Generates empty state messages for Service Desk issues list.
- *
- * @param {emptyStateMeta} emptyStateMeta - Meta data used to generate empty state messages
- * @returns {Object} Object containing empty state messages generated using the meta data.
- */
-export function generateMessages(emptyStateMeta) {
- const {
- svgPath,
- serviceDeskHelpPage,
- serviceDeskAddress,
- editProjectPage,
- incomingEmailHelpPage,
- } = emptyStateMeta;
-
- const serviceDeskSupportedTitle = s__(
- 'ServiceDesk|Use Service Desk to connect with your users and offer customer support through email right inside GitLab',
- );
-
- const serviceDeskSupportedMessage = s__(
- 'ServiceDesk|Issues created from Service Desk emails will appear here. Each comment becomes part of the email conversation.',
- );
-
- const commonDescription = `
- <span>${serviceDeskSupportedMessage}</span>
- <a href="${serviceDeskHelpPage}">${__('Learn more.')}</a>`;
-
- return {
- serviceDeskEnabledAndCanEditProjectSettings: {
- title: serviceDeskSupportedTitle,
- svgPath,
- description: `<p>${s__('ServiceDesk|Your users can send emails to this address:')}
- <code>${serviceDeskAddress}</code>
- </p>
- ${commonDescription}`,
- },
- serviceDeskEnabledAndCannotEditProjectSettings: {
- title: serviceDeskSupportedTitle,
- svgPath,
- description: commonDescription,
- },
- serviceDeskDisabledAndCanEditProjectSettings: {
- title: serviceDeskSupportedTitle,
- svgPath,
- description: commonDescription,
- primaryLink: editProjectPage,
- primaryText: s__('ServiceDesk|Enable Service Desk'),
- },
- serviceDeskDisabledAndCannotEditProjectSettings: {
- title: serviceDeskSupportedTitle,
- svgPath,
- description: commonDescription,
- },
- serviceDeskIsNotSupported: {
- title: s__('ServiceDesk|Service Desk is not supported'),
- svgPath,
- description: s__(
- 'ServiceDesk|To enable Service Desk on this instance, an instance administrator must first set up incoming email.',
- ),
- primaryLink: incomingEmailHelpPage,
- primaryText: __('Learn more.'),
- },
- serviceDeskIsNotEnabled: {
- title: s__('ServiceDesk|Service Desk is not enabled'),
- svgPath,
- description: s__(
- 'ServiceDesk|For help setting up the Service Desk for your instance, please contact an administrator.',
- ),
- },
- };
-}
-
-/**
- * Returns the attributes used for gl-empty-state in the Service Desk issues list.
- *
- * @param {Object} emptyStateMeta - Meta data used to generate empty state messages
- * @returns {Object}
- */
-export function emptyStateHelper(emptyStateMeta) {
- const messages = generateMessages(emptyStateMeta);
-
- const { isServiceDeskSupported, canEditProjectSettings, isServiceDeskEnabled } = emptyStateMeta;
-
- if (isServiceDeskSupported) {
- if (isServiceDeskEnabled && canEditProjectSettings) {
- return messages.serviceDeskEnabledAndCanEditProjectSettings;
- }
-
- if (isServiceDeskEnabled && !canEditProjectSettings) {
- return messages.serviceDeskEnabledAndCannotEditProjectSettings;
- }
-
- // !isServiceDeskEnabled && canEditProjectSettings
- if (canEditProjectSettings) {
- return messages.serviceDeskDisabledAndCanEditProjectSettings;
- }
-
- // !isServiceDeskEnabled && !canEditProjectSettings
- return messages.serviceDeskDisabledAndCannotEditProjectSettings;
- }
-
- // !serviceDeskSupported && canEditProjectSettings
- if (canEditProjectSettings) {
- return messages.serviceDeskIsNotSupported;
- }
-
- // !serviceDeskSupported && !canEditProjectSettings
- return messages.serviceDeskIsNotEnabled;
-}
diff --git a/app/assets/javascripts/jira_import/utils/constants.js b/app/assets/javascripts/jira_import/utils/constants.js
index 178159be009..4230f85e443 100644
--- a/app/assets/javascripts/jira_import/utils/constants.js
+++ b/app/assets/javascripts/jira_import/utils/constants.js
@@ -1,5 +1,7 @@
import { __ } from '~/locale';
+export const JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY = 'jira-import-success-alert-hide-map';
+
export const debounceWait = 500;
export const dropdownLabel = __(
diff --git a/app/assets/javascripts/jira_import/utils/jira_import_utils.js b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
index 4e3b5b2fbde..bd83dd4d219 100644
--- a/app/assets/javascripts/jira_import/utils/jira_import_utils.js
+++ b/app/assets/javascripts/jira_import/utils/jira_import_utils.js
@@ -1,5 +1,5 @@
import { last } from 'lodash';
-import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from '~/issues_list/constants';
+import { JIRA_IMPORT_SUCCESS_ALERT_HIDE_MAP_KEY } from './constants';
export const IMPORT_STATE = {
FAILED: 'failed',
diff --git a/app/assets/javascripts/jobs/bridge/app.vue b/app/assets/javascripts/jobs/bridge/app.vue
index 67c22712776..c639e49083b 100644
--- a/app/assets/javascripts/jobs/bridge/app.vue
+++ b/app/assets/javascripts/jobs/bridge/app.vue
@@ -1,20 +1,118 @@
<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __, sprintf } from '~/locale';
+import CiHeader from '~/vue_shared/components/header_ci_component.vue';
+import getPipelineQuery from './graphql/queries/pipeline.query.graphql';
import BridgeEmptyState from './components/empty_state.vue';
import BridgeSidebar from './components/sidebar.vue';
+import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './components/constants';
export default {
name: 'BridgePageApp',
components: {
BridgeEmptyState,
BridgeSidebar,
+ CiHeader,
+ GlLoadingIcon,
+ },
+ inject: ['buildId', 'projectFullPath', 'pipelineIid'],
+ apollo: {
+ pipeline: {
+ query: getPipelineQuery,
+ variables() {
+ return {
+ fullPath: this.projectFullPath,
+ iid: this.pipelineIid,
+ };
+ },
+ update(data) {
+ if (!data?.project?.pipeline) {
+ return null;
+ }
+
+ const { pipeline } = data.project;
+ const stages = pipeline?.stages.edges.map((edge) => edge.node) || [];
+ const jobs = stages.map((stage) => stage.jobs.nodes).flat();
+
+ return {
+ ...pipeline,
+ commit: {
+ ...pipeline.commit,
+ commit_path: pipeline.commit.webPath,
+ short_id: pipeline.commit.shortId,
+ },
+ id: getIdFromGraphQLId(pipeline.id),
+ jobs,
+ stages,
+ };
+ },
+ },
+ },
+ data() {
+ return {
+ isSidebarExpanded: true,
+ pipeline: {},
+ };
+ },
+ computed: {
+ bridgeJob() {
+ return (
+ this.pipeline.jobs?.filter(
+ (job) => getIdFromGraphQLId(job.id) === Number(this.buildId),
+ )[0] || {}
+ );
+ },
+ bridgeName() {
+ return sprintf(__('Job %{jobName}'), { jobName: this.bridgeJob.name });
+ },
+ isPipelineLoading() {
+ return this.$apollo.queries.pipeline.loading;
+ },
+ },
+ created() {
+ window.addEventListener('resize', this.onResize);
+ },
+ mounted() {
+ this.onResize();
+ },
+ methods: {
+ toggleSidebar() {
+ this.isSidebarExpanded = !this.isSidebarExpanded;
+ },
+ onResize() {
+ const breakpoint = bp.getBreakpointSize();
+ if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
+ this.isSidebarExpanded = false;
+ } else if (!this.isSidebarExpanded) {
+ this.isSidebarExpanded = true;
+ }
+ },
},
};
</script>
<template>
<div>
- <!-- TODO: get job details and show CI header -->
- <!-- TODO: add downstream pipeline path -->
- <bridge-empty-state downstream-pipeline-path="#" />
- <bridge-sidebar />
+ <gl-loading-icon v-if="isPipelineLoading" size="lg" class="gl-mt-4" />
+ <div v-else>
+ <ci-header
+ class="gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
+ :status="bridgeJob.detailedStatus"
+ :time="bridgeJob.createdAt"
+ :user="pipeline.user"
+ :has-sidebar-button="true"
+ :item-name="bridgeName"
+ @clickedSidebarButton="toggleSidebar"
+ />
+ <bridge-empty-state :downstream-pipeline-path="bridgeJob.downstreamPipeline.path" />
+ <bridge-sidebar
+ v-if="isSidebarExpanded"
+ :bridge-job="bridgeJob"
+ :commit="pipeline.commit"
+ :is-sidebar-expanded="isSidebarExpanded"
+ @toggleSidebar="toggleSidebar"
+ />
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/bridge/components/sidebar.vue b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
index 68b767408f0..3ba07cf55d1 100644
--- a/app/assets/javascripts/jobs/bridge/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/bridge/components/sidebar.vue
@@ -1,14 +1,13 @@
<script>
import { GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { __ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import { JOB_SIDEBAR } from '../../constants';
-import { SIDEBAR_COLLAPSE_BREAKPOINTS } from './constants';
+import CommitBlock from '../../components/commit_block.vue';
export default {
styles: {
- top: '75px',
width: '290px',
},
name: 'BridgeSidebar',
@@ -18,40 +17,47 @@ export default {
retryTriggerJob: __('Retry the trigger job'),
retryDownstreamPipeline: __('Retry the downstream pipeline'),
},
- borderTopClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100'],
+ sectionClass: ['gl-border-t-solid', 'gl-border-t-1', 'gl-border-t-gray-100', 'gl-py-5'],
components: {
+ CommitBlock,
GlButton,
GlDropdown,
GlDropdownItem,
TooltipOnTruncate,
},
- inject: {
- buildName: {
- type: String,
- default: '',
+ mixins: [glFeatureFlagsMixin()],
+ props: {
+ bridgeJob: {
+ type: Object,
+ required: true,
+ },
+ commit: {
+ type: Object,
+ required: true,
},
},
data() {
return {
- isSidebarExpanded: true,
+ topPosition: 0,
};
},
- created() {
- window.addEventListener('resize', this.onResize);
+ computed: {
+ rootStyle() {
+ return { ...this.$options.styles, top: `${this.topPosition}px` };
+ },
},
mounted() {
- this.onResize();
+ this.setTopPosition();
},
methods: {
- toggleSidebar() {
- this.isSidebarExpanded = !this.isSidebarExpanded;
+ onSidebarButtonClick() {
+ this.$emit('toggleSidebar');
},
- onResize() {
- const breakpoint = bp.getBreakpointSize();
- if (SIDEBAR_COLLAPSE_BREAKPOINTS.includes(breakpoint)) {
- this.isSidebarExpanded = false;
- } else if (!this.isSidebarExpanded) {
- this.isSidebarExpanded = true;
+ setTopPosition() {
+ const navbarEl = document.querySelector('.js-navbar');
+
+ if (navbarEl) {
+ this.topPosition = navbarEl.getBoundingClientRect().bottom;
}
},
},
@@ -60,19 +66,19 @@ export default {
<template>
<aside
class="gl-fixed gl-right-0 gl-px-5 gl-bg-gray-10 gl-h-full gl-border-l-solid gl-border-1 gl-border-gray-100 gl-z-index-200 gl-overflow-hidden"
- :style="this.$options.styles"
- :class="{
- 'gl-display-none': !isSidebarExpanded,
- }"
+ :style="rootStyle"
>
<div class="gl-py-5 gl-display-flex gl-align-items-center">
- <tooltip-on-truncate :title="buildName" truncate-target="child"
+ <tooltip-on-truncate :title="bridgeJob.name" truncate-target="child"
><h4 class="gl-mb-0 gl-mr-2 gl-text-truncate">
- {{ buildName }}
+ {{ bridgeJob.name }}
</h4>
</tooltip-on-truncate>
<!-- TODO: implement retry actions -->
- <div class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right">
+ <div
+ v-if="glFeatures.triggerJobRetryAction"
+ class="gl-flex-grow-1 gl-flex-shrink-0 gl-text-right"
+ >
<gl-dropdown
:text="$options.i18n.retryButton"
category="primary"
@@ -90,9 +96,10 @@ export default {
category="tertiary"
class="gl-md-display-none gl-ml-2"
icon="chevron-double-lg-right"
- @click="toggleSidebar"
+ @click="onSidebarButtonClick"
/>
</div>
- <!-- TODO: get job details and show commit block, stage dropdown, jobs list -->
+ <commit-block :commit="commit" :class="$options.sectionClass" />
+ <!-- TODO: show stage dropdown, jobs list -->
</aside>
</template>
diff --git a/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql b/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql
new file mode 100644
index 00000000000..338ca9f16c7
--- /dev/null
+++ b/app/assets/javascripts/jobs/bridge/graphql/queries/pipeline.query.graphql
@@ -0,0 +1,70 @@
+query getPipelineData($fullPath: ID!, $iid: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ pipeline(iid: $iid) {
+ id
+ iid
+ path
+ sha
+ ref
+ refPath
+ commit {
+ id
+ shortId
+ title
+ webPath
+ }
+ detailedStatus {
+ id
+ icon
+ group
+ }
+ stages {
+ edges {
+ node {
+ id
+ name
+ jobs {
+ nodes {
+ id
+ createdAt
+ name
+ scheduledAt
+ startedAt
+ status
+ triggered
+ detailedStatus {
+ id
+ detailsPath
+ icon
+ group
+ text
+ tooltip
+ }
+ downstreamPipeline {
+ id
+ path
+ }
+ stage {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ user {
+ id
+ avatarUrl
+ name
+ username
+ webPath
+ webUrl
+ status {
+ message
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
index 97141a27a5e..8e35fd91481 100644
--- a/app/assets/javascripts/jobs/components/job_log_controllers.vue
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -107,6 +107,7 @@ export default {
:data-confirm="__('Are you sure you want to erase this build?')"
class="gl-ml-3"
data-testid="job-log-erase-link"
+ data-confirm-btn-variant="danger"
data-method="post"
icon="remove"
/>
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 5451cd21c14..5428f657252 100644
--- a/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/jobs/components/sidebar_job_details_container.vue
@@ -1,5 +1,6 @@
<script>
import { mapState } from 'vuex';
+import { GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
import { __, sprintf } from '~/locale';
@@ -10,6 +11,7 @@ export default {
name: 'JobSidebarDetailsContainer',
components: {
DetailRow,
+ GlBadge,
},
mixins: [timeagoMixin],
computed: {
@@ -100,12 +102,7 @@ export default {
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ __('Tags:') }}</span>
- <span
- v-for="(tag, i) in job.tags"
- :key="i"
- class="badge badge-pill badge-primary gl-badge sm"
- >{{ tag }}</span
- >
+ <gl-badge v-for="(tag, i) in job.tags" :key="i" variant="info">{{ tag }}</gl-badge>
</p>
</div>
</template>
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index e078a6c2319..6e958ea1842 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -54,7 +54,13 @@ const initializeJobPage = (element) => {
};
const initializeBridgePage = (el) => {
- const { buildName, emptyStateIllustrationPath } = el.dataset;
+ const {
+ buildId,
+ downstreamPipelinePath,
+ emptyStateIllustrationPath,
+ pipelineIid,
+ projectFullPath,
+ } = el.dataset;
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
@@ -65,8 +71,11 @@ const initializeBridgePage = (el) => {
el,
apolloProvider,
provide: {
- buildName,
+ buildId,
+ downstreamPipelinePath,
emptyStateIllustrationPath,
+ pipelineIid,
+ projectFullPath,
},
render(h) {
return h(BridgeApp);
diff --git a/app/assets/javascripts/labels/components/delete_label_modal.vue b/app/assets/javascripts/labels/components/delete_label_modal.vue
index 1ff0938d086..2be404de1e1 100644
--- a/app/assets/javascripts/labels/components/delete_label_modal.vue
+++ b/app/assets/javascripts/labels/components/delete_label_modal.vue
@@ -56,6 +56,7 @@ export default {
</gl-sprintf>
</template>
<gl-sprintf
+ v-if="subjectName"
:message="
__(
`%{strongStart}${labelName}%{strongEnd} will be permanently deleted from ${subjectName}. This cannot be undone.`,
@@ -66,6 +67,18 @@ export default {
<strong>{{ content }}</strong>
</template>
</gl-sprintf>
+ <gl-sprintf
+ v-else
+ :message="
+ __(
+ `%{strongStart}${labelName}%{strongEnd} will be permanently deleted. This cannot be undone.`,
+ )
+ "
+ >
+ <template #strong="{ content }">
+ <strong>{{ content }}</strong>
+ </template>
+ </gl-sprintf>
<template #modal-footer>
<gl-button category="secondary" @click="closeModal">{{ __('Cancel') }}</gl-button>
<gl-button
diff --git a/app/assets/javascripts/labels/create_label_dropdown.js b/app/assets/javascripts/labels/create_label_dropdown.js
index 8c166158a44..033ca9dd3ea 100644
--- a/app/assets/javascripts/labels/create_label_dropdown.js
+++ b/app/assets/javascripts/labels/create_label_dropdown.js
@@ -16,6 +16,7 @@ export default class CreateLabelDropdown {
this.$colorPreview = $('.js-dropdown-label-color-preview', this.$el);
this.$addList = $('.js-add-list', this.$el);
this.$newLabelError = $('.js-label-error', this.$el);
+ this.$newLabelErrorContent = $('.gl-alert-content', this.$newLabelError);
this.$newLabelCreateButton = $('.js-new-label-btn', this.$el);
this.$colorSuggestions = $('.suggest-colors-dropdown a', this.$el);
@@ -119,7 +120,8 @@ export default class CreateLabelDropdown {
.join('<br/>');
}
- this.$newLabelError.html(errors).show();
+ this.$newLabelErrorContent.html(errors);
+ this.$newLabelError.show();
} else {
const addNewList = this.$addList.is(':checked');
this.$dropdownBack.trigger('click');
diff --git a/app/assets/javascripts/labels/index.js b/app/assets/javascripts/labels/index.js
index 22a9c0a89c0..e87ad8d9a06 100644
--- a/app/assets/javascripts/labels/index.js
+++ b/app/assets/javascripts/labels/index.js
@@ -26,7 +26,7 @@ export function initLabels() {
if ($('.prioritized-labels').length) {
new LabelManager(); // eslint-disable-line no-new
}
- $('.label-subscription').each((i, el) => {
+ $('.js-label-subscription').each((i, el) => {
const $el = $(el);
if ($el.find('.dropdown-group-label').length) {
diff --git a/app/assets/javascripts/lib/mermaid.js b/app/assets/javascripts/lib/mermaid.js
new file mode 100644
index 00000000000..d621c9ddf9e
--- /dev/null
+++ b/app/assets/javascripts/lib/mermaid.js
@@ -0,0 +1,61 @@
+import mermaid from 'mermaid';
+import { getParameterByName } from '~/lib/utils/url_utility';
+
+const setIframeRenderedSize = (h, w) => {
+ const { origin } = window.location;
+ window.parent.postMessage({ h, w }, origin);
+};
+
+const drawDiagram = (source) => {
+ const element = document.getElementById('app');
+ const insertSvg = (svgCode) => {
+ element.innerHTML = svgCode;
+
+ const height = parseInt(element.firstElementChild.getAttribute('height'), 10);
+ const width = parseInt(element.firstElementChild.style.maxWidth, 10);
+ setIframeRenderedSize(height, width);
+ };
+ mermaid.mermaidAPI.render('mermaid', source, insertSvg);
+};
+
+const darkModeEnabled = () => getParameterByName('darkMode') === 'true';
+
+const initMermaid = () => {
+ let theme = 'neutral';
+
+ if (darkModeEnabled()) {
+ theme = 'dark';
+ }
+
+ mermaid.initialize({
+ // mermaid core options
+ mermaid: {
+ startOnLoad: false,
+ },
+ // mermaidAPI options
+ theme,
+ flowchart: {
+ useMaxWidth: true,
+ htmlLabels: true,
+ },
+ secure: ['secure', 'securityLevel', 'startOnLoad', 'maxTextSize', 'htmlLabels'],
+ securityLevel: 'strict',
+ });
+};
+
+const addListener = () => {
+ window.addEventListener(
+ 'message',
+ (event) => {
+ if (event.origin !== window.location.origin) {
+ return;
+ }
+ drawDiagram(event.data);
+ },
+ false,
+ );
+};
+
+addListener();
+initMermaid();
+export default {};
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 7235b38848c..eff00dff7a7 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -181,6 +181,7 @@ export const contentTop = () => {
},
() => getOuterHeight('.merge-request-tabs'),
() => getOuterHeight('.js-diff-files-changed'),
+ () => getOuterHeight('.issue-sticky-header.gl-fixed'),
({ desktop }) => {
const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
let size;
@@ -746,3 +747,12 @@ export const isLoggedIn = () => Boolean(window.gon?.current_user_id);
*/
export const convertArrayOfObjectsToCamelCase = (array) =>
array.map((o) => convertObjectPropsToCamelCase(o));
+
+export const getFirstPropertyValue = (data) => {
+ if (!data) return null;
+
+ const [key] = Object.keys(data);
+ if (!key) return null;
+
+ return data[key];
+};
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index a108b02bcbf..36c6545164e 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -17,7 +17,6 @@ export const BV_HIDE_MODAL = 'bv::hide::modal';
export const BV_HIDE_TOOLTIP = 'bv::hide::tooltip';
export const BV_DROPDOWN_SHOW = 'bv::dropdown::show';
export const BV_DROPDOWN_HIDE = 'bv::dropdown::hide';
-export const BV_COLLAPSE_STATE = 'bv::collapse::state';
export const DEFAULT_TH_CLASSES =
'gl-bg-transparent! gl-border-b-solid! gl-border-b-gray-100! gl-p-5! gl-border-b-1!';
diff --git a/app/assets/javascripts/lib/utils/resize_observer.js b/app/assets/javascripts/lib/utils/resize_observer.js
new file mode 100644
index 00000000000..e72c6fe1679
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/resize_observer.js
@@ -0,0 +1,58 @@
+import { contentTop } from './common_utils';
+
+const interactionEvents = ['mousedown', 'touchstart', 'keydown', 'wheel'];
+
+export function createResizeObserver() {
+ return new ResizeObserver((entries) => {
+ entries.forEach((entry) => {
+ entry.target.dispatchEvent(new CustomEvent(`ResizeUpdate`, { detail: { entry } }));
+ });
+ });
+}
+
+// watches for change in size of a container element (e.g. for lazy-loaded images)
+// and scroll the target element to the top of the content area
+// stop watching after any user input. So if user opens sidebar or manually
+// scrolls the page we don't hijack their scroll position
+export function scrollToTargetOnResize({
+ target = window.location.hash,
+ container = '#content-body',
+} = {}) {
+ if (!target) return null;
+
+ const ro = createResizeObserver();
+ const containerEl = document.querySelector(container);
+ let interactionListenersAdded = false;
+
+ function keepTargetAtTop() {
+ const anchorEl = document.querySelector(target);
+
+ if (!anchorEl) return;
+
+ const anchorTop = anchorEl.getBoundingClientRect().top + window.scrollY;
+ const top = anchorTop - contentTop();
+ document.documentElement.scrollTo({
+ top,
+ });
+
+ if (!interactionListenersAdded) {
+ interactionEvents.forEach((event) =>
+ // eslint-disable-next-line no-use-before-define
+ document.addEventListener(event, removeListeners),
+ );
+ interactionListenersAdded = true;
+ }
+ }
+
+ function removeListeners() {
+ interactionEvents.forEach((event) => document.removeEventListener(event, removeListeners));
+
+ ro.unobserve(containerEl);
+ containerEl.removeEventListener('ResizeUpdate', keepTargetAtTop);
+ }
+
+ containerEl.addEventListener('ResizeUpdate', keepTargetAtTop);
+
+ ro.observe(containerEl);
+ return ro;
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index e221a54d9c6..376134afef0 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -101,6 +101,21 @@ function deferredInitialisation() {
initFeatureHighlight();
initCopyCodeButton();
+ const helpToggle = document.querySelector('.header-help-dropdown-toggle');
+ if (helpToggle) {
+ helpToggle.addEventListener(
+ 'click',
+ () => {
+ import(/* webpackChunkName: 'versionCheck' */ './gitlab_version_check')
+ .then(({ default: initGitlabVersionCheck }) => {
+ initGitlabVersionCheck();
+ })
+ .catch(() => {});
+ },
+ { once: true },
+ );
+ }
+
const search = document.querySelector('#search');
if (search) {
search.addEventListener(
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index de733ae75df..e09d16cf680 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -6,6 +6,7 @@ import { canOverride, canRemove, canResend, canUpdate } from 'ee_else_ce/members
import { mergeUrlParams } from '~/lib/utils/url_utility';
import initUserPopovers from '~/user_popovers';
import {
+ FIELD_KEY_ACTIONS,
FIELDS,
ACTIVE_TAB_QUERY_PARAM_NAME,
TAB_QUERY_PARAM_VALUES,
@@ -63,17 +64,10 @@ export default {
return state[this.namespace].pagination;
},
}),
- filteredFields() {
+ filteredAndModifiedFields() {
return FIELDS.filter(
(field) => this.tableFields.includes(field.key) && this.showField(field),
- ).map((field) => {
- const tdClassFunction = this[field.tdClassFunction];
-
- return {
- ...field,
- ...(tdClassFunction && { tdClass: tdClassFunction }),
- };
- });
+ ).map(this.modifyFieldDefinition);
},
userIsLoggedIn() {
return this.currentUserId !== null;
@@ -100,20 +94,29 @@ export default {
);
},
showField(field) {
- if (!Object.prototype.hasOwnProperty.call(field, 'showFunction')) {
- return true;
- }
+ switch (field.key) {
+ case FIELD_KEY_ACTIONS:
+ if (!this.userIsLoggedIn) {
+ return false;
+ }
- return this[field.showFunction]();
+ return this.members.some((member) => this.hasActionButtons(member));
+ default:
+ return true;
+ }
},
- showActionsField() {
- if (!this.userIsLoggedIn) {
- return false;
+ modifyFieldDefinition(field) {
+ switch (field.key) {
+ case FIELD_KEY_ACTIONS:
+ return {
+ ...field,
+ tdClass: this.actionsFieldTdClass,
+ };
+ default:
+ return field;
}
-
- return this.members.some((member) => this.hasActionButtons(member));
},
- tdClassActions(value, key, member) {
+ actionsFieldTdClass(value, key, member) {
if (this.hasActionButtons(member)) {
return 'col-actions';
}
@@ -219,7 +222,7 @@ export default {
data-testid="members-table"
head-variant="white"
stacked="lg"
- :fields="filteredFields"
+ :fields="filteredAndModifiedFields"
:items="members"
primary-key="id"
thead-class="border-bottom"
diff --git a/app/assets/javascripts/members/constants.js b/app/assets/javascripts/members/constants.js
index f5ca881ab0d..62241eaed04 100644
--- a/app/assets/javascripts/members/constants.js
+++ b/app/assets/javascripts/members/constants.js
@@ -1,8 +1,18 @@
import { __ } from '~/locale';
+export const FIELD_KEY_ACCOUNT = 'account';
+export const FIELD_KEY_SOURCE = 'source';
+export const FIELD_KEY_GRANTED = 'granted';
+export const FIELD_KEY_INVITED = 'invited';
+export const FIELD_KEY_REQUESTED = 'requested';
+export const FIELD_KEY_MAX_ROLE = 'maxRole';
+export const FIELD_KEY_EXPIRATION = 'expiration';
+export const FIELD_KEY_LAST_SIGN_IN = 'lastSignIn';
+export const FIELD_KEY_ACTIONS = 'actions';
+
export const FIELDS = [
{
- key: 'account',
+ key: FIELD_KEY_ACCOUNT,
label: __('Account'),
sort: {
asc: 'name_asc',
@@ -10,13 +20,13 @@ export const FIELDS = [
},
},
{
- key: 'source',
+ key: FIELD_KEY_SOURCE,
label: __('Source'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
- key: 'granted',
+ key: FIELD_KEY_GRANTED,
label: __('Access granted'),
thClass: 'col-meta',
tdClass: 'col-meta',
@@ -26,19 +36,19 @@ export const FIELDS = [
},
},
{
- key: 'invited',
+ key: FIELD_KEY_INVITED,
label: __('Invited'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
- key: 'requested',
+ key: FIELD_KEY_REQUESTED,
label: __('Requested'),
thClass: 'col-meta',
tdClass: 'col-meta',
},
{
- key: 'maxRole',
+ key: FIELD_KEY_MAX_ROLE,
label: __('Max role'),
thClass: 'col-max-role',
tdClass: 'col-max-role',
@@ -48,13 +58,13 @@ export const FIELDS = [
},
},
{
- key: 'expiration',
+ key: FIELD_KEY_EXPIRATION,
label: __('Expiration'),
thClass: 'col-expiration',
tdClass: 'col-expiration',
},
{
- key: 'lastSignIn',
+ key: FIELD_KEY_LAST_SIGN_IN,
label: __('Last sign-in'),
sort: {
asc: 'recent_sign_in',
@@ -62,10 +72,8 @@ export const FIELDS = [
},
},
{
- key: 'actions',
+ key: FIELD_KEY_ACTIONS,
thClass: 'col-actions',
- showFunction: 'showActionsField',
- tdClassFunction: 'tdClassActions',
},
];
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index d988ad8d8ca..29c181f04fb 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
@@ -143,6 +143,7 @@ export default {
</template>
<template #right-actions>
<gl-dropdown
+ v-if="!deleteButtonDisabled"
icon="ellipsis_v"
text="More actions"
:text-sr-only="true"
@@ -150,11 +151,7 @@ export default {
no-caret
right
>
- <gl-dropdown-item
- variant="danger"
- :disabled="deleteButtonDisabled"
- @click="$emit('delete')"
- >
+ <gl-dropdown-item variant="danger" @click="$emit('delete')">
{{ __('Delete image repository') }}
</gl-dropdown-item>
</gl-dropdown>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
deleted file mode 100644
index a16d95a6b30..00000000000
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/empty_state.vue
+++ /dev/null
@@ -1,44 +0,0 @@
-<script>
-import { GlEmptyState } from '@gitlab/ui';
-import {
- NO_TAGS_TITLE,
- NO_TAGS_MESSAGE,
- MISSING_OR_DELETED_IMAGE_TITLE,
- MISSING_OR_DELETED_IMAGE_MESSAGE,
-} from '../../constants/index';
-
-export default {
- components: {
- GlEmptyState,
- },
- props: {
- noContainersImage: {
- type: String,
- required: false,
- default: '',
- },
- isEmptyImage: {
- type: Boolean,
- default: false,
- required: false,
- },
- },
- computed: {
- title() {
- return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_TITLE : NO_TAGS_TITLE;
- },
- description() {
- return this.isEmptyImage ? MISSING_OR_DELETED_IMAGE_MESSAGE : NO_TAGS_MESSAGE;
- },
- },
-};
-</script>
-
-<template>
- <gl-empty-state
- :title="title"
- :svg-path="noContainersImage"
- :description="description"
- class="gl-mx-auto gl-my-0"
- />
-</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
index 2d32295b537..4fda4058711 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list.vue
@@ -1,28 +1,38 @@
<script>
+import { GlEmptyState } from '@gitlab/ui';
import createFlash from '~/flash';
import { n__ } from '~/locale';
import { joinPaths } from '~/lib/utils/url_utility';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+
+import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
+import { FILTERED_SEARCH_TERM } from '~/packages_and_registries/shared/constants';
import {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
GRAPHQL_PAGE_SIZE,
FETCH_IMAGES_LIST_ERROR_MESSAGE,
+ NAME_SORT_FIELD,
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ NO_TAGS_MATCHING_FILTERS_TITLE,
+ NO_TAGS_MATCHING_FILTERS_DESCRIPTION,
} from '../../constants/index';
import getContainerRepositoryTagsQuery from '../../graphql/queries/get_container_repository_tags.query.graphql';
-import EmptyState from './empty_state.vue';
import TagsListRow from './tags_list_row.vue';
import TagsLoader from './tags_loader.vue';
export default {
name: 'TagsList',
components: {
+ GlEmptyState,
TagsListRow,
- EmptyState,
TagsLoader,
RegistryList,
+ PersistedSearch,
},
inject: ['config'],
+
props: {
id: {
type: [Number, String],
@@ -44,6 +54,7 @@ export default {
required: false,
},
},
+ searchConfig: { NAME_SORT_FIELD },
i18n: {
REMOVE_TAGS_BUTTON_TITLE,
TAGS_LIST_TITLE,
@@ -51,6 +62,9 @@ export default {
apollo: {
containerRepository: {
query: getContainerRepositoryTagsQuery,
+ skip() {
+ return !this.sort;
+ },
variables() {
return this.queryVariables;
},
@@ -62,6 +76,8 @@ export default {
data() {
return {
containerRepository: {},
+ filters: {},
+ sort: null,
};
},
computed: {
@@ -78,6 +94,8 @@ export default {
return {
id: joinPaths(this.config.gidPrefix, `${this.id}`),
first: GRAPHQL_PAGE_SIZE,
+ name: this.filters?.name,
+ sort: this.sort,
};
},
showMultiDeleteButton() {
@@ -87,7 +105,16 @@ export default {
return this.tags.length === 0;
},
isLoading() {
- return this.isImageLoading || this.$apollo.queries.containerRepository.loading;
+ return this.isImageLoading || this.$apollo.queries.containerRepository.loading || !this.sort;
+ },
+ hasFilters() {
+ return this.filters?.name;
+ },
+ emptyStateTitle() {
+ return this.hasFilters ? NO_TAGS_MATCHING_FILTERS_TITLE : NO_TAGS_TITLE;
+ },
+ emptyStateDescription() {
+ return this.hasFilters ? NO_TAGS_MATCHING_FILTERS_DESCRIPTION : NO_TAGS_MESSAGE;
},
},
methods: {
@@ -114,15 +141,47 @@ export default {
},
});
},
+ handleSearchUpdate({ sort, filters }) {
+ this.sort = sort;
+
+ const parsed = {
+ name: '',
+ };
+
+ // This takes in account the fact that we will be adding more filters types
+ // this is why is an object and not an array or a simple string
+ this.filters = filters.reduce((acc, filter) => {
+ if (filter.type === FILTERED_SEARCH_TERM) {
+ return {
+ ...acc,
+ name: `${acc.name} ${filter.value.data}`.trim(),
+ };
+ }
+ return acc;
+ }, parsed);
+ },
},
};
</script>
<template>
<div>
+ <persisted-search
+ class="gl-mb-5"
+ :sortable-fields="[$options.searchConfig.NAME_SORT_FIELD]"
+ :default-order="$options.searchConfig.NAME_SORT_FIELD.orderBy"
+ default-sort="asc"
+ @update="handleSearchUpdate"
+ />
<tags-loader v-if="isLoading" />
<template v-else>
- <empty-state v-if="hasNoTags" :no-containers-image="config.noContainersImage" />
+ <gl-empty-state
+ v-if="hasNoTags"
+ :title="emptyStateTitle"
+ :svg-path="config.noContainersImage"
+ :description="emptyStateDescription"
+ class="gl-mx-auto gl-my-0"
+ />
<template v-else>
<registry-list
:title="listTitle"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
index 0556fd298aa..15d92ab0ef7 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue
@@ -107,11 +107,8 @@ export default {
isInvalidTag() {
return !this.tag.digest;
},
- isCheckboxDisabled() {
- return this.isInvalidTag || this.disabled;
- },
isDeleteDisabled() {
- return this.isInvalidTag || this.disabled || !this.tag.canDelete;
+ return this.disabled || !this.tag.canDelete;
},
},
};
@@ -122,7 +119,7 @@ export default {
<template #left-action>
<gl-form-checkbox
v-if="tag.canDelete"
- :disabled="isCheckboxDisabled"
+ :disabled="disabled"
class="gl-m-0"
:checked="selected"
@change="$emit('select')"
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
index f7beec2c935..17adaec7a7d 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/common.js
@@ -2,3 +2,5 @@ import { s__, __ } from '~/locale';
export const ROOT_IMAGE_TEXT = s__('ContainerRegistry|Root image');
export const MORE_ACTIONS_TEXT = __('More actions');
+
+export const NAME_SORT_FIELD = { orderBy: 'NAME', label: __('Name') };
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
index 19e1a75fb2f..8b8769a884d 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/details.js
@@ -116,6 +116,13 @@ export const ROOT_IMAGE_TOOLTIP = s__(
'ContainerRegistry|Image repository with no name located at the project URL.',
);
+export const NO_TAGS_MATCHING_FILTERS_TITLE = s__(
+ 'ContainerRegistry|The filter returned no results',
+);
+export const NO_TAGS_MATCHING_FILTERS_DESCRIPTION = s__(
+ 'ContainerRegistry|Please try different search criteria',
+);
+
// Parameters
export const DEFAULT_PAGE = 1;
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
index d21a154d1b8..7fa950ccfd0 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/constants/list.js
@@ -1,4 +1,5 @@
import { s__, __ } from '~/locale';
+import { NAME_SORT_FIELD } from './common';
// Translations strings
@@ -49,5 +50,5 @@ export const GRAPHQL_PAGE_SIZE = 10;
export const SORT_FIELDS = [
{ orderBy: 'UPDATED', label: __('Updated') },
{ orderBy: 'CREATED', label: __('Created') },
- { orderBy: 'NAME', label: __('Name') },
+ NAME_SORT_FIELD,
];
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
index 502382010f9..d753d33a02c 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/graphql/queries/get_container_repository_tags.query.graphql
@@ -6,11 +6,13 @@ query getContainerRepositoryTags(
$last: Int
$after: String
$before: String
+ $name: String
+ $sort: ContainerRepositoryTagSort
) {
containerRepository(id: $id) {
id
tagsCount
- tags(after: $after, before: $before, first: $first, last: $last) {
+ tags(after: $after, before: $before, first: $first, last: $last, name: $name, sort: $sort) {
nodes {
digest
location
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
index 246a6768593..ca5bd8d6964 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/index.js
@@ -3,7 +3,8 @@ import Vue from 'vue';
import { parseBoolean } from '~/lib/utils/common_utils';
import PerformancePlugin from '~/performance/vue_performance_plugin';
import Translate from '~/vue_shared/translate';
-import RegistryBreadcrumb from './components/registry_breadcrumb.vue';
+import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
+import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
import { apolloProvider } from './graphql/index';
import RegistryExplorer from './pages/index.vue';
import createRouter from './router';
@@ -84,38 +85,8 @@ export default () => {
},
});
- const attachBreadcrumb = () => {
- const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
- const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
- const crumbs = [breadCrumbEl.querySelector('h2')];
- const nestedBreadcrumbEl = document.createElement('div');
- breadCrumbEl.replaceChild(nestedBreadcrumbEl, breadCrumbEl.querySelector('h2'));
- return new Vue({
- el: nestedBreadcrumbEl,
- router,
- apolloProvider,
- components: {
- RegistryBreadcrumb,
- },
- render(createElement) {
- // FIXME(@tnir): this is a workaround until the MR gets merged:
- // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
- const parentEl = breadCrumbEl.parentElement.parentElement;
- if (parentEl) {
- parentEl.classList.remove('breadcrumbs-container');
- parentEl.classList.add('gl-display-flex');
- parentEl.classList.add('w-100');
- }
- // End of FIXME(@tnir)
- return createElement('registry-breadcrumb', {
- class: breadCrumbEl.className,
- props: {
- crumbs,
- },
- });
- },
- });
+ return {
+ attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachMainComponent,
};
-
- return { attachBreadcrumb, attachMainComponent };
};
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
index bc6e3091f0e..bb687ffdb89 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/pages/details.vue
@@ -1,5 +1,5 @@
<script>
-import { GlResizeObserverDirective } from '@gitlab/ui';
+import { GlResizeObserverDirective, GlEmptyState } from '@gitlab/ui';
import { GlBreakpointInstance } from '@gitlab/ui/dist/utils';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
@@ -9,7 +9,6 @@ import DeleteImage from '../components/delete_image.vue';
import DeleteAlert from '../components/details_page/delete_alert.vue';
import DeleteModal from '../components/details_page/delete_modal.vue';
import DetailsHeader from '../components/details_page/details_header.vue';
-import EmptyState from '../components/details_page/empty_state.vue';
import PartialCleanupAlert from '../components/details_page/partial_cleanup_alert.vue';
import StatusAlert from '../components/details_page/status_alert.vue';
import TagsList from '../components/details_page/tags_list.vue';
@@ -26,6 +25,8 @@ import {
MISSING_OR_DELETED_IMAGE_BREADCRUMB,
ROOT_IMAGE_TEXT,
GRAPHQL_PAGE_SIZE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
} from '../constants/index';
import deleteContainerRepositoryTagsMutation from '../graphql/mutations/delete_container_repository_tags.mutation.graphql';
import getContainerRepositoryDetailsQuery from '../graphql/queries/get_container_repository_details.query.graphql';
@@ -34,13 +35,13 @@ import getContainerRepositoryTagsQuery from '../graphql/queries/get_container_re
export default {
name: 'RegistryDetailsPage',
components: {
+ GlEmptyState,
DeleteAlert,
PartialCleanupAlert,
DetailsHeader,
DeleteModal,
TagsList,
TagsLoader,
- EmptyState,
StatusAlert,
DeleteImage,
},
@@ -49,6 +50,10 @@ export default {
},
mixins: [Tracking.mixin()],
inject: ['breadCrumbState', 'config'],
+ i18n: {
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+ },
apollo: {
containerRepository: {
query: getContainerRepositoryDetailsQuery,
@@ -230,6 +235,12 @@ export default {
@cancel="track('cancel_delete')"
/>
</template>
- <empty-state v-else is-empty-image :no-containers-image="config.noContainersImage" />
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.MISSING_OR_DELETED_IMAGE_TITLE"
+ :description="$options.i18n.MISSING_OR_DELETED_IMAGE_MESSAGE"
+ :svg-path="config.noContainersImage"
+ class="gl-mx-auto gl-my-0"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue
index cc629ae394c..a482c29bf50 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/composer_installation.vue
@@ -6,6 +6,7 @@ import {
TRACKING_ACTION_COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
+ COMPOSER_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -17,7 +18,7 @@ export default {
GlLink,
GlSprintf,
},
- inject: ['composerHelpPath', 'composerConfigRepositoryName', 'composerPath', 'groupListUrl'],
+ inject: ['groupListUrl'],
props: {
packageEntity: {
type: Object,
@@ -27,7 +28,7 @@ export default {
computed: {
composerRegistryInclude() {
// eslint-disable-next-line @gitlab/require-i18n-strings
- return `composer config repositories.${this.composerConfigRepositoryName} '{"type": "composer", "url": "${this.composerPath}"}'`;
+ return `composer config repositories.${this.packageEntity.composerConfigRepositoryUrl} '{"type": "composer", "url": "${this.packageEntity.composerUrl}"}'`;
},
composerPackageInclude() {
// eslint-disable-next-line @gitlab/require-i18n-strings
@@ -51,6 +52,9 @@ export default {
TRACKING_ACTION_COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
},
+ links: {
+ COMPOSER_HELP_PATH,
+ },
installOptions: [{ value: 'composer', label: s__('PackageRegistry|Show Composer commands') }],
};
</script>
@@ -79,7 +83,7 @@ export default {
<span data-testid="help-text">
<gl-sprintf :message="$options.i18n.infoLine">
<template #link="{ content }">
- <gl-link :href="composerHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.COMPOSER_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue
index 99e27c9d44a..ba0a3fcf5a1 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/conan_installation.vue
@@ -6,6 +6,7 @@ import {
TRACKING_ACTION_COPY_CONAN_COMMAND,
TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
+ CONAN_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -17,7 +18,6 @@ export default {
GlLink,
GlSprintf,
},
- inject: ['conanHelpPath', 'conanPath'],
props: {
packageEntity: {
type: Object,
@@ -31,7 +31,7 @@ export default {
},
conanSetupCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
- return `conan remote add gitlab ${this.conanPath}`;
+ return `conan remote add gitlab ${this.packageEntity.conanUrl}`;
},
},
i18n: {
@@ -44,7 +44,7 @@ export default {
TRACKING_ACTION_COPY_CONAN_SETUP_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
},
-
+ links: { CONAN_HELP_PATH },
installOptions: [{ value: 'conan', label: s__('PackageRegistry|Show Conan commands') }],
};
</script>
@@ -72,7 +72,7 @@ export default {
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
- <gl-link :href="conanHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.CONAN_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue
index 2070f0bbca0..4510c7a7322 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/maven_installation.vue
@@ -12,6 +12,7 @@ import {
TRACKING_ACTION_COPY_KOTLIN_ADD_TO_SOURCE_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
TRACKING_LABEL_MAVEN_INSTALLATION,
+ MAVEN_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -23,7 +24,6 @@ export default {
GlLink,
GlSprintf,
},
- inject: ['mavenHelpPath', 'mavenPath'],
props: {
packageEntity: {
type: Object,
@@ -36,6 +36,9 @@ export default {
};
},
computed: {
+ mavenUrl() {
+ return this.packageEntity.mavenUrl;
+ },
appGroup() {
return this.packageEntity.metadata.appGroup;
},
@@ -61,19 +64,19 @@ export default {
return `<repositories>
<repository>
<id>gitlab-maven</id>
- <url>${this.mavenPath}</url>
+ <url>${this.mavenUrl}</url>
</repository>
</repositories>
<distributionManagement>
<repository>
<id>gitlab-maven</id>
- <url>${this.mavenPath}</url>
+ <url>${this.mavenUrl}</url>
</repository>
<snapshotRepository>
<id>gitlab-maven</id>
- <url>${this.mavenPath}</url>
+ <url>${this.mavenUrl}</url>
</snapshotRepository>
</distributionManagement>`;
},
@@ -86,7 +89,7 @@ export default {
gradleGroovyAddSourceCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
return `maven {
- url '${this.mavenPath}'
+ url '${this.mavenUrl}'
}`;
},
@@ -95,7 +98,7 @@ export default {
},
gradleKotlinAddSourceCommand() {
- return `maven("${this.mavenPath}")`;
+ return `maven("${this.mavenUrl}")`;
},
showMaven() {
return this.instructionType === 'maven';
@@ -126,7 +129,7 @@ export default {
TRACKING_LABEL_CODE_INSTRUCTION,
TRACKING_LABEL_MAVEN_INSTALLATION,
},
-
+ links: { MAVEN_HELP_PATH },
installOptions: [
{ value: 'maven', label: s__('PackageRegistry|Maven XML') },
{ value: 'groovy', label: s__('PackageRegistry|Gradle Groovy DSL') },
@@ -185,7 +188,7 @@ export default {
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
- <gl-link :href="mavenHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.MAVEN_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
index 2448324549e..7479f748a56 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/npm_installation.vue
@@ -13,6 +13,7 @@ import {
YARN_PACKAGE_MANAGER,
PROJECT_PACKAGE_ENDPOINT_TYPE,
INSTANCE_PACKAGE_ENDPOINT_TYPE,
+ NPM_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -25,7 +26,7 @@ export default {
GlSprintf,
GlFormRadioGroup,
},
- inject: ['npmHelpPath', 'npmPath', 'npmProjectPath'],
+ inject: ['npmInstanceUrl'],
props: {
packageEntity: {
type: Object,
@@ -65,7 +66,9 @@ export default {
npmSetupCommand(type, endpointType) {
const scope = this.packageEntity.name.substring(0, this.packageEntity.name.indexOf('/'));
const npmPathForEndpoint =
- endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE ? this.npmPath : this.npmProjectPath;
+ endpointType === INSTANCE_PACKAGE_ENDPOINT_TYPE
+ ? this.npmInstanceUrl
+ : this.packageEntity.npmUrl;
if (type === NPM_PACKAGE_MANAGER) {
return `echo ${scope}:registry=${npmPathForEndpoint}/ >> .npmrc`;
@@ -89,6 +92,7 @@ export default {
'PackageRegistry|You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more.',
),
},
+ links: { NPM_HELP_PATH },
installOptions: [
{ value: NPM_PACKAGE_MANAGER, label: s__('PackageRegistry|Show NPM commands') },
{ value: YARN_PACKAGE_MANAGER, label: s__('PackageRegistry|Show Yarn commands') },
@@ -150,7 +154,7 @@ export default {
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
- <gl-link :href="npmHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.NPM_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
index 2e9991b7be5..b2007df142c 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/nuget_installation.vue
@@ -6,6 +6,7 @@ import {
TRACKING_ACTION_COPY_NUGET_INSTALL_COMMAND,
TRACKING_ACTION_COPY_NUGET_SETUP_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
+ NUGET_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -17,7 +18,6 @@ export default {
GlLink,
GlSprintf,
},
- inject: ['nugetHelpPath', 'nugetPath'],
props: {
packageEntity: {
type: Object,
@@ -29,7 +29,7 @@ export default {
return `nuget install ${this.packageEntity.name} -Source "GitLab"`;
},
nugetSetupCommand() {
- return `nuget source Add -Name "GitLab" -Source "${this.nugetPath}" -UserName <your_username> -Password <your_token>`;
+ return `nuget source Add -Name "GitLab" -Source "${this.packageEntity.nugetUrl}" -UserName <your_username> -Password <your_token>`;
},
},
tracking: {
@@ -42,6 +42,7 @@ export default {
'PackageRegistry|For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}.',
),
},
+ links: { NUGET_HELP_PATH },
installOptions: [{ value: 'nuget', label: s__('PackageRegistry|Show Nuget commands') }],
};
</script>
@@ -68,7 +69,7 @@ export default {
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
- <gl-link :href="nugetHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.NUGET_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
index bf7fe6fb91b..3724e371e01 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/package_files.vue
@@ -22,8 +22,12 @@ export default {
FileSha,
},
mixins: [Tracking.mixin()],
- inject: ['canDelete'],
props: {
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
packageFiles: {
type: Array,
required: false,
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
index 669adab9df6..a126d30f1ec 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/pypi_installation.vue
@@ -7,6 +7,7 @@ import {
TRACKING_ACTION_COPY_PIP_INSTALL_COMMAND,
TRACKING_ACTION_COPY_PYPI_SETUP_COMMAND,
TRACKING_LABEL_CODE_INSTRUCTION,
+ PYPI_HELP_PATH,
} from '~/packages_and_registries/package_registry/constants';
import CodeInstruction from '~/vue_shared/components/registry/code_instruction.vue';
@@ -18,7 +19,6 @@ export default {
GlLink,
GlSprintf,
},
- inject: ['pypiHelpPath', 'pypiPath', 'pypiSetupPath'],
props: {
packageEntity: {
type: Object,
@@ -28,11 +28,11 @@ export default {
computed: {
pypiPipCommand() {
// eslint-disable-next-line @gitlab/require-i18n-strings
- return `pip install ${this.packageEntity.name} --extra-index-url ${this.pypiPath}`;
+ return `pip install ${this.packageEntity.name} --extra-index-url ${this.packageEntity.pypiUrl}`;
},
pypiSetupCommand() {
return `[gitlab]
-repository = ${this.pypiSetupPath}
+repository = ${this.packageEntity.pypiSetupUrl}
username = __token__
password = <your personal access token>`;
},
@@ -50,6 +50,7 @@ password = <your personal access token>`;
'PackageRegistry|For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}.',
),
},
+ links: { PYPI_HELP_PATH },
installOptions: [{ value: 'pypi', label: s__('PackageRegistry|Show PyPi commands') }],
};
</script>
@@ -86,7 +87,7 @@ password = <your personal access token>`;
/>
<gl-sprintf :message="$options.i18n.helpText">
<template #link="{ content }">
- <gl-link :href="pypiHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.links.PYPI_HELP_PATH" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</div>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
index 6fd96c0654f..6222c2e73d7 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlLink, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
+import { GlButton, GlSprintf, GlTooltipDirective, GlTruncate } from '@gitlab/ui';
import { s__, __ } from '~/locale';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import {
@@ -18,7 +18,6 @@ export default {
name: 'PackageListRow',
components: {
GlButton,
- GlLink,
GlSprintf,
GlTruncate,
PackageTags,
@@ -42,9 +41,8 @@ export default {
packageType() {
return getPackageTypeLabel(this.packageEntity.packageType);
},
- packageLink() {
- const { project, id } = this.packageEntity;
- return `${project?.webUrl}/-/packages/${getIdFromGraphQLId(id)}`;
+ packageId() {
+ return getIdFromGraphQLId(this.packageEntity.id);
},
pipeline() {
return this.packageEntity?.pipelines?.nodes[0];
@@ -61,6 +59,9 @@ export default {
disabledRow() {
return this.packageEntity.status && this.packageEntity.status !== PACKAGE_DEFAULT_STATUS;
},
+ routerLinkEvent() {
+ return this.disabledRow ? '' : 'click';
+ },
},
i18n: {
erroredPackageText: s__('PackageRegistry|Invalid Package: failed metadata extraction'),
@@ -73,14 +74,15 @@ export default {
<list-item data-qa-selector="package_row" :disabled="disabledRow">
<template #left-primary>
<div class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0">
- <gl-link
- :href="packageLink"
+ <router-link
class="gl-text-body gl-min-w-0"
+ data-testid="details-link"
data-qa-selector="package_link"
- :disabled="disabledRow"
+ :event="routerLinkEvent"
+ :to="{ name: 'details', params: { id: packageId } }"
>
<gl-truncate :text="packageEntity.name" />
- </gl-link>
+ </router-link>
<gl-button
v-if="showWarningIcon"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/constants.js b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
index ab6541e4264..c4d331fa384 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/constants.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/constants.js
@@ -74,6 +74,7 @@ export const FETCH_PACKAGE_DETAILS_ERROR_MESSAGE = s__(
);
export const DELETE_PACKAGE_SUCCESS_MESSAGE = s__('PackageRegistry|Package deleted successfully');
+export const PACKAGE_REGISTRY_TITLE = __('Package Registry');
export const PACKAGE_ERROR_STATUS = 'ERROR';
export const PACKAGE_DEFAULT_STATUS = 'DEFAULT';
@@ -142,3 +143,9 @@ export const PACKAGE_TYPES = [
export const EMPTY_LIST_HELP_URL = helpPagePath('user/packages/package_registry/index');
export const PACKAGE_HELP_URL = helpPagePath('user/packages/index');
+export const NPM_HELP_PATH = helpPagePath('user/packages/npm_registry/index');
+export const MAVEN_HELP_PATH = helpPagePath('user/packages/maven_repository/index');
+export const CONAN_HELP_PATH = helpPagePath('user/packages/conan_repository/index');
+export const NUGET_HELP_PATH = helpPagePath('user/packages/nuget_repository/index');
+export const PYPI_HELP_PATH = helpPagePath('user/packages/pypi_repository/index');
+export const COMPOSER_HELP_PATH = helpPagePath('user/packages/composer_repository/index');
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
index 08ea0938a59..c45cbe56e00 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_details.query.graphql
@@ -7,9 +7,19 @@ query getPackageDetails($id: ID!) {
createdAt
updatedAt
status
+ canDestroy
+ npmUrl
+ mavenUrl
+ conanUrl
+ nugetUrl
+ pypiUrl
+ pypiSetupUrl
+ composerUrl
+ composerConfigRepositoryUrl
project {
id
path
+ name
}
tags(first: 10) {
nodes {
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/index.js b/app/assets/javascripts/packages_and_registries/package_registry/index.js
index 7ec931ff9a0..6680e612985 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/index.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/index.js
@@ -2,29 +2,59 @@ import Vue from 'vue';
import Translate from '~/vue_shared/translate';
import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
import PackageRegistry from '~/packages_and_registries/package_registry/pages/index.vue';
+import RegistryBreadcrumb from '~/packages_and_registries/shared/components/registry_breadcrumb.vue';
+import { renderBreadcrumb } from '~/packages_and_registries/shared/utils';
import createRouter from './router';
Vue.use(Translate);
export default () => {
const el = document.getElementById('js-vue-packages-list');
- const { endpoint, resourceId, fullPath, pageType, emptyListIllustration } = el.dataset;
- const router = createRouter(endpoint);
+ const {
+ endpoint,
+ resourceId,
+ fullPath,
+ pageType,
+ emptyListIllustration,
+ npmInstanceUrl,
+ projectListUrl,
+ groupListUrl,
+ } = el.dataset;
const isGroupPage = pageType === 'groups';
- return new Vue({
- el,
- router,
- apolloProvider,
- provide: {
- resourceId,
- fullPath,
- emptyListIllustration,
- isGroupPage,
- },
- render(createElement) {
- return createElement(PackageRegistry);
+ // This is a mini state to help the breadcrumb have the correct name in the details page
+ const breadCrumbState = Vue.observable({
+ name: '',
+ updateName(value) {
+ this.name = value;
},
});
+
+ const router = createRouter(endpoint, breadCrumbState);
+
+ const attachMainComponent = () =>
+ new Vue({
+ el,
+ router,
+ apolloProvider,
+ provide: {
+ resourceId,
+ fullPath,
+ emptyListIllustration,
+ isGroupPage,
+ npmInstanceUrl,
+ projectListUrl,
+ groupListUrl,
+ breadCrumbState,
+ },
+ render(createElement) {
+ return createElement(PackageRegistry);
+ },
+ });
+
+ return {
+ attachBreadcrumb: renderBreadcrumb(router, apolloProvider, RegistryBreadcrumb),
+ attachMainComponent,
+ };
};
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
deleted file mode 100644
index d94bbd21035..00000000000
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/details.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import { parseBoolean } from '~/lib/utils/common_utils';
-import PackagesApp from '~/packages_and_registries/package_registry/components/details/app.vue';
-import { apolloProvider } from '~/packages_and_registries/package_registry/graphql/index';
-import Translate from '~/vue_shared/translate';
-
-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,
- apolloProvider,
- provide: {
- canDelete: parseBoolean(canDelete),
- ...datasetOptions,
- },
- render(createElement) {
- return createElement(PackagesApp);
- },
- });
-};
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
index d49c1be5202..162b420a784 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/details.vue
@@ -68,16 +68,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
- inject: [
- 'packageId',
- 'projectName',
- 'canDelete',
- 'svgPath',
- 'npmPath',
- 'npmHelpPath',
- 'projectListUrl',
- 'groupListUrl',
- ],
+ inject: ['emptyListIllustration', 'projectListUrl', 'groupListUrl', 'breadCrumbState'],
trackingActions: {
DELETE_PACKAGE_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
@@ -100,7 +91,7 @@ export default {
return this.queryVariables;
},
update(data) {
- return data.package;
+ return data.package || {};
},
error(error) {
createFlash({
@@ -109,22 +100,33 @@ export default {
error,
});
},
+ result() {
+ this.breadCrumbState.updateName(
+ `${this.packageEntity?.name} v ${this.packageEntity?.version}`,
+ );
+ },
},
},
computed: {
+ projectName() {
+ return this.packageEntity.project?.name;
+ },
+ packageId() {
+ return this.$route.params.id;
+ },
queryVariables() {
return {
id: convertToGraphQLId('Packages::Package', this.packageId),
};
},
packageFiles() {
- return this.packageEntity?.packageFiles?.nodes;
+ return this.packageEntity.packageFiles?.nodes;
},
isLoading() {
return this.$apollo.queries.packageEntity.loading;
},
isValidPackage() {
- return this.isLoading || Boolean(this.packageEntity?.name);
+ return this.isLoading || Boolean(this.packageEntity.name);
},
tracking() {
return {
@@ -141,7 +143,7 @@ export default {
return this.packageEntity.packageType === PACKAGE_TYPE_NUGET;
},
showFiles() {
- return this.packageEntity?.packageType !== PACKAGE_TYPE_COMPOSER;
+ return this.packageEntity.packageType !== PACKAGE_TYPE_COMPOSER;
},
},
methods: {
@@ -235,13 +237,13 @@ export default {
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"
+ :svg-path="emptyListIllustration"
/>
<div v-else-if="!isLoading" class="packages-app">
<package-title :package-entity="packageEntity">
<template #delete-button>
<gl-button
- v-if="canDelete"
+ v-if="packageEntity.canDestroy"
v-gl-modal="'delete-modal'"
variant="danger"
category="primary"
@@ -265,6 +267,7 @@ export default {
<package-files
v-if="showFiles"
+ :can-delete="packageEntity.canDestroy"
:package-files="packageFiles"
@download-file="track($options.trackingActions.PULL_PACKAGE)"
@delete-file="handleFileDelete"
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/router.js b/app/assets/javascripts/packages_and_registries/package_registry/router.js
index ea5b740e879..c5ef4f70dd9 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/router.js
+++ b/app/assets/javascripts/packages_and_registries/package_registry/router.js
@@ -1,10 +1,12 @@
import Vue from 'vue';
import VueRouter from 'vue-router';
import List from '~/packages_and_registries/package_registry/pages/list.vue';
+import Details from '~/packages_and_registries/package_registry/pages/details.vue';
+import { PACKAGE_REGISTRY_TITLE } from '~/packages_and_registries/package_registry/constants';
Vue.use(VueRouter);
-export default function createRouter(base) {
+export default function createRouter(base, breadCrumbState) {
const router = new VueRouter({
base,
mode: 'history',
@@ -13,9 +15,25 @@ export default function createRouter(base) {
name: 'list',
path: '/',
component: List,
+ meta: {
+ nameGenerator: () => PACKAGE_REGISTRY_TITLE,
+ root: true,
+ },
+ },
+ {
+ name: 'details',
+ path: '/:id',
+ component: Details,
+ meta: {
+ nameGenerator: () => breadCrumbState.name,
+ },
},
],
});
+ router.afterEach(() => {
+ breadCrumbState.updateName('');
+ });
+
return router;
}
diff --git a/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
new file mode 100644
index 00000000000..9b2de1a1b84
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/shared/components/persisted_search.vue
@@ -0,0 +1,80 @@
+<script>
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { extractFilterAndSorting, getQueryParams } from '~/packages_and_registries/shared/utils';
+
+export default {
+ components: { RegistrySearch, UrlSync },
+ props: {
+ sortableFields: {
+ type: Array,
+ required: true,
+ },
+ defaultOrder: {
+ type: String,
+ required: true,
+ },
+ defaultSort: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ filters: [],
+ sorting: {
+ orderBy: this.defaultOrder,
+ sort: this.defaultSort,
+ },
+ mountRegistrySearch: false,
+ };
+ },
+ computed: {
+ parsedSorting() {
+ const cleanOrderBy = this.sorting?.orderBy.replace('_at', '');
+ return `${cleanOrderBy}_${this.sorting?.sort}`.toUpperCase();
+ },
+ },
+ mounted() {
+ const queryParams = getQueryParams(window.document.location.search);
+ const { sorting, filters } = extractFilterAndSorting(queryParams);
+ this.updateSorting(sorting);
+ this.updateFilters(filters);
+ this.mountRegistrySearch = true;
+ this.emitUpdate();
+ },
+ methods: {
+ updateFilters(newValue) {
+ this.filters = newValue;
+ },
+ updateSorting(newValue) {
+ this.sorting = { ...this.sorting, ...newValue };
+ },
+ updateSortingAndEmitUpdate(newValue) {
+ this.updateSorting(newValue);
+ this.emitUpdate();
+ },
+ emitUpdate() {
+ this.$emit('update', { sort: this.parsedSorting, filters: this.filters });
+ },
+ },
+};
+</script>
+
+<template>
+ <url-sync>
+ <template #default="{ updateQuery }">
+ <registry-search
+ v-if="mountRegistrySearch"
+ :filter="filters"
+ :sorting="sorting"
+ :tokens="$options.tokens"
+ :sortable-fields="sortableFields"
+ @sorting:changed="updateSortingAndEmitUpdate"
+ @filter:changed="updateFilters"
+ @filter:submit="emitUpdate"
+ @query:changed="updateQuery"
+ />
+ </template>
+ </url-sync>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
index e77eda31596..a1e3c06812c 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/registry_breadcrumb.vue
+++ b/app/assets/javascripts/packages_and_registries/shared/components/registry_breadcrumb.vue
@@ -20,8 +20,11 @@ export default {
isRootRoute() {
return this.$route.name === this.rootRoute.name;
},
+ detailsRouteName() {
+ return this.detailsRoute.meta.nameGenerator();
+ },
isLoaded() {
- return this.isRootRoute || this.$store?.state.imageDetails?.name;
+ return this.isRootRoute || this.detailsRouteName;
},
allCrumbs() {
const crumbs = [
@@ -32,7 +35,7 @@ export default {
];
if (!this.isRootRoute) {
crumbs.push({
- text: this.detailsRoute.meta.nameGenerator(),
+ text: this.detailsRouteName,
href: this.detailsRoute.meta.path,
});
}
@@ -45,7 +48,9 @@ export default {
<template>
<gl-breadcrumb :key="isLoaded" :items="allCrumbs">
<template #separator>
- <gl-icon name="angle-right" :size="8" />
+ <span class="gl-mx-n5">
+ <gl-icon name="angle-right" :size="8" />
+ </span>
</template>
</gl-breadcrumb>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index cf18f655e79..7e963cd0b08 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import { queryToObject } from '~/lib/utils/url_utility';
import { FILTERED_SEARCH_TERM } from './constants';
@@ -38,3 +39,37 @@ export const getCommitLink = ({ project_path: projectPath, pipeline = {} }, isGr
return `../commit/${pipeline.sha}`;
};
+
+export const renderBreadcrumb = (router, apolloProvider, RegistryBreadcrumb) => () => {
+ const breadCrumbEls = document.querySelectorAll('nav .js-breadcrumbs-list li');
+ const breadCrumbEl = breadCrumbEls[breadCrumbEls.length - 1];
+ const lastCrumb = breadCrumbEl.children[0];
+ const crumbs = [lastCrumb];
+ const nestedBreadcrumbEl = document.createElement('div');
+ breadCrumbEl.replaceChild(nestedBreadcrumbEl, lastCrumb);
+ return new Vue({
+ el: nestedBreadcrumbEl,
+ router,
+ apolloProvider,
+ components: {
+ RegistryBreadcrumb,
+ },
+ render(createElement) {
+ // FIXME(@tnir): this is a workaround until the MR gets merged:
+ // https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48115
+ const parentEl = breadCrumbEl.parentElement.parentElement;
+ if (parentEl) {
+ parentEl.classList.remove('breadcrumbs-container');
+ parentEl.classList.add('gl-display-flex');
+ parentEl.classList.add('w-100');
+ }
+ // End of FIXME(@tnir)
+ return createElement('registry-breadcrumb', {
+ class: breadCrumbEl.className,
+ props: {
+ crumbs,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/admin/integrations/edit/index.js b/app/assets/javascripts/pages/admin/integrations/edit/index.js
index 8485b460261..c354ed1c142 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 initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-initIntegrationSettingsForm('.js-integration-settings-form');
+initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js
index a3b9c43388a..a5eee2857df 100644
--- a/app/assets/javascripts/pages/admin/labels/edit/index.js
+++ b/app/assets/javascripts/pages/admin/labels/edit/index.js
@@ -1,3 +1,5 @@
import Labels from '~/labels/labels';
+import { initDeleteLabelModal } from '~/labels';
new Labels(); // eslint-disable-line no-new
+initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/admin/runners/edit/index.js b/app/assets/javascripts/pages/admin/runners/edit/index.js
new file mode 100644
index 00000000000..ddf135a2732
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/runners/edit/index.js
@@ -0,0 +1,3 @@
+import { initAdminRunnerEdit } from '~/runner/admin_runner_edit';
+
+initAdminRunnerEdit();
diff --git a/app/assets/javascripts/pages/admin/runners/show/index.js b/app/assets/javascripts/pages/admin/runners/show/index.js
deleted file mode 100644
index d1853772fda..00000000000
--- a/app/assets/javascripts/pages/admin/runners/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { initRunnerDetail } from '~/runner/runner_details';
-
-initRunnerDetail();
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index a1e7eb5d3de..cabb1b24ae6 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -172,8 +172,12 @@ export default class Todos {
updateBadges(data) {
$(document).trigger('todo:toggle', data.count);
- document.querySelector('.js-todos-pending .badge').innerHTML = addDelimiter(data.count);
- document.querySelector('.js-todos-done .badge').innerHTML = addDelimiter(data.done_count);
+ document.querySelector('.js-todos-pending .js-todos-badge').innerHTML = addDelimiter(
+ data.count,
+ );
+ document.querySelector('.js-todos-done .js-todos-badge').innerHTML = addDelimiter(
+ data.done_count,
+ );
}
goToTodoUrl(e) {
diff --git a/app/assets/javascripts/pages/explore/groups/index.js b/app/assets/javascripts/pages/explore/groups/index.js
index 808fcce46df..05078191e5c 100644
--- a/app/assets/javascripts/pages/explore/groups/index.js
+++ b/app/assets/javascripts/pages/explore/groups/index.js
@@ -1,6 +1,6 @@
-import GroupsList from '~/groups_list';
-import Landing from '~/landing';
-import initGroupsList from '../../../groups';
+import initGroupsList from '~/groups';
+import GroupsList from '~/groups/groups_list';
+import Landing from '~/groups/landing';
function exploreGroups() {
new GroupsList(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/groups/edit/index.js b/app/assets/javascripts/pages/groups/edit/index.js
index 604da77f60c..f6155b2ab2f 100644
--- a/app/assets/javascripts/pages/groups/edit/index.js
+++ b/app/assets/javascripts/pages/groups/edit/index.js
@@ -1,20 +1,18 @@
import { GROUP_BADGE } from '~/badges/constants';
-import initConfirmDangerModal from '~/confirm_danger_modal';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
import TransferDropdown from '~/groups/transfer_dropdown';
+import setupTransferEdit from '~/groups/transfer_edit';
import groupsSelect from '~/groups_select';
import { initCascadingSettingsLockPopovers } from '~/namespaces/cascading_settings';
import mountBadgeSettings from '~/pages/shared/mount_badge_settings';
import projectSelect from '~/project_select';
import initSearchSettings from '~/search_settings';
import initSettingsPanels from '~/settings_panels';
-import setupTransferEdit from '~/transfer_edit';
import initConfirmDanger from '~/init_confirm_danger';
document.addEventListener('DOMContentLoaded', () => {
initFilePickers();
- initConfirmDangerModal();
initConfirmDanger();
initSettingsPanels();
dirtySubmitFactory(
diff --git a/app/assets/javascripts/pages/groups/issues/index.js b/app/assets/javascripts/pages/groups/issues/index.js
index 966d55e5587..725c38defc3 100644
--- a/app/assets/javascripts/pages/groups/issues/index.js
+++ b/app/assets/javascripts/pages/groups/issues/index.js
@@ -1,6 +1,6 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
-import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
-import { mountIssuablesListApp, mountIssuesListApp } from '~/issues_list';
+import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
+import { mountIssuesListApp } from '~/issues/list';
import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
@@ -13,7 +13,7 @@ if (gon.features?.vueIssuesList) {
IssuableFilteredSearchTokenKeys.addExtraTokensForIssues();
IssuableFilteredSearchTokenKeys.removeTokensForKeys('release');
- issuableInitBulkUpdateSidebar.init(ISSUE_BULK_UPDATE_PREFIX);
+ initBulkUpdateSidebar(ISSUE_BULK_UPDATE_PREFIX);
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
@@ -23,8 +23,4 @@ if (gon.features?.vueIssuesList) {
});
projectSelect();
initManualOrdering();
-
- if (gon.features?.vueIssuablesList) {
- mountIssuablesListApp();
- }
}
diff --git a/app/assets/javascripts/pages/groups/labels/edit/index.js b/app/assets/javascripts/pages/groups/labels/edit/index.js
index e4e377f62fc..c032321d039 100644
--- a/app/assets/javascripts/pages/groups/labels/edit/index.js
+++ b/app/assets/javascripts/pages/groups/labels/edit/index.js
@@ -1,4 +1,6 @@
import Labels from 'ee_else_ce/labels/labels';
+import { initDeleteLabelModal } from '~/labels';
// eslint-disable-next-line no-new
new Labels();
+initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/groups/merge_requests/index.js b/app/assets/javascripts/pages/groups/merge_requests/index.js
index cb38ee1c6e0..de28f027126 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/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+import { initBulkUpdateSidebar } from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import projectSelect from '~/project_select';
@@ -8,7 +8,7 @@ import projectSelect from '~/project_select';
const ISSUABLE_BULK_UPDATE_PREFIX = 'merge_request_';
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
-issuableInitBulkUpdateSidebar.init(ISSUABLE_BULK_UPDATE_PREFIX);
+initBulkUpdateSidebar(ISSUABLE_BULK_UPDATE_PREFIX);
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/groups/new/index.js b/app/assets/javascripts/pages/groups/new/index.js
index 7b0418e1ad5..702b152d25a 100644
--- a/app/assets/javascripts/pages/groups/new/index.js
+++ b/app/assets/javascripts/pages/groups/new/index.js
@@ -15,7 +15,7 @@ initFilePickers();
new Group(); // eslint-disable-line no-new
function initNewGroupCreation(el) {
- const { hasErrors } = el.dataset;
+ const { hasErrors, verificationRequired, verificationFormUrl, subscriptionsUrl } = el.dataset;
const props = {
hasErrors: parseBoolean(hasErrors),
@@ -23,6 +23,11 @@ function initNewGroupCreation(el) {
return new Vue({
el,
+ provide: {
+ verificationRequired: parseBoolean(verificationRequired),
+ verificationFormUrl,
+ subscriptionsUrl,
+ },
render(h) {
return h(NewGroupCreationApp, { props });
},
diff --git a/app/assets/javascripts/pages/groups/packages/index.js b/app/assets/javascripts/pages/groups/packages/index.js
new file mode 100644
index 00000000000..cbe08565cfa
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/packages/index.js
@@ -0,0 +1,8 @@
+import packageApp from '~/packages_and_registries/package_registry/index';
+
+const app = packageApp();
+
+if (app) {
+ app.attachBreadcrumb();
+ app.attachMainComponent();
+}
diff --git a/app/assets/javascripts/pages/groups/packages/index/index.js b/app/assets/javascripts/pages/groups/packages/index/index.js
deleted file mode 100644
index 174973a9fad..00000000000
--- a/app/assets/javascripts/pages/groups/packages/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import packageApp from '~/packages_and_registries/package_registry/index';
-
-packageApp();
diff --git a/app/assets/javascripts/pages/groups/settings/access_tokens/index.js b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
new file mode 100644
index 00000000000..dc1bb88bf4b
--- /dev/null
+++ b/app/assets/javascripts/pages/groups/settings/access_tokens/index.js
@@ -0,0 +1,3 @@
+import { initExpiresAtField } from '~/access_tokens';
+
+initExpiresAtField();
diff --git a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
index 8485b460261..c354ed1c142 100644
--- a/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
+++ b/app/assets/javascripts/pages/groups/settings/integrations/edit/index.js
@@ -1,7 +1,7 @@
import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
-initIntegrationSettingsForm('.js-integration-settings-form');
+initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
diff --git a/app/assets/javascripts/pages/help/index/index.js b/app/assets/javascripts/pages/help/index/index.js
index 736add8dca3..a8e67c57307 100644
--- a/app/assets/javascripts/pages/help/index/index.js
+++ b/app/assets/javascripts/pages/help/index/index.js
@@ -1,6 +1,5 @@
-import $ from 'jquery';
import docs from '~/docs/docs_bundle';
-import VersionCheckImage from '~/version_check_image';
+import initGitlabVersionCheck from '~/gitlab_version_check';
docs();
-VersionCheckImage.bindErrorEvent($('img.js-version-status-badge'));
+initGitlabVersionCheck();
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
index 1b291d9509d..6b12604c76b 100644
--- a/app/assets/javascripts/pages/profiles/keys/index.js
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -7,11 +7,13 @@ function initSshKeyValidation() {
const input = document.querySelector('.js-add-ssh-key-validation-input');
if (!input) return;
+ const supportedAlgorithms = JSON.parse(input.dataset.supportedAlgorithms);
const warning = document.querySelector('.js-add-ssh-key-validation-warning');
const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit');
const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit');
const addSshKeyValidation = new AddSshKeyValidation(
+ supportedAlgorithms,
input,
warning,
originalSubmit,
diff --git a/app/assets/javascripts/pages/projects/blob/show/index.js b/app/assets/javascripts/pages/projects/blob/show/index.js
index b365e039191..2fc9a111405 100644
--- a/app/assets/javascripts/pages/projects/blob/show/index.js
+++ b/app/assets/javascripts/pages/projects/blob/show/index.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import TableOfContents from '~/blob/components/table_contents.vue';
import PipelineTourSuccessModal from '~/blob/pipeline_tour_success_modal.vue';
import { BlobViewer, initAuxiliaryViewer } from '~/blob/viewer/index';
@@ -12,11 +13,14 @@ import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
import '~/sourcegraph/load';
Vue.use(VueApollo);
+Vue.use(VueRouter);
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
+const router = new VueRouter({ mode: 'history' });
+
const viewBlobEl = document.querySelector('#js-view-blob-app');
if (viewBlobEl) {
@@ -25,6 +29,7 @@ if (viewBlobEl) {
// eslint-disable-next-line no-new
new Vue({
el: viewBlobEl,
+ router,
apolloProvider,
provide: {
targetBranch,
@@ -41,6 +46,7 @@ if (viewBlobEl) {
});
initAuxiliaryViewer();
+ initBlob();
} else {
new BlobViewer(); // eslint-disable-line no-new
initBlob();
diff --git a/app/assets/javascripts/pages/projects/branches/index/index.js b/app/assets/javascripts/pages/projects/branches/index/index.js
index 97dc76908af..d279c4cbb08 100644
--- a/app/assets/javascripts/pages/projects/branches/index/index.js
+++ b/app/assets/javascripts/pages/projects/branches/index/index.js
@@ -1,13 +1,11 @@
import initDeprecatedRemoveRowBehavior from '~/behaviors/deprecated_remove_row_behavior';
import AjaxLoadingSpinner from '~/branches/ajax_loading_spinner';
import BranchSortDropdown from '~/branches/branch_sort_dropdown';
-import DeleteModal from '~/branches/branches_delete_modal';
import initDiverganceGraph from '~/branches/divergence_graph';
import initDeleteBranchButton from '~/branches/init_delete_branch_button';
import initDeleteBranchModal from '~/branches/init_delete_branch_modal';
AjaxLoadingSpinner.init();
-new DeleteModal(); // eslint-disable-line no-new
const { divergingCountsEndpoint, defaultBranch } = document.querySelector(
'.js-branch-list',
diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js
index 100ca5b36d9..c0eb2a8fd77 100644
--- a/app/assets/javascripts/pages/projects/edit/index.js
+++ b/app/assets/javascripts/pages/projects/edit/index.js
@@ -1,5 +1,4 @@
import { PROJECT_BADGE } from '~/badges/constants';
-import initLegacyConfirmDangerModal from '~/confirm_danger_modal';
import initConfirmDanger from '~/init_confirm_danger';
import dirtySubmitFactory from '~/dirty_submit/dirty_submit_factory';
import initFilePickers from '~/file_pickers';
@@ -15,7 +14,6 @@ import initProjectPermissionsSettings from '../shared/permissions';
import initProjectLoadingSpinner from '../shared/save_project_loader';
initFilePickers();
-initLegacyConfirmDangerModal();
initConfirmDanger();
initSettingsPanels();
initProjectDeleteButton();
diff --git a/app/assets/javascripts/pages/projects/find_file/show/index.js b/app/assets/javascripts/pages/projects/find_file/show/index.js
index a8225167c6b..f47888f0cb8 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file';
-import ProjectFindFile from '~/project_find_file';
+import ProjectFindFile from '~/projects/project_find_file';
const findElement = document.querySelector('.js-file-finder');
const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
diff --git a/app/assets/javascripts/pages/projects/imports/show/index.js b/app/assets/javascripts/pages/projects/imports/show/index.js
index 8397826f8eb..21d07e04ddc 100644
--- a/app/assets/javascripts/pages/projects/imports/show/index.js
+++ b/app/assets/javascripts/pages/projects/imports/show/index.js
@@ -1,3 +1,3 @@
-import ProjectImport from '~/project_import';
+import ProjectImport from '~/projects/project_import';
new ProjectImport(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/incidents/show/index.js b/app/assets/javascripts/pages/projects/incidents/show/index.js
index 4633eaef8f9..5a8cfcf8462 100644
--- a/app/assets/javascripts/pages/projects/incidents/show/index.js
+++ b/app/assets/javascripts/pages/projects/incidents/show/index.js
@@ -1,6 +1,6 @@
+import { initShow } from '~/issues';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '~/issues/show';
initShow();
initSidebarBundle();
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 06aba866ccf..7db34816cfe 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -2,8 +2,8 @@ import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
+import LineHighlighter from '~/blob/line_highlighter';
import initBlobBundle from '~/blob_edit/blob_bundle';
-import LineHighlighter from '~/line_highlighter';
export default () => {
new LineHighlighter(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/issues/edit/index.js b/app/assets/javascripts/pages/projects/issues/edit/index.js
index aa00d1f58bd..06dcd2c2d94 100644
--- a/app/assets/javascripts/pages/projects/issues/edit/index.js
+++ b/app/assets/javascripts/pages/projects/issues/edit/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/issues/form';
+import { initForm } from 'ee_else_ce/issues';
initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index e937713044c..44b1d5277d1 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,8 +1,8 @@
import IssuableFilteredSearchTokenKeys from 'ee_else_ce/filtered_search/issuable_filtered_search_token_keys';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
-import { mountIssuablesListApp, mountIssuesListApp, mountJiraIssuesListApp } from '~/issues_list';
+import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
+import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list';
import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { ISSUABLE_INDEX } from '~/issuable/constants';
@@ -20,16 +20,13 @@ if (gon.features?.vueIssuesList) {
useDefaultState: true,
});
- issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.ISSUE);
+ initBulkUpdateSidebar(ISSUABLE_INDEX.ISSUE);
+ initIssueStatusSelect();
new UsersSelect(); // eslint-disable-line no-new
initCsvImportExportButtons();
initIssuableByEmail();
initManualOrdering();
-
- if (gon.features?.vueIssuablesList) {
- mountIssuablesListApp();
- }
}
new ShortcutsNavigation(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/issues/new/index.js b/app/assets/javascripts/pages/projects/issues/new/index.js
index aa00d1f58bd..06dcd2c2d94 100644
--- a/app/assets/javascripts/pages/projects/issues/new/index.js
+++ b/app/assets/javascripts/pages/projects/issues/new/index.js
@@ -1,3 +1,3 @@
-import initForm from 'ee_else_ce/issues/form';
+import { initForm } from 'ee_else_ce/issues';
initForm();
diff --git a/app/assets/javascripts/pages/projects/issues/service_desk/index.js b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
index 69639d17f8a..7dd128fedb9 100644
--- a/app/assets/javascripts/pages/projects/issues/service_desk/index.js
+++ b/app/assets/javascripts/pages/projects/issues/service_desk/index.js
@@ -1,8 +1,3 @@
-import { mountIssuablesListApp } from '~/issues_list';
-import { initFilteredSearchServiceDesk } from '~/issues/init_filtered_search_service_desk';
+import { initFilteredSearchServiceDesk } from '~/issues';
initFilteredSearchServiceDesk();
-
-if (gon.features?.vueIssuablesList) {
- mountIssuablesListApp();
-}
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index d0b1942f2a4..46a34c025b6 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,7 +1,7 @@
+import { initShow } from '~/issues';
import { store } from '~/notes/stores';
import initRelatedIssues from '~/related_issues';
import initSidebarBundle from '~/sidebar/sidebar_bundle';
-import initShow from '~/issues/show';
initShow();
initSidebarBundle(store);
diff --git a/app/assets/javascripts/pages/projects/labels/edit/index.js b/app/assets/javascripts/pages/projects/labels/edit/index.js
index c4d7af39767..cb554e3d4da 100644
--- a/app/assets/javascripts/pages/projects/labels/edit/index.js
+++ b/app/assets/javascripts/pages/projects/labels/edit/index.js
@@ -1,3 +1,5 @@
import Labels from 'ee_else_ce/labels/labels';
+import { initDeleteLabelModal } from '~/labels';
new Labels(); // eslint-disable-line no-new
+initDeleteLabelModal();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/index/index.js b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
index acd1731a700..e284e7b2c5e 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -2,13 +2,14 @@ import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { initCsvImportExportButtons, initIssuableByEmail } from '~/issuable';
-import issuableInitBulkUpdateSidebar from '~/issuable/bulk_update_sidebar/issuable_init_bulk_update_sidebar';
+import { initBulkUpdateSidebar, initIssueStatusSelect } from '~/issuable/bulk_update_sidebar';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import { ISSUABLE_INDEX } from '~/issuable/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import UsersSelect from '~/users_select';
-issuableInitBulkUpdateSidebar.init(ISSUABLE_INDEX.MERGE_REQUEST);
+initBulkUpdateSidebar(ISSUABLE_INDEX.MERGE_REQUEST);
+initIssueStatusSelect();
addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys);
IssuableFilteredSearchTokenKeys.removeTokensForKeys('iteration');
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index d89b4d0e0a3..5d830872ed9 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -1,5 +1,5 @@
import { initNewProjectCreation, initNewProjectUrlSelect } from '~/projects/new';
-import initProjectVisibilitySelector from '~/project_visibility';
+import initProjectVisibilitySelector from '~/projects/project_visibility';
import initProjectNew from '~/projects/project_new';
initProjectVisibilitySelector();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index.js b/app/assets/javascripts/pages/projects/packages/packages/index.js
new file mode 100644
index 00000000000..cbe08565cfa
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/packages/packages/index.js
@@ -0,0 +1,8 @@
+import packageApp from '~/packages_and_registries/package_registry/index';
+
+const app = packageApp();
+
+if (app) {
+ app.attachBreadcrumb();
+ app.attachMainComponent();
+}
diff --git a/app/assets/javascripts/pages/projects/packages/packages/index/index.js b/app/assets/javascripts/pages/projects/packages/packages/index/index.js
deleted file mode 100644
index 174973a9fad..00000000000
--- a/app/assets/javascripts/pages/projects/packages/packages/index/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import packageApp from '~/packages_and_registries/package_registry/index';
-
-packageApp();
diff --git a/app/assets/javascripts/pages/projects/packages/packages/show/index.js b/app/assets/javascripts/pages/projects/packages/packages/show/index.js
deleted file mode 100644
index 2dee87985cb..00000000000
--- a/app/assets/javascripts/pages/projects/packages/packages/show/index.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import initPackageDetails from '~/packages_and_registries/package_registry/pages/details';
-
-initPackageDetails();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index e92b9b30fa4..277d2e0d30a 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,6 +1,6 @@
import initDeprecatedJQueryDropdown from '~/deprecated_jquery_dropdown';
-const defaultTimezone = { name: 'UTC', offset: 0 };
+const defaultTimezone = { identifier: 'Etc/UTC', name: 'UTC', offset: 0 };
const defaults = {
$inputEl: null,
$dropdownEl: null,
@@ -70,7 +70,7 @@ export default class TimezoneDropdown {
setDropdownValue(timezone) {
this.$dropdownToggle.text(this.displayFormat(timezone));
- this.$input.val(timezone.name);
+ this.$input.val(timezone.identifier);
}
handleDropdownChange({ selectedObj, e }) {
diff --git a/app/assets/javascripts/pages/projects/services/edit/index.js b/app/assets/javascripts/pages/projects/services/edit/index.js
index a2b18d86240..2048d3dfc37 100644
--- a/app/assets/javascripts/pages/projects/services/edit/index.js
+++ b/app/assets/javascripts/pages/projects/services/edit/index.js
@@ -2,7 +2,7 @@ import initIntegrationSettingsForm from '~/integrations/edit';
import PrometheusAlerts from '~/prometheus_alerts';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
-initIntegrationSettingsForm('.js-integration-settings-form');
+initIntegrationSettingsForm();
const prometheusSettingsSelector = '.js-prometheus-metrics-monitoring';
const prometheusSettingsWrapper = document.querySelector(prometheusSettingsSelector);
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 384ee1f5034..d5e00f54e91 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
@@ -1,6 +1,6 @@
<script>
-import { GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
-
+import { GlButton, GlIcon, GlSprintf, GlLink, GlFormCheckbox, GlToggle } from '@gitlab/ui';
+import ConfirmDanger from '~/vue_shared/components/confirm_danger/confirm_danger.vue';
import settingsMixin from 'ee_else_ce/pages/projects/shared/permissions/mixins/settings_pannel_mixin';
import { __, s__ } from '~/locale';
import {
@@ -41,16 +41,19 @@ export default {
pucWarningHelpText: s__(
'ProjectSettings|Highlight the usage of hidden unicode characters. These have innocent uses for right-to-left languages, but can also be used in potential exploits.',
),
+ confirmButtonText: __('Save changes'),
},
components: {
projectFeatureSetting,
projectSettingRow,
+ GlButton,
GlIcon,
GlSprintf,
GlLink,
GlFormCheckbox,
GlToggle,
+ ConfirmDanger,
},
mixins: [settingsMixin],
@@ -163,6 +166,15 @@ export default {
required: false,
default: '',
},
+ confirmationPhrase: {
+ type: String,
+ required: true,
+ },
+ showVisibilityConfirmModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
const defaults = {
@@ -274,6 +286,12 @@ export default {
cveIdRequestIsDisabled() {
return this.visibilityLevel !== visibilityOptions.PUBLIC;
},
+ isVisibilityReduced() {
+ return (
+ this.showVisibilityConfirmModal &&
+ this.visibilityLevel < this.currentSettings.visibilityLevel
+ );
+ },
},
watch: {
@@ -774,5 +792,23 @@ export default {
<template #help>{{ $options.i18n.pucWarningHelpText }}</template>
</gl-form-checkbox>
</project-setting-row>
+ <confirm-danger
+ v-if="isVisibilityReduced"
+ button-variant="confirm"
+ :disabled="false"
+ :phrase="confirmationPhrase"
+ :button-text="$options.i18n.confirmButtonText"
+ data-testid="project-features-save-button"
+ @confirm="$emit('confirm')"
+ />
+ <gl-button
+ v-else
+ type="submit"
+ variant="confirm"
+ data-testid="project-features-save-button"
+ data-qa-selector="visibility_features_permissions_save_button"
+ >
+ {{ $options.i18n.confirmButtonText }}
+ </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js
index d7bae44e96e..de8b1cc400e 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/index.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import settingsPanel from './components/settings_panel.vue';
export default function initProjectPermissionsSettings() {
@@ -6,8 +7,36 @@ export default function initProjectPermissionsSettings() {
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
const componentProps = JSON.parse(componentPropsEl.innerHTML);
+ const {
+ targetFormId,
+ additionalInformation,
+ confirmDangerMessage,
+ confirmButtonText,
+ showVisibilityConfirmModal,
+ htmlConfirmationMessage,
+ phrase: confirmationPhrase,
+ } = mountPoint.dataset;
+
return new Vue({
el: mountPoint,
- render: (createElement) => createElement(settingsPanel, { props: { ...componentProps } }),
+ provide: {
+ additionalInformation,
+ confirmDangerMessage,
+ confirmButtonText,
+ htmlConfirmationMessage: parseBoolean(htmlConfirmationMessage),
+ },
+ render: (createElement) =>
+ createElement(settingsPanel, {
+ props: {
+ ...componentProps,
+ confirmationPhrase,
+ showVisibilityConfirmModal: parseBoolean(showVisibilityConfirmModal),
+ },
+ on: {
+ confirm: () => {
+ if (targetFormId) document.getElementById(targetFormId)?.submit();
+ },
+ },
+ }),
});
}
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 31d69a731fe..71c6773c176 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,16 +1,16 @@
import initTree from 'ee_else_ce/repository';
import Activities from '~/activities';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
-import { BlobViewer } from '~/blob/viewer/index';
+import { BlobViewer } from '~/blob/viewer';
import { initUploadForm } from '~/blob_edit/blob_bundle';
import initInviteMembersModal from '~/invite_members/init_invite_members_modal';
import initInviteMembersTrigger from '~/invite_members/init_invite_members_trigger';
import leaveByUrl from '~/namespaces/leave_by_url';
import initVueNotificationsDropdown from '~/notifications';
+import Star from '~/projects/star';
import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import UserCallout from '~/user_callout';
-import Star from '../../../star';
initReadMore();
new Star(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/registrations/new/index.js b/app/assets/javascripts/pages/registrations/new/index.js
index ae605edeaf0..8bbe81a9ed5 100644
--- a/app/assets/javascripts/pages/registrations/new/index.js
+++ b/app/assets/javascripts/pages/registrations/new/index.js
@@ -1,3 +1,5 @@
+import { trackNewRegistrations } from '~/google_tag_manager';
+
import NoEmojiValidator from '~/emoji/no_emoji_validator';
import LengthValidator from '~/pages/sessions/new/length_validator';
import UsernameValidator from '~/pages/sessions/new/username_validator';
@@ -5,3 +7,5 @@ import UsernameValidator from '~/pages/sessions/new/username_validator';
new UsernameValidator(); // eslint-disable-line no-new
new LengthValidator(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
+
+trackNewRegistrations();
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 b29e9455755..c28de88554a 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -596,7 +596,9 @@ export default {
:disabled="disableSubmitButton"
>{{ submitButtonText }}</gl-button
>
- <gl-button :href="cancelFormPath" class="float-right">{{ $options.i18n.cancel }}</gl-button>
+ <gl-button data-testid="wiki-cancel-button" :href="cancelFormPath" class="float-right">{{
+ $options.i18n.cancel
+ }}</gl-button>
</div>
</gl-form>
</template>
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 9f82d4a5395..ca78f194a82 100644
--- a/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
+++ b/app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue
@@ -142,6 +142,7 @@ export default {
class="js-no-auto-disable"
category="primary"
variant="confirm"
+ data-qa-selector="commit_changes_button"
:disabled="submitDisabled"
:loading="isSaving"
>
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 92fa411d5af..bfbf24c6b13 100644
--- a/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
+++ b/app/assets/javascripts/pipeline_editor/components/editor/text_editor.vue
@@ -15,12 +15,10 @@ export default {
onCiConfigUpdate(content) {
this.$emit('updateCiConfig', content);
},
- registerCiSchema() {
+ registerCiSchema({ detail: { instance } }) {
if (this.glFeatures.schemaLinting) {
- const editorInstance = this.$refs.editor.getEditor();
-
- editorInstance.use({ definition: CiSchemaExtension });
- editorInstance.registerCiSchema();
+ instance.use({ definition: CiSchemaExtension });
+ instance.registerCiSchema();
}
},
},
@@ -33,7 +31,7 @@ export default {
ref="editor"
:file-name="ciConfigPath"
v-bind="$attrs"
- @[$options.readyEvent]="registerCiSchema"
+ @[$options.readyEvent]="registerCiSchema($event)"
@input="onCiConfigUpdate"
v-on="$listeners"
/>
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 16ad648afca..72b492a5877 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue
@@ -153,7 +153,9 @@ export default {
<span class="gl-font-weight-bold">
<gl-sprintf :message="$options.i18n.pipelineInfo">
<template #id="{ content }">
- <span data-testid="pipeline-id"> {{ content }}{{ pipelineId }} </span>
+ <span data-testid="pipeline-id" data-qa-selector="pipeline_id_content">
+ {{ content }}{{ pipelineId }}
+ </span>
</template>
<template #status>{{ status.text }}</template>
<template #commit>
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 833d784f940..23f1592cac1 100644
--- a/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
+++ b/app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue
@@ -5,6 +5,7 @@ import getAppStatus from '~/pipeline_editor/graphql/queries/client/app_status.qu
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import {
EDITOR_APP_STATUS_EMPTY,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
} from '../../constants';
@@ -17,6 +18,7 @@ export const i18n = {
loading: s__('Pipelines|Validating GitLab CI configuration…'),
invalid: s__('Pipelines|This GitLab CI configuration is invalid.'),
invalidWithReason: s__('Pipelines|This GitLab CI configuration is invalid: %{reason}.'),
+ unavailableValidation: s__('Pipelines|Configuration validation currently not available.'),
valid: s__('Pipelines|This GitLab CI configuration is valid.'),
};
@@ -29,6 +31,9 @@ export default {
TooltipOnTruncate,
},
inject: {
+ lintUnavailableHelpPagePath: {
+ default: '',
+ },
ymlHelpPagePath: {
default: '',
},
@@ -49,9 +54,15 @@ export default {
},
},
computed: {
+ helpPath() {
+ return this.isLintUnavailable ? this.lintUnavailableHelpPagePath : this.ymlHelpPagePath;
+ },
isEmpty() {
return this.appStatus === EDITOR_APP_STATUS_EMPTY;
},
+ isLintUnavailable() {
+ return this.appStatus === EDITOR_APP_STATUS_LINT_UNAVAILABLE;
+ },
isLoading() {
return this.appStatus === EDITOR_APP_STATUS_LOADING;
},
@@ -62,6 +73,8 @@ export default {
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return 'check';
+ case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
+ return 'time-out';
case EDITOR_APP_STATUS_VALID:
return 'check';
default:
@@ -74,6 +87,8 @@ export default {
switch (this.appStatus) {
case EDITOR_APP_STATUS_EMPTY:
return this.$options.i18n.empty;
+ case EDITOR_APP_STATUS_LINT_UNAVAILABLE:
+ return this.$options.i18n.unavailableValidation;
case EDITOR_APP_STATUS_VALID:
return this.$options.i18n.valid;
default:
@@ -96,10 +111,13 @@ export default {
<span v-else class="gl-display-inline-flex gl-white-space-nowrap gl-max-w-full">
<tooltip-on-truncate :title="message" class="gl-text-truncate">
- <gl-icon :name="icon" /> <span data-testid="validationMsg">{{ message }}</span>
+ <gl-icon :name="icon" />
+ <span data-qa-selector="validation_message_content" data-testid="validationMsg">
+ {{ message }}
+ </span>
</tooltip-on-truncate>
<span v-if="!isEmpty" class="gl-flex-shrink-0 gl-pl-2">
- <gl-link data-testid="learnMoreLink" :href="ymlHelpPagePath">
+ <gl-link data-testid="learnMoreLink" :href="helpPath">
{{ $options.i18n.learnMore }}
</gl-link>
</span>
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 3f50a1225d8..c75b1d4bb11 100644
--- a/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -11,6 +11,7 @@ import {
EDITOR_APP_STATUS_INVALID,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
LINT_TAB,
MERGED_TAB,
TAB_QUERY_PARAM,
@@ -106,6 +107,9 @@ export default {
isInvalid() {
return this.appStatus === EDITOR_APP_STATUS_INVALID;
},
+ isLintUnavailable() {
+ return this.appStatus === EDITOR_APP_STATUS_LINT_UNAVAILABLE;
+ },
isValid() {
return this.appStatus === EDITOR_APP_STATUS_VALID;
},
@@ -142,6 +146,7 @@ export default {
<template>
<gl-tabs
class="file-editor gl-mb-3"
+ data-qa-selector="file_editor_container"
:query-param-name="$options.query.TAB_QUERY_PARAM"
sync-active-tab-with-query-params
>
@@ -166,6 +171,7 @@ export default {
:empty-message="$options.i18n.empty.visualization"
:is-empty="isEmpty"
:is-invalid="isInvalid"
+ :is-unavailable="isLintUnavailable"
:keep-component-mounted="false"
:title="$options.i18n.tabGraph"
lazy
@@ -179,6 +185,7 @@ export default {
class="gl-mb-3"
:empty-message="$options.i18n.empty.lint"
:is-empty="isEmpty"
+ :is-unavailable="isLintUnavailable"
:title="$options.i18n.tabLint"
data-testid="lint-tab"
@click="setCurrentTab($options.tabConstants.LINT_TAB)"
@@ -192,6 +199,7 @@ export default {
:keep-component-mounted="false"
:is-empty="isEmpty"
:is-invalid="isInvalid"
+ :is-unavailable="isLintUnavailable"
:title="$options.i18n.tabMergedYaml"
lazy
data-testid="merged-tab"
diff --git a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue
index 7c032441a04..673599da085 100644
--- a/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue
+++ b/app/assets/javascripts/pipeline_editor/components/ui/editor_tab.vue
@@ -42,6 +42,9 @@ import { __, s__ } from '~/locale';
export default {
i18n: {
invalid: __('Your CI/CD configuration syntax is invalid. View Lint tab for more details.'),
+ unavailable: __(
+ "We're experiencing difficulties and this tab content is currently unavailable.",
+ ),
},
components: {
GlAlert,
@@ -66,14 +69,14 @@ export default {
isEmpty: {
type: Boolean,
required: false,
- default: null,
+ default: false,
},
isInvalid: {
type: Boolean,
required: false,
- default: null,
+ default: false,
},
- lazy: {
+ isUnavailable: {
type: Boolean,
required: false,
default: false,
@@ -83,6 +86,11 @@ export default {
required: false,
default: true,
},
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -109,6 +117,9 @@ export default {
<template>
<gl-tab :lazy="isLazy" v-bind="$attrs" v-on="$listeners">
<gl-alert v-if="isEmpty" variant="tip">{{ emptyMessage }}</gl-alert>
+ <gl-alert v-else-if="isUnavailable" variant="danger" :dismissible="false">
+ {{ $options.i18n.unavailable }}</gl-alert
+ >
<gl-alert v-else-if="isInvalid" variant="danger">{{ $options.i18n.invalid }}</gl-alert>
<template v-else>
<slot v-for="slot in slots" :name="slot"></slot>
diff --git a/app/assets/javascripts/pipeline_editor/constants.js b/app/assets/javascripts/pipeline_editor/constants.js
index a2eaeeef286..bc79b0742e7 100644
--- a/app/assets/javascripts/pipeline_editor/constants.js
+++ b/app/assets/javascripts/pipeline_editor/constants.js
@@ -6,12 +6,14 @@ export const CI_CONFIG_STATUS_VALID = 'VALID';
// represent the global state of the pipeline editor app.
export const EDITOR_APP_STATUS_EMPTY = 'EMPTY';
export const EDITOR_APP_STATUS_INVALID = CI_CONFIG_STATUS_INVALID;
+export const EDITOR_APP_STATUS_LINT_UNAVAILABLE = 'LINT_DOWN';
export const EDITOR_APP_STATUS_LOADING = 'LOADING';
export const EDITOR_APP_STATUS_VALID = CI_CONFIG_STATUS_VALID;
export const EDITOR_APP_VALID_STATUSES = [
EDITOR_APP_STATUS_EMPTY,
EDITOR_APP_STATUS_INVALID,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
EDITOR_APP_STATUS_LOADING,
EDITOR_APP_STATUS_VALID,
];
diff --git a/app/assets/javascripts/pipeline_editor/index.js b/app/assets/javascripts/pipeline_editor/index.js
index ee93e327b76..04f91cb3d1e 100644
--- a/app/assets/javascripts/pipeline_editor/index.js
+++ b/app/assets/javascripts/pipeline_editor/index.js
@@ -37,6 +37,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
emptyStateIllustrationPath,
helpPaths,
lintHelpPagePath,
+ lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
@@ -124,6 +125,7 @@ export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
emptyStateIllustrationPath,
helpPaths,
lintHelpPagePath,
+ lintUnavailableHelpPagePath,
needsHelpPagePath,
newMergeRequestPath,
pipelinePagePath,
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
index e397054f06a..90f48195c5e 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_app.vue
@@ -12,8 +12,9 @@ import PipelineEditorMessages from './components/ui/pipeline_editor_messages.vue
import {
COMMIT_SHA_POLL_INTERVAL,
EDITOR_APP_STATUS_EMPTY,
- EDITOR_APP_VALID_STATUSES,
EDITOR_APP_STATUS_LOADING,
+ EDITOR_APP_STATUS_LINT_UNAVAILABLE,
+ EDITOR_APP_VALID_STATUSES,
LOAD_FAILURE_UNKNOWN,
STARTER_TEMPLATE_NAME,
} from './constants';
@@ -51,6 +52,7 @@ export default {
failureReasons: [],
initialCiFileContent: '',
isFetchingCommitSha: false,
+ isLintUnavailable: false,
isNewCiConfigFile: false,
lastCommittedContent: '',
shouldSkipStartScreen: false,
@@ -147,10 +149,19 @@ export default {
return { ...ciConfig, stages };
},
result({ data }) {
- this.setAppStatus(data?.ciConfig?.status);
+ if (data?.ciConfig?.status) {
+ this.setAppStatus(data.ciConfig.status);
+ if (this.isLintUnavailable) {
+ this.isLintUnavailable = false;
+ }
+ }
},
- error(err) {
- this.reportFailure(LOAD_FAILURE_UNKNOWN, [String(err)]);
+ error() {
+ // We are not using `reportFailure` here because we don't
+ // need to bring attention to the linter being down. We let
+ // the user work on their file and if they look at their
+ // lint status, they will notice that the service is down
+ this.isLintUnavailable = true;
},
watchLoading(isLoading) {
if (isLoading) {
@@ -247,6 +258,13 @@ export default {
this.setAppStatus(EDITOR_APP_STATUS_EMPTY);
}
},
+ isLintUnavailable(flag) {
+ if (flag) {
+ // We cannot set this status directly in the `error`
+ // hook otherwise we get an infinite loop caused by apollo.
+ this.setAppStatus(EDITOR_APP_STATUS_LINT_UNAVAILABLE);
+ }
+ },
},
mounted() {
this.loadTemplateFromURL();
@@ -269,14 +287,10 @@ export default {
await this.$apollo.queries.initialCiFileContent.refetch();
},
reportFailure(type, reasons = []) {
- const isCurrentFailure = this.failureType === type && this.failureReasons[0] === reasons[0];
-
- if (!isCurrentFailure) {
- this.showFailure = true;
- this.failureType = type;
- this.failureReasons = reasons;
- window.scrollTo({ top: 0, behavior: 'smooth' });
- }
+ this.showFailure = true;
+ this.failureType = type;
+ this.failureReasons = reasons;
+ window.scrollTo({ top: 0, behavior: 'smooth' });
},
reportSuccess(type) {
window.scrollTo({ top: 0, behavior: 'smooth' });
@@ -289,7 +303,10 @@ export default {
},
setAppStatus(appStatus) {
if (EDITOR_APP_VALID_STATUSES.includes(appStatus)) {
- this.$apollo.mutate({ mutation: updateAppStatus, variables: { appStatus } });
+ this.$apollo.mutate({
+ mutation: updateAppStatus,
+ variables: { appStatus },
+ });
}
},
setNewEmptyCiConfigFile() {
diff --git a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
index 8e8f31a4acc..96680080f0c 100644
--- a/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/pipeline_editor/pipeline_editor_home.vue
@@ -90,7 +90,7 @@ export default {
</script>
<template>
- <div class="gl-pr-9 gl-transition-medium gl-w-full">
+ <div class="gl-pr-10 gl-transition-medium gl-w-full">
<gl-modal
v-if="showSwitchBranchModal"
visible
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
index 4db6a3c9fd8..8088858f381 100644
--- a/app/assets/javascripts/pipelines/components/header_component.vue
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -212,7 +212,9 @@ export default {
</script>
<template>
<div class="js-pipeline-header-container">
- <gl-alert v-if="hasError" :variant="failure.variant">{{ failure.text }}</gl-alert>
+ <gl-alert v-if="hasError" :variant="failure.variant" :dismissible="false">{{
+ failure.text
+ }}</gl-alert>
<ci-header
v-if="shouldRenderContent"
:status="pipeline.detailedStatus"
diff --git a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
index ffac8206b58..e11073aee33 100644
--- a/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
+++ b/app/assets/javascripts/pipelines/components/jobs/jobs_app.vue
@@ -112,7 +112,7 @@ export default {
</gl-skeleton-loader>
</div>
- <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" />
+ <jobs-table v-else :jobs="jobs" :table-fields="$options.fields" data-testid="jobs-tab-table" />
<gl-intersection-observer v-if="jobsPageInfo.hasNextPage" @appear="fetchMoreJobs">
<gl-loading-icon v-if="$apollo.loading" size="md" />
diff --git a/app/assets/javascripts/pipelines/components/parsing_utils.js b/app/assets/javascripts/pipelines/components/parsing_utils.js
index fa7330ce890..cae4e11c13f 100644
--- a/app/assets/javascripts/pipelines/components/parsing_utils.js
+++ b/app/assets/javascripts/pipelines/components/parsing_utils.js
@@ -1,5 +1,6 @@
import { memoize } from 'lodash';
import { createNodeDict } from '../utils';
+import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
import { createSankey } from './dag/drawing_utils';
/*
@@ -15,12 +16,14 @@ const deduplicate = (item, itemIndex, arr) => {
return foundIdx === itemIndex;
};
-export const makeLinksFromNodes = (nodes, nodeDict) => {
+export const makeLinksFromNodes = (nodes, nodeDict, { needsKey = NEEDS_PROPERTY } = {}) => {
const constantLinkValue = 10; // all links are the same weight
return nodes
.map(({ jobs, name: groupName }) =>
- jobs.map(({ needs = [] }) =>
- needs.reduce((acc, needed) => {
+ jobs.map((job) => {
+ const needs = job[needsKey] || [];
+
+ return needs.reduce((acc, needed) => {
// It's possible that we have an optional job, which
// is being needed by another job. In that scenario,
// the needed job doesn't exist, so we don't want to
@@ -34,8 +37,8 @@ export const makeLinksFromNodes = (nodes, nodeDict) => {
}
return acc;
- }, []),
- ),
+ }, []);
+ }),
)
.flat(2);
};
@@ -76,9 +79,9 @@ export const filterByAncestors = (links, nodeDict) =>
return !allAncestors.includes(source);
});
-export const parseData = (nodes) => {
- const nodeDict = createNodeDict(nodes);
- const allLinks = makeLinksFromNodes(nodes, nodeDict);
+export const parseData = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
+ const nodeDict = createNodeDict(nodes, { needsKey });
+ const allLinks = makeLinksFromNodes(nodes, nodeDict, { needsKey });
const filteredLinks = allLinks.filter(deduplicate);
const links = filterByAncestors(filteredLinks, nodeDict);
@@ -123,7 +126,8 @@ export const removeOrphanNodes = (sankeyfiedNodes) => {
export const listByLayers = ({ stages }) => {
const arrayOfJobs = stages.flatMap(({ groups }) => groups);
const parsedData = parseData(arrayOfJobs);
- const dataWithLayers = createSankey()(parsedData);
+ const explicitParsedData = parseData(arrayOfJobs, { needsKey: EXPLICIT_NEEDS_PROPERTY });
+ const dataWithLayers = createSankey()(explicitParsedData);
const pipelineLayers = dataWithLayers.nodes.reduce((acc, { layer, name }) => {
/* sort groups by layer */
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 64210576b29..8daf85e2b2e 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_graph/pipeline_graph.vue
@@ -132,6 +132,7 @@ export default {
:ref="$options.CONTAINER_REF"
class="gl-bg-gray-10 gl-overflow-auto"
data-testid="graph-container"
+ data-qa-selector="pipeline_graph_container"
>
<links-layer
:pipeline-data="pipelineStages"
@@ -147,7 +148,10 @@ export default {
:key="`${stage.name}-${index}`"
class="gl-flex-direction-column"
>
- <div class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5">
+ <div
+ class="gl-display-flex gl-align-items-center gl-w-full gl-px-9 gl-py-4 gl-mb-5"
+ data-qa-selector="stage_container"
+ >
<stage-name :stage-name="stage.name" />
</div>
<div :class="$options.jobWrapperClasses">
@@ -158,6 +162,7 @@ export default {
:pipeline-id="$options.PIPELINE_ID"
:is-hovered="highlightedJob === group.name"
:is-faded-out="isFadedOut(group.name)"
+ data-qa-selector="job_container"
@on-mouse-enter="setHoveredJob"
@on-mouse-leave="removeHoveredJob"
/>
diff --git a/app/assets/javascripts/pipelines/components/unwrapping_utils.js b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
index 2d24beb8323..d42a11c3aba 100644
--- a/app/assets/javascripts/pipelines/components/unwrapping_utils.js
+++ b/app/assets/javascripts/pipelines/components/unwrapping_utils.js
@@ -1,4 +1,5 @@
import { reportToSentry } from '../utils';
+import { EXPLICIT_NEEDS_PROPERTY, NEEDS_PROPERTY } from '../constants';
const unwrapGroups = (stages) => {
return stages.map((stage, idx) => {
@@ -27,12 +28,16 @@ const unwrapNodesWithName = (jobArray, prop, field = 'name') => {
}
return jobArray.map((job) => {
- return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
+ if (job[prop]) {
+ return { ...job, [prop]: job[prop].nodes.map((item) => item[field] || '') };
+ }
+ return job;
});
};
const unwrapJobWithNeeds = (denodedJobArray) => {
- return unwrapNodesWithName(denodedJobArray, 'needs');
+ const explicitNeedsUnwrapped = unwrapNodesWithName(denodedJobArray, EXPLICIT_NEEDS_PROPERTY);
+ return unwrapNodesWithName(explicitNeedsUnwrapped, NEEDS_PROPERTY);
};
const unwrapStagesWithNeedsAndLookup = (denodedStages) => {
diff --git a/app/assets/javascripts/pipelines/constants.js b/app/assets/javascripts/pipelines/constants.js
index d123f7a203c..410fc7b82cd 100644
--- a/app/assets/javascripts/pipelines/constants.js
+++ b/app/assets/javascripts/pipelines/constants.js
@@ -7,6 +7,8 @@ export const ANY_TRIGGER_AUTHOR = 'Any';
export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source'];
export const FILTER_TAG_IDENTIFIER = 'tag';
export const SCHEDULE_ORIGIN = 'schedule';
+export const NEEDS_PROPERTY = 'needs';
+export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
export const TestStatus = {
FAILED: 'failed',
diff --git a/app/assets/javascripts/pipelines/graphql/fragmentTypes.json b/app/assets/javascripts/pipelines/graphql/fragmentTypes.json
new file mode 100644
index 00000000000..4601b74b5c1
--- /dev/null
+++ b/app/assets/javascripts/pipelines/graphql/fragmentTypes.json
@@ -0,0 +1 @@
+{"__schema":{"types":[{"kind":"UNION","name":"JobNeedUnion","possibleTypes":[{"name":"CiBuildNeed"},{"name":"CiJob"}]}]}} \ No newline at end of file
diff --git a/app/assets/javascripts/pipelines/pipeline_shared_client.js b/app/assets/javascripts/pipelines/pipeline_shared_client.js
index c3be487caae..84276588d6a 100644
--- a/app/assets/javascripts/pipelines/pipeline_shared_client.js
+++ b/app/assets/javascripts/pipelines/pipeline_shared_client.js
@@ -1,10 +1,19 @@
import VueApollo from 'vue-apollo';
+import { IntrospectionFragmentMatcher } from 'apollo-cache-inmemory';
import createDefaultClient from '~/lib/graphql';
+import introspectionQueryResultData from './graphql/fragmentTypes.json';
+
+export const fragmentMatcher = new IntrospectionFragmentMatcher({
+ introspectionQueryResultData,
+});
export const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(
{},
{
+ cacheConfig: {
+ fragmentMatcher,
+ },
useGet: true,
},
),
diff --git a/app/assets/javascripts/pipelines/utils.js b/app/assets/javascripts/pipelines/utils.js
index e28eb74fb1b..f6e1c8b7412 100644
--- a/app/assets/javascripts/pipelines/utils.js
+++ b/app/assets/javascripts/pipelines/utils.js
@@ -1,6 +1,6 @@
import * as Sentry from '@sentry/browser';
import { pickBy } from 'lodash';
-import { SUPPORTED_FILTER_PARAMETERS } from './constants';
+import { SUPPORTED_FILTER_PARAMETERS, NEEDS_PROPERTY } from './constants';
/*
The following functions are the main engine in transforming the data as
@@ -35,11 +35,11 @@ import { SUPPORTED_FILTER_PARAMETERS } from './constants';
10 -> value (constant)
*/
-export const createNodeDict = (nodes) => {
+export const createNodeDict = (nodes, { needsKey = NEEDS_PROPERTY } = {}) => {
return nodes.reduce((acc, node) => {
const newNode = {
...node,
- needs: node.jobs.map((job) => job.needs || []).flat(),
+ needs: node.jobs.map((job) => job[needsKey] || []).flat(),
};
if (node.size > 1) {
diff --git a/app/assets/javascripts/profile/add_ssh_key_validation.js b/app/assets/javascripts/profile/add_ssh_key_validation.js
index 5c78de7ffb0..628dd159db8 100644
--- a/app/assets/javascripts/profile/add_ssh_key_validation.js
+++ b/app/assets/javascripts/profile/add_ssh_key_validation.js
@@ -1,8 +1,17 @@
export default class AddSshKeyValidation {
- constructor(inputElement, warningElement, originalSubmitElement, confirmSubmitElement) {
+ constructor(
+ supportedAlgorithms,
+ inputElement,
+ warningElement,
+ originalSubmitElement,
+ confirmSubmitElement,
+ ) {
this.inputElement = inputElement;
this.form = inputElement.form;
+ this.supportedAlgorithms = supportedAlgorithms;
+ this.publicKeyRegExp = new RegExp(`^(${this.supportedAlgorithms.join('|')})`);
+
this.warningElement = warningElement;
this.originalSubmitElement = originalSubmitElement;
@@ -23,7 +32,7 @@ export default class AddSshKeyValidation {
}
submit(event) {
- this.isValid = AddSshKeyValidation.isPublicKey(this.inputElement.value);
+ this.isValid = this.isPublicKey(this.inputElement.value);
if (this.isValid) return true;
@@ -37,7 +46,7 @@ export default class AddSshKeyValidation {
this.originalSubmitElement.classList.toggle('hide', isVisible);
}
- static isPublicKey(value) {
- return /^(ssh|ecdsa-sha2)-/.test(value);
+ isPublicKey(value) {
+ return this.publicKeyRegExp.test(value);
}
}
diff --git a/app/assets/javascripts/project_select_combo_button.js b/app/assets/javascripts/project_select_combo_button.js
index fd45d643ecc..09dbf2cee04 100644
--- a/app/assets/javascripts/project_select_combo_button.js
+++ b/app/assets/javascripts/project_select_combo_button.js
@@ -1,11 +1,12 @@
import $ from 'jquery';
+import { sprintf, __ } from '~/locale';
import AccessorUtilities from './lib/utils/accessor';
import { loadCSSFile } from './lib/utils/css_utils';
export default class ProjectSelectComboButton {
constructor(select) {
this.projectSelectInput = $(select);
- this.newItemBtn = $('.new-project-item-link');
+ this.newItemBtn = $('.js-new-project-item-link');
this.resourceType = this.newItemBtn.data('type');
this.resourceLabel = this.newItemBtn.data('label');
this.formattedText = this.deriveTextVariants();
@@ -80,9 +81,18 @@ export default class ProjectSelectComboButton {
setNewItemBtnAttributes(project) {
if (project) {
this.newItemBtn.attr('href', project.url);
- this.newItemBtn.text(`${this.formattedText.defaultTextPrefix} in ${project.name}`);
+ this.newItemBtn.text(
+ sprintf(__('New %{type} in %{project}'), {
+ type: this.resourceLabel,
+ project: project.name,
+ }),
+ );
} else {
- this.newItemBtn.text(`Select project to create ${this.formattedText.presetTextSuffix}`);
+ this.newItemBtn.text(
+ sprintf(__('Select project to create %{type}'), {
+ type: this.formattedText.presetTextSuffix,
+ }),
+ );
}
}
@@ -99,15 +109,12 @@ export default class ProjectSelectComboButton {
}
deriveTextVariants() {
- const defaultTextPrefix = this.resourceLabel;
-
// the trailing slice call depluralizes each of these strings (e.g. new-issues -> new-issue)
const localStorageItemType = `new-${this.resourceType.split('_').join('-').slice(0, -1)}`;
const presetTextSuffix = this.resourceType.split('_').join(' ').slice(0, -1);
return {
localStorageItemType, // new-issue / new-merge-request
- defaultTextPrefix, // New issue / New merge request
presetTextSuffix, // issue / merge request
};
}
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/projects/project_find_file.js
index d295c06928f..d295c06928f 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/projects/project_find_file.js
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/projects/project_import.js
index a51a2a2242f..27a218f1f52 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/projects/project_import.js
@@ -1,4 +1,4 @@
-import { visitUrl } from './lib/utils/url_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
export default function projectImport() {
setTimeout(() => {
diff --git a/app/assets/javascripts/project_visibility.js b/app/assets/javascripts/projects/project_visibility.js
index 1b57a69d464..c962554c9f4 100644
--- a/app/assets/javascripts/project_visibility.js
+++ b/app/assets/javascripts/projects/project_visibility.js
@@ -1,4 +1,6 @@
import $ from 'jquery';
+import { escape } from 'lodash';
+import { __, sprintf } from '~/locale';
import eventHub from '~/projects/new/event_hub';
// Values are from lib/gitlab/visibility_level.rb
@@ -25,10 +27,21 @@ function setVisibilityOptions({ name, visibility, showPath, editPath }) {
if (reason) {
const optionTitle = option.querySelector('.option-title');
const optionName = optionTitle ? optionTitle.innerText.toLowerCase() : '';
- reason.innerHTML = `This project cannot be ${optionName} because the visibility of
- <a href="${showPath}">${name}</a> is ${visibility}. To make this project
- ${optionName}, you must first <a href="${editPath}">change the visibility</a>
- of the parent group.`;
+ reason.innerHTML = sprintf(
+ __(
+ 'This project cannot be %{visibilityLevel} because the visibility of %{openShowLink}%{name}%{closeShowLink} is %{visibility}. To make this project %{visibilityLevel}, you must first %{openEditLink}change the visibility%{closeEditLink} of the parent group.',
+ ),
+ {
+ visibilityLevel: optionName,
+ name: escape(name),
+ visibility,
+ openShowLink: `<a href="${showPath}">`,
+ closeShowLink: '</a>',
+ openEditLink: `<a href="${editPath}">`,
+ closeEditLink: '</a>',
+ },
+ false,
+ );
}
} else {
option.classList.remove('disabled');
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/projects/star.js
index 7cba445d9b1..578e22ca25d 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/projects/star.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-import createFlash from './flash';
-import axios from './lib/utils/axios_utils';
-import { spriteIcon } from './lib/utils/common_utils';
-import { __, s__ } from './locale';
+import createFlash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
+import { spriteIcon } from '~/lib/utils/common_utils';
+import { __, s__ } from '~/locale';
export default class Star {
constructor(container = '.project-home-panel') {
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 58138655241..8b39851405e 100644
--- a/app/assets/javascripts/related_issues/components/related_issues_list.vue
+++ b/app/assets/javascripts/related_issues/components/related_issues_list.vue
@@ -110,7 +110,7 @@ export default {
v-for="issue in relatedIssues"
:key="issue.id"
:class="{
- 'user-can-drag': canReorder,
+ 'gl-cursor-grab': canReorder,
'sortable-row': canReorder,
'card card-slim': canReorder,
}"
diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue
index 6f540bf8ece..857795c71b0 100644
--- a/app/assets/javascripts/repository/components/blob_button_group.vue
+++ b/app/assets/javascripts/repository/components/blob_button_group.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButtonGroup, GlButton, GlModalDirective } from '@gitlab/ui';
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
import { uniqueId } from 'lodash';
import { sprintf, __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -20,9 +20,6 @@ export default {
DeleteBlobModal,
LockButton: () => import('ee_component/repository/components/lock_button.vue'),
},
- directives: {
- GlModal: GlModalDirective,
- },
mixins: [getRefMixin, glFeatureFlagMixin()],
inject: {
targetBranch: {
@@ -73,6 +70,10 @@ export default {
type: Boolean,
required: true,
},
+ showForkSuggestion: {
+ type: Boolean,
+ required: true,
+ },
},
computed: {
replaceModalId() {
@@ -91,6 +92,16 @@ export default {
return this.canLock ? 'lock_button' : 'disabled_lock_button';
},
},
+ methods: {
+ showModal(modalId) {
+ if (this.showForkSuggestion) {
+ this.$emit('fork');
+ return;
+ }
+
+ this.$refs[modalId].show();
+ },
+ },
};
</script>
@@ -107,14 +118,15 @@ export default {
data-testid="lock"
:data-qa-selector="lockBtnQASelector"
/>
- <gl-button v-gl-modal="replaceModalId" data-testid="replace">
+ <gl-button data-testid="replace" @click="showModal(replaceModalId)">
{{ $options.i18n.replace }}
</gl-button>
- <gl-button v-gl-modal="deleteModalId" data-testid="delete">
+ <gl-button data-testid="delete" @click="showModal(deleteModalId)">
{{ $options.i18n.delete }}
</gl-button>
</gl-button-group>
<upload-blob-modal
+ :ref="replaceModalId"
:modal-id="replaceModalId"
:modal-title="replaceModalTitle"
:commit-message="replaceModalTitle"
@@ -126,6 +138,7 @@ export default {
:primary-btn-text="$options.i18n.replacePrimaryBtnText"
/>
<delete-blob-modal
+ :ref="deleteModalId"
:modal-id="deleteModalId"
:modal-title="deleteModalTitle"
:delete-path="deletePath"
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index f3fa4526999..9368d7e6058 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -105,8 +105,10 @@ export default {
forkAndEditPath: '',
ideForkAndEditPath: '',
storedExternally: false,
+ externalStorage: '',
canModifyBlob: false,
canCurrentUserPushToBranch: false,
+ archived: false,
rawPath: '',
externalStorageUrl: '',
replacePath: '',
@@ -166,7 +168,7 @@ export default {
return pushCode && downloadCode;
},
pathLockedByUser() {
- const pathLock = this.project.pathLocks.nodes.find((node) => node.path === this.path);
+ const pathLock = this.project?.pathLocks?.nodes.find((node) => node.path === this.path);
return pathLock ? pathLock.user : null;
},
@@ -249,6 +251,7 @@ export default {
>
<template #actions>
<blob-edit
+ v-if="!blobInfo.archived"
:show-edit-button="!isBinaryFileType"
:edit-path="blobInfo.editBlobPath"
:web-ide-path="blobInfo.ideEditPath"
@@ -268,7 +271,7 @@ export default {
</gl-button>
<blob-button-group
- v-if="isLoggedIn"
+ v-if="isLoggedIn && !blobInfo.archived"
:path="path"
:name="blobInfo.name"
:replace-path="blobInfo.replacePath"
@@ -279,6 +282,8 @@ export default {
:project-path="projectPath"
:is-locked="Boolean(pathLockedByUser)"
:can-lock="canLock"
+ :show-fork-suggestion="showForkSuggestion"
+ @fork="setForkTarget('ide')"
/>
</template>
</blob-header>
@@ -289,6 +294,7 @@ export default {
/>
<blob-content
v-if="!blobViewer"
+ class="js-syntax-highlight"
:rich-viewer="legacyRichViewer"
:blob="blobInfo"
:content="legacySimpleViewer"
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
new file mode 100644
index 00000000000..3223ed92fe2
--- /dev/null
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -0,0 +1,119 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __ } from '~/locale';
+import createFlash from '~/flash';
+import getRefMixin from '~/repository/mixins/get_ref';
+import initSourcegraph from '~/sourcegraph';
+import { updateElementsVisibility } from '../utils/dom';
+import blobControlsQuery from '../queries/blob_controls.query.graphql';
+
+export default {
+ i18n: {
+ findFile: __('Find file'),
+ blame: __('Blame'),
+ history: __('History'),
+ permalink: __('Permalink'),
+ errorMessage: __('An error occurred while loading the blob controls.'),
+ },
+ buttonClassList: 'gl-sm-w-auto gl-w-full gl-sm-mt-0 gl-mt-3',
+ components: {
+ GlButton,
+ },
+ mixins: [getRefMixin],
+ apollo: {
+ project: {
+ query: blobControlsQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ filePath: this.filePath,
+ ref: this.ref,
+ };
+ },
+ skip() {
+ return !this.filePath;
+ },
+ error() {
+ createFlash({ message: this.$options.i18n.errorMessage });
+ },
+ },
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ project: {
+ repository: {
+ blobs: {
+ nodes: [
+ {
+ findFilePath: null,
+ blamePath: null,
+ historyPath: null,
+ permalinkPath: null,
+ storedExternally: null,
+ externalStorage: null,
+ },
+ ],
+ },
+ },
+ },
+ };
+ },
+ computed: {
+ filePath() {
+ return this.$route.params.path;
+ },
+ showBlobControls() {
+ return this.filePath && this.$route.name === 'blobPathDecoded';
+ },
+ blobInfo() {
+ return this.project?.repository?.blobs?.nodes[0] || {};
+ },
+ showBlameButton() {
+ return !this.blobInfo.storedExternally && this.blobInfo.externalStorage !== 'lfs';
+ },
+ },
+ watch: {
+ showBlobControls(shouldShow) {
+ updateElementsVisibility('.tree-controls', !shouldShow);
+ },
+ blobInfo() {
+ initSourcegraph();
+ },
+ },
+};
+</script>
+
+<template>
+ <div v-if="showBlobControls">
+ <gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList">
+ {{ $options.i18n.findFile }}
+ </gl-button>
+ <gl-button
+ v-if="showBlameButton"
+ data-testid="blame"
+ :href="blobInfo.blamePath"
+ :class="$options.buttonClassList"
+ >
+ {{ $options.i18n.blame }}
+ </gl-button>
+
+ <gl-button data-testid="history" :href="blobInfo.historyPath" :class="$options.buttonClassList">
+ {{ $options.i18n.history }}
+ </gl-button>
+
+ <gl-button
+ data-testid="permalink"
+ :href="blobInfo.permalinkPath"
+ :class="$options.buttonClassList"
+ class="js-data-file-blob-permalink-url"
+ >
+ {{ $options.i18n.permalink }}
+ </gl-button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repository/components/blob_edit.vue b/app/assets/javascripts/repository/components/blob_edit.vue
index fd377ba1b81..69e2bd563c9 100644
--- a/app/assets/javascripts/repository/components/blob_edit.vue
+++ b/app/assets/javascripts/repository/components/blob_edit.vue
@@ -50,6 +50,7 @@ export default {
:web-ide-url="webIdePath"
:needs-to-fork="needsToFork"
:is-blob="true"
+ disable-fork-modal
@edit="onEdit"
/>
<div v-else>
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 0d3dc06c2c8..f3c9aea36f1 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -146,6 +146,9 @@ export default {
/* eslint-enable dot-notation */
},
methods: {
+ show() {
+ this.$refs[this.modalId].show();
+ },
submitForm(e) {
e.preventDefault(); // Prevent modal from closing
this.form.showValidation = true;
@@ -164,6 +167,7 @@ export default {
<template>
<gl-modal
+ :ref="modalId"
v-bind="$attrs"
data-testid="modal-delete"
:modal-id="modalId"
diff --git a/app/assets/javascripts/repository/components/fork_suggestion.vue b/app/assets/javascripts/repository/components/fork_suggestion.vue
index c266bea319b..471f1dad2e3 100644
--- a/app/assets/javascripts/repository/components/fork_suggestion.vue
+++ b/app/assets/javascripts/repository/components/fork_suggestion.vue
@@ -32,6 +32,7 @@ export default {
class="gl-mr-3"
category="secondary"
variant="confirm"
+ data-method="post"
:href="forkPath"
data-testid="fork"
>
diff --git a/app/assets/javascripts/repository/components/preview/index.vue b/app/assets/javascripts/repository/components/preview/index.vue
index c6e461b10e0..dc5a031c9f3 100644
--- a/app/assets/javascripts/repository/components/preview/index.vue
+++ b/app/assets/javascripts/repository/components/preview/index.vue
@@ -47,6 +47,9 @@ export default {
}
},
},
+ safeHtmlConfig: {
+ ADD_TAGS: ['copy-code'],
+ },
};
</script>
@@ -62,7 +65,11 @@ export default {
</div>
<div class="blob-viewer" data-qa-selector="blob_viewer_content" itemprop="about">
<gl-loading-icon v-if="loading > 0" size="md" color="dark" class="my-4 mx-auto" />
- <div v-else-if="readme" ref="readme" v-safe-html="readme.html"></div>
+ <div
+ v-else-if="readme"
+ ref="readme"
+ v-safe-html:[$options.safeHtmlConfig]="readme.html"
+ ></div>
</div>
</article>
</template>
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index b56c9ce5247..7fcaf772aac 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -136,6 +136,9 @@ export default {
},
},
methods: {
+ show() {
+ this.$refs[this.modalId].show();
+ },
setFile(file) {
this.file = file;
@@ -206,6 +209,7 @@ export default {
<template>
<gl-form>
<gl-modal
+ :ref="modalId"
:modal-id="modalId"
:title="modalTitle"
:action-primary="primaryOptions"
diff --git a/app/assets/javascripts/repository/index.js b/app/assets/javascripts/repository/index.js
index 197b19387cf..120c32caefd 100644
--- a/app/assets/javascripts/repository/index.js
+++ b/app/assets/javascripts/repository/index.js
@@ -9,6 +9,7 @@ import App from './components/app.vue';
import Breadcrumbs from './components/breadcrumbs.vue';
import DirectoryDownloadLinks from './components/directory_download_links.vue';
import LastCommit from './components/last_commit.vue';
+import BlobControls from './components/blob_controls.vue';
import apolloProvider from './graphql';
import commitsQuery from './queries/commits.query.graphql';
import projectPathQuery from './queries/project_path.query.graphql';
@@ -71,8 +72,26 @@ export default function setupVueRepositoryList() {
},
});
+ const initBlobControlsApp = () =>
+ new Vue({
+ el: document.getElementById('js-blob-controls'),
+ router,
+ apolloProvider,
+ render(h) {
+ return h(BlobControls, {
+ props: {
+ projectPath,
+ },
+ });
+ },
+ });
+
initLastCommitApp();
+ if (gon.features.refactorBlobViewer) {
+ initBlobControlsApp();
+ }
+
router.afterEach(({ params: { path } }) => {
setTitle(path, ref, fullName);
});
@@ -144,7 +163,7 @@ export default function setupVueRepositoryList() {
}`,
// Ideally passing this class to `props` should work
// But it doesn't work here. :(
- class: 'btn btn-default btn-md gl-button ml-sm-0',
+ class: 'btn btn-default btn-md gl-button',
},
},
[__('History')],
diff --git a/app/assets/javascripts/repository/queries/blob_controls.query.graphql b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
new file mode 100644
index 00000000000..fc1cf5f254b
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/blob_controls.query.graphql
@@ -0,0 +1,18 @@
+query getBlobControls($projectPath: ID!, $filePath: String!, $ref: String!) {
+ project(fullPath: $projectPath) {
+ id
+ repository {
+ blobs(paths: [$filePath], ref: $ref) {
+ nodes {
+ id
+ findFilePath
+ blamePath
+ historyPath
+ permalinkPath
+ storedExternally
+ externalStorage
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql
index 45d1ba80917..ae20a0f0bc4 100644
--- a/app/assets/javascripts/repository/queries/blob_info.query.graphql
+++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql
@@ -1,22 +1,14 @@
+#import "ee_else_ce/repository/queries/path_locks.fragment.graphql"
+
query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
project(fullPath: $projectPath) {
- id
userPermissions {
pushCode
downloadCode
createMergeRequestIn
forkProject
}
- pathLocks {
- nodes {
- id
- path
- user {
- id
- username
- }
- }
- }
+ ...ProjectPathLocksFragment
repository {
empty
blobs(paths: [$filePath], ref: $ref) {
@@ -35,7 +27,9 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) {
ideForkAndEditPath
canModifyBlob
canCurrentUserPushToBranch
+ archived
storedExternally
+ externalStorage
rawPath
replacePath
pipelineEditorPath
diff --git a/app/assets/javascripts/repository/queries/path_locks.fragment.graphql b/app/assets/javascripts/repository/queries/path_locks.fragment.graphql
new file mode 100644
index 00000000000..868a513362d
--- /dev/null
+++ b/app/assets/javascripts/repository/queries/path_locks.fragment.graphql
@@ -0,0 +1,3 @@
+fragment ProjectPathLocksFragment on Project {
+ id
+}
diff --git a/app/assets/javascripts/runner/runner_details/runner_details_app.vue b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue
index 6557a7834e7..4d2ca9b0c58 100644
--- a/app/assets/javascripts/runner/runner_details/runner_details_app.vue
+++ b/app/assets/javascripts/runner/admin_runner_edit/admin_runner_edit_app.vue
@@ -1,20 +1,17 @@
<script>
-import createFlash from '~/flash';
+import { createAlert } 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 RunnerHeader from '../components/runner_header.vue';
import RunnerUpdateForm from '../components/runner_update_form.vue';
-import { I18N_DETAILS_TITLE, I18N_FETCH_ERROR } from '../constants';
+import { I18N_FETCH_ERROR } from '../constants';
import getRunnerQuery from '../graphql/get_runner.query.graphql';
import { captureException } from '../sentry_utils';
export default {
- name: 'RunnerDetailsApp',
+ name: 'AdminRunnerEditApp',
components: {
- RunnerTypeAlert,
- RunnerTypeBadge,
+ RunnerHeader,
RunnerUpdateForm,
},
props: {
@@ -37,17 +34,12 @@ export default {
};
},
error(error) {
- createFlash({ message: I18N_FETCH_ERROR });
+ createAlert({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
},
- computed: {
- pageTitle() {
- return sprintf(I18N_DETAILS_TITLE, { runner_id: this.runnerId });
- },
- },
errorCaptured(error) {
this.reportToSentry(error);
},
@@ -60,12 +52,7 @@ export default {
</script>
<template>
<div>
- <h2 class="page-title">
- {{ pageTitle }} <runner-type-badge v-if="runner" :type="runner.runnerType" />
- </h2>
-
- <runner-type-alert v-if="runner" :type="runner.runnerType" />
-
+ <runner-header v-if="runner" :runner="runner" />
<runner-update-form :runner="runner" class="gl-my-5" />
</div>
</template>
diff --git a/app/assets/javascripts/runner/runner_details/index.js b/app/assets/javascripts/runner/admin_runner_edit/index.js
index db8f239a3c3..adb420f9963 100644
--- a/app/assets/javascripts/runner/runner_details/index.js
+++ b/app/assets/javascripts/runner/admin_runner_edit/index.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
-import RunnerDetailsApp from './runner_details_app.vue';
+import AdminRunnerEditApp from './admin_runner_edit_app.vue';
Vue.use(VueApollo);
-export const initRunnerDetail = (selector = '#js-runner-details') => {
+export const initAdminRunnerEdit = (selector = '#js-admin-runner-edit') => {
const el = document.querySelector(selector);
if (!el) {
@@ -22,7 +22,7 @@ export const initRunnerDetail = (selector = '#js-runner-details') => {
el,
apolloProvider,
render(h) {
- return h(RunnerDetailsApp, {
+ return h(AdminRunnerEditApp, {
props: {
runnerId,
},
diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
index f8220553db6..bb2bac531a7 100644
--- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
+++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue
@@ -1,14 +1,15 @@
<script>
import { GlBadge, GlLink } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
+import { formatNumber } from '~/locale';
import RegistrationDropdown from '../components/registration/registration_dropdown.vue';
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
-import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
+import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -19,9 +20,13 @@ import {
INSTANCE_TYPE,
GROUP_TYPE,
PROJECT_TYPE,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
I18N_FETCH_ERROR,
} from '../constants';
import getRunnersQuery from '../graphql/get_runners.query.graphql';
+import getRunnersCountQuery from '../graphql/get_runners_count.query.graphql';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
@@ -29,6 +34,17 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
+const runnersCountSmartQuery = {
+ query: getRunnersCountQuery,
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ return data?.runners?.count;
+ },
+ error(error) {
+ this.reportToSentry(error);
+ },
+};
+
export default {
name: 'AdminRunnersApp',
components: {
@@ -38,7 +54,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
- RunnerOnlineStat,
+ RunnerStats,
RunnerPagination,
RunnerTypeTabs,
},
@@ -47,26 +63,6 @@ export default {
type: String,
required: true,
},
- activeRunnersCount: {
- type: String,
- required: true,
- },
- allRunnersCount: {
- type: String,
- required: true,
- },
- instanceRunnersCount: {
- type: String,
- required: true,
- },
- groupRunnersCount: {
- type: String,
- required: true,
- },
- projectRunnersCount: {
- type: String,
- required: true,
- },
},
data() {
return {
@@ -95,16 +91,78 @@ export default {
};
},
error(error) {
- createFlash({ message: I18N_FETCH_ERROR });
+ createAlert({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
+ allRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return this.countVariables;
+ },
+ },
+ instanceRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: INSTANCE_TYPE,
+ };
+ },
+ },
+ groupRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: GROUP_TYPE,
+ };
+ },
+ },
+ projectRunnersCount: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ ...this.countVariables,
+ type: PROJECT_TYPE,
+ };
+ },
+ },
+ onlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_ONLINE,
+ };
+ },
+ },
+ offlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_OFFLINE,
+ };
+ },
+ },
+ staleRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ status: STATUS_STALE,
+ };
+ },
+ },
},
computed: {
variables() {
return fromSearchToVariables(this.search);
},
+ countVariables() {
+ // Exclude pagination variables, leave only filters variables
+ const { sort, before, last, after, first, ...countVariables } = this.variables;
+ return countVariables;
+ },
runnersLoading() {
return this.$apollo.queries.runners.loading;
},
@@ -125,7 +183,7 @@ export default {
search: {
deep: true,
handler() {
- // TODO Implement back button reponse using onpopstate
+ // TODO Implement back button response using onpopstate
updateHistory({
url: fromSearchToUrl(this.search),
title: document.title,
@@ -138,18 +196,27 @@ export default {
},
methods: {
tabCount({ runnerType }) {
+ let count;
switch (runnerType) {
case null:
- return this.allRunnersCount;
+ count = this.allRunnersCount;
+ break;
case INSTANCE_TYPE:
- return this.instanceRunnersCount;
+ count = this.instanceRunnersCount;
+ break;
case GROUP_TYPE:
- return this.groupRunnersCount;
+ count = this.groupRunnersCount;
+ break;
case PROJECT_TYPE:
- return this.projectRunnersCount;
+ count = this.projectRunnersCount;
+ break;
default:
return null;
}
+ if (typeof count === 'number') {
+ return formatNumber(count);
+ }
+ return '';
},
reportToSentry(error) {
captureException({ error, component: this.$options.name });
@@ -161,7 +228,11 @@ export default {
</script>
<template>
<div>
- <runner-online-stat class="gl-py-6 gl-px-5" :value="activeRunnersCount" />
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
<div
class="gl-display-flex gl-align-items-center gl-flex-direction-column-reverse gl-md-flex-direction-row gl-mt-3 gl-md-mt-0"
diff --git a/app/assets/javascripts/runner/admin_runners/index.js b/app/assets/javascripts/runner/admin_runners/index.js
index 62da6cbfa2b..3b8a8fe9cd1 100644
--- a/app/assets/javascripts/runner/admin_runners/index.js
+++ b/app/assets/javascripts/runner/admin_runners/index.js
@@ -2,6 +2,8 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { updateOutdatedUrl } from '~/runner/runner_search_utils';
import AdminRunnersApp from './admin_runners_app.vue';
Vue.use(GlToast);
@@ -14,18 +16,16 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return null;
}
- // TODO `activeRunnersCount` should be implemented using a GraphQL API
- // https://gitlab.com/gitlab-org/gitlab/-/issues/333806
- const {
- runnerInstallHelpPage,
- registrationToken,
+ // Redirect outdated URLs
+ const updatedUrlQuery = updateOutdatedUrl();
+ if (updatedUrlQuery) {
+ visitUrl(updatedUrlQuery);
- activeRunnersCount,
- allRunnersCount,
- instanceRunnersCount,
- groupRunnersCount,
- projectRunnersCount,
- } = el.dataset;
+ // Prevent mounting the rest of the app, redirecting now.
+ return null;
+ }
+
+ const { runnerInstallHelpPage, registrationToken } = el.dataset;
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -41,14 +41,6 @@ export const initAdminRunners = (selector = '#js-admin-runners') => {
return h(AdminRunnersApp, {
props: {
registrationToken,
-
- // All runner counts are returned as formatted
- // strings, we do not use `parseInt`.
- activeRunnersCount,
- allRunnersCount,
- instanceRunnersCount,
- groupRunnersCount,
- projectRunnersCount,
},
});
},
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 33f7a67aba4..0934508c87f 100644
--- a/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_actions_cell.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlButtonGroup, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { __, s__, sprintf } from '~/locale';
import runnerDeleteMutation from '~/runner/graphql/runner_delete.mutation.graphql';
import runnerActionsUpdateMutation from '~/runner/graphql/runner_actions_update.mutation.graphql';
@@ -69,6 +69,12 @@ export default {
runnerDeleteModalId() {
return `delete-runner-modal-${this.runnerId}`;
},
+ canUpdate() {
+ return this.runner.userPermissions?.updateRunner;
+ },
+ canDelete() {
+ return this.runner.userPermissions?.deleteRunner;
+ },
},
methods: {
async onToggleActive() {
@@ -133,7 +139,7 @@ export default {
onError(error) {
const { message } = error;
- createFlash({ message });
+ createAlert({ message });
this.reportToSentry(error);
},
@@ -156,14 +162,15 @@ export default {
See https://gitlab.com/gitlab-org/gitlab/-/issues/334802
-->
<gl-button
- v-if="runner.adminUrl"
+ v-if="canUpdate && runner.editAdminUrl"
v-gl-tooltip.hover.viewport="$options.I18N_EDIT"
- :href="runner.adminUrl"
+ :href="runner.editAdminUrl"
:aria-label="$options.I18N_EDIT"
icon="pencil"
data-testid="edit-runner"
/>
<gl-button
+ v-if="canUpdate"
v-gl-tooltip.hover.viewport="toggleActiveTitle"
:aria-label="toggleActiveTitle"
:icon="toggleActiveIcon"
@@ -172,6 +179,7 @@ export default {
@click="onToggleActive"
/>
<gl-button
+ v-if="canDelete"
v-gl-tooltip.hover.viewport="deleteTitle"
v-gl-modal="runnerDeleteModalId"
:aria-label="deleteTitle"
@@ -182,6 +190,7 @@ export default {
/>
<runner-delete-modal
+ v-if="canDelete"
:ref="runnerDeleteModalId"
:modal-id="runnerDeleteModalId"
:runner-name="runnerName"
diff --git a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
index 473cd7e9794..93f86ae2a2c 100644
--- a/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/runner/components/cells/runner_status_cell.vue
@@ -28,7 +28,15 @@ export default {
<template>
<div>
- <runner-status-badge :runner="runner" size="sm" />
- <runner-paused-badge v-if="paused" size="sm" />
+ <runner-status-badge
+ :runner="runner"
+ size="sm"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ />
+ <runner-paused-badge
+ v-if="paused"
+ size="sm"
+ class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
index 3bb15bff8d8..0e259807f98 100644
--- a/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
+++ b/app/assets/javascripts/runner/components/registration/registration_token_reset_dropdown_item.vue
@@ -1,6 +1,6 @@
<script>
-import { GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { GlDropdownItem, GlLoadingIcon, GlModal, GlModalDirective } from '@gitlab/ui';
+import { createAlert } from '~/flash';
import { TYPE_GROUP, TYPE_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { __, s__ } from '~/locale';
@@ -10,9 +10,17 @@ import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../../constants';
export default {
name: 'RunnerRegistrationTokenReset',
+ i18n: {
+ modalTitle: __('Reset registration token'),
+ modalCopy: __('Are you sure you want to reset the registration token?'),
+ },
components: {
GlDropdownItem,
GlLoadingIcon,
+ GlModal,
+ },
+ directives: {
+ GlModal: GlModalDirective,
},
inject: {
groupId: {
@@ -22,6 +30,7 @@ export default {
default: null,
},
},
+ modalID: 'token-reset-modal',
props: {
type: {
type: String,
@@ -59,14 +68,10 @@ export default {
},
},
methods: {
+ handleModalPrimary() {
+ this.resetToken();
+ },
async resetToken() {
- // TODO Replace confirmation with gl-modal
- // See: https://gitlab.com/gitlab-org/gitlab/-/issues/333810
- // eslint-disable-next-line no-alert
- if (!window.confirm(__('Are you sure you want to reset the registration token?'))) {
- return;
- }
-
this.loading = true;
try {
const {
@@ -91,7 +96,7 @@ export default {
},
onError(error) {
const { message } = error;
- createFlash({ message });
+ createAlert({ message });
this.reportToSentry(error);
},
@@ -106,8 +111,15 @@ export default {
};
</script>
<template>
- <gl-dropdown-item @click.capture.native.stop="resetToken">
+ <gl-dropdown-item v-gl-modal="$options.modalID">
{{ __('Reset registration token') }}
+ <gl-modal
+ :modal-id="$options.modalID"
+ :title="$options.i18n.modalTitle"
+ @primary="handleModalPrimary"
+ >
+ <p>{{ $options.i18n.modalCopy }}</p>
+ </gl-modal>
<gl-loading-icon v-if="loading" inline />
</gl-dropdown-item>
</template>
diff --git a/app/assets/javascripts/runner/components/runner_header.vue b/app/assets/javascripts/runner/components/runner_header.vue
new file mode 100644
index 00000000000..09f58df7bd0
--- /dev/null
+++ b/app/assets/javascripts/runner/components/runner_header.vue
@@ -0,0 +1,52 @@
+<script>
+import { GlSprintf } from '@gitlab/ui';
+import { sprintf } from '~/locale';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { I18N_DETAILS_TITLE } from '../constants';
+import RunnerTypeBadge from './runner_type_badge.vue';
+import RunnerStatusBadge from './runner_status_badge.vue';
+
+export default {
+ components: {
+ GlSprintf,
+ TimeAgo,
+ RunnerTypeBadge,
+ RunnerStatusBadge,
+ },
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ paused() {
+ return !this.runner.active;
+ },
+ heading() {
+ const id = getIdFromGraphQLId(this.runner.id);
+ return sprintf(I18N_DETAILS_TITLE, { runner_id: id });
+ },
+ },
+};
+</script>
+<template>
+ <div class="gl-py-5 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100">
+ <runner-status-badge :runner="runner" />
+ <runner-type-badge v-if="runner" :type="runner.runnerType" />
+ <template v-if="runner.createdAt">
+ <gl-sprintf :message="__('%{runner} created %{timeago}')">
+ <template #runner>
+ <strong>{{ heading }}</strong>
+ </template>
+ <template #timeago>
+ <time-ago :time="runner.createdAt" />
+ </template>
+ </gl-sprintf>
+ </template>
+ <template v-else>
+ <strong>{{ heading }}</strong>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/runner_status_badge.vue b/app/assets/javascripts/runner/components/runner_status_badge.vue
index 0823876a187..6d0445ecb7a 100644
--- a/app/assets/javascripts/runner/components/runner_status_badge.vue
+++ b/app/assets/javascripts/runner/components/runner_status_badge.vue
@@ -4,11 +4,10 @@ import { __, s__, sprintf } from '~/locale';
import { getTimeago } from '~/lib/utils/datetime_utility';
import {
I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION,
- I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION,
I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION,
I18N_STALE_RUNNER_DESCRIPTION,
STATUS_ONLINE,
- STATUS_NOT_CONNECTED,
STATUS_NEVER_CONTACTED,
STATUS_OFFLINE,
STATUS_STALE,
@@ -45,12 +44,11 @@ export default {
timeAgo: this.contactedAtTimeAgo,
}),
};
- case STATUS_NOT_CONNECTED:
case STATUS_NEVER_CONTACTED:
return {
variant: 'muted',
- label: s__('Runners|not connected'),
- tooltip: I18N_NOT_CONNECTED_RUNNER_DESCRIPTION,
+ label: s__('Runners|never contacted'),
+ tooltip: I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION,
};
case STATUS_OFFLINE:
return {
diff --git a/app/assets/javascripts/runner/components/runner_type_alert.vue b/app/assets/javascripts/runner/components/runner_type_alert.vue
deleted file mode 100644
index 1400875a1d6..00000000000
--- a/app/assets/javascripts/runner/components/runner_type_alert.vue
+++ /dev/null
@@ -1,54 +0,0 @@
-<script>
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { s__ } from '~/locale';
-import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '../constants';
-
-const ALERT_DATA = {
- [INSTANCE_TYPE]: {
- message: s__(
- 'Runners|This runner is available to all groups and projects in your GitLab instance.',
- ),
- anchor: 'shared-runners',
- },
- [GROUP_TYPE]: {
- message: s__('Runners|This runner is available to all projects and subgroups in a group.'),
- anchor: 'group-runners',
- },
- [PROJECT_TYPE]: {
- message: s__('Runners|This runner is associated with one or more projects.'),
- anchor: 'specific-runners',
- },
-};
-
-export default {
- components: {
- GlAlert,
- GlLink,
- },
- props: {
- type: {
- type: String,
- required: false,
- default: null,
- validator(type) {
- return Boolean(ALERT_DATA[type]);
- },
- },
- },
- computed: {
- alert() {
- return ALERT_DATA[this.type];
- },
- helpHref() {
- return helpPagePath('ci/runners/runners_scope', { anchor: this.alert.anchor });
- },
- },
-};
-</script>
-<template>
- <gl-alert v-if="alert" variant="info" :dismissible="false">
- {{ alert.message }}
- <gl-link :href="helpHref">{{ __('Learn more.') }}</gl-link>
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/runner/components/runner_update_form.vue b/app/assets/javascripts/runner/components/runner_update_form.vue
index 9a6fc07f6dd..e3deb94236e 100644
--- a/app/assets/javascripts/runner/components/runner_update_form.vue
+++ b/app/assets/javascripts/runner/components/runner_update_form.vue
@@ -10,8 +10,8 @@ import {
import {
modelToUpdateMutationVariables,
runnerToModel,
-} from 'ee_else_ce/runner/runner_details/runner_update_form_utils';
-import createFlash, { FLASH_TYPES } from '~/flash';
+} from 'ee_else_ce/runner/runner_update_form_utils';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { __ } from '~/locale';
import { captureException } from '~/runner/sentry_utils';
import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED, PROJECT_TYPE } from '../constants';
@@ -75,14 +75,14 @@ export default {
if (errors?.length) {
// Validation errors need not be thrown
- createFlash({ message: errors[0] });
+ createAlert({ message: errors[0] });
return;
}
this.onSuccess();
} catch (error) {
const { message } = error;
- createFlash({ message });
+ createAlert({ message });
this.reportToSentry(error);
} finally {
@@ -90,7 +90,7 @@ export default {
}
},
onSuccess() {
- createFlash({ message: __('Changes saved.'), type: FLASH_TYPES.SUCCESS });
+ createAlert({ message: __('Changes saved.'), variant: VARIANT_SUCCESS });
this.model = runnerToModel(this.runner);
},
reportToSentry(error) {
diff --git a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
index 4b356fa47ed..79038eb8228 100644
--- a/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
+++ b/app/assets/javascripts/runner/components/search_tokens/status_token_config.js
@@ -6,7 +6,7 @@ import {
STATUS_PAUSED,
STATUS_ONLINE,
STATUS_OFFLINE,
- STATUS_NOT_CONNECTED,
+ STATUS_NEVER_CONTACTED,
STATUS_STALE,
PARAM_KEY_STATUS,
} from '../../constants';
@@ -16,7 +16,7 @@ const options = [
{ value: STATUS_PAUSED, title: s__('Runners|Paused') },
{ value: STATUS_ONLINE, title: s__('Runners|Online') },
{ value: STATUS_OFFLINE, title: s__('Runners|Offline') },
- { value: STATUS_NOT_CONNECTED, title: s__('Runners|Not connected') },
+ { value: STATUS_NEVER_CONTACTED, title: s__('Runners|Never contacted') },
{ value: STATUS_STALE, title: s__('Runners|Stale') },
];
diff --git a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
index 7461308ab91..59230bb809e 100644
--- a/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
+++ b/app/assets/javascripts/runner/components/search_tokens/tag_token.vue
@@ -1,6 +1,6 @@
<script>
import { GlFilteredSearchSuggestion, GlToken } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { s__ } from '~/locale';
@@ -50,7 +50,7 @@ export default {
try {
this.tags = await this.getTagsOptions(searchTerm);
} catch {
- createFlash({
+ createAlert({
message: s__('Runners|Something went wrong while fetching the tags suggestions'),
});
} finally {
diff --git a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue b/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
deleted file mode 100644
index b92b9badef0..00000000000
--- a/app/assets/javascripts/runner/components/stat/runner_online_stat.vue
+++ /dev/null
@@ -1,17 +0,0 @@
-<script>
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-
-export default {
- components: {
- GlSingleStat,
- },
-};
-</script>
-<template>
- <gl-single-stat
- v-bind="$attrs"
- variant="success"
- :title="s__('Runners|Online Runners')"
- :meta-text="s__('Runners|online')"
- />
-</template>
diff --git a/app/assets/javascripts/runner/components/stat/runner_stats.vue b/app/assets/javascripts/runner/components/stat/runner_stats.vue
new file mode 100644
index 00000000000..d3693ee593e
--- /dev/null
+++ b/app/assets/javascripts/runner/components/stat/runner_stats.vue
@@ -0,0 +1,49 @@
+<script>
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
+import RunnerStatusStat from './runner_status_stat.vue';
+
+export default {
+ components: {
+ RunnerStatusStat,
+ },
+ props: {
+ onlineRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ offlineRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ staleRunnersCount: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ },
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-py-6">
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_ONLINE"
+ :value="onlineRunnersCount"
+ />
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_OFFLINE"
+ :value="offlineRunnersCount"
+ />
+ <runner-status-stat
+ class="gl-px-5"
+ :status="$options.STATUS_STALE"
+ :value="staleRunnersCount"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/runner/components/stat/runner_status_stat.vue b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue
new file mode 100644
index 00000000000..b77bbe15541
--- /dev/null
+++ b/app/assets/javascripts/runner/components/stat/runner_status_stat.vue
@@ -0,0 +1,65 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+import { s__, formatNumber } from '~/locale';
+import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE } from '../../constants';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+ props: {
+ value: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ status: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ formattedValue() {
+ if (typeof this.value === 'number') {
+ return formatNumber(this.value);
+ }
+ return '-';
+ },
+ stat() {
+ switch (this.status) {
+ case STATUS_ONLINE:
+ return {
+ variant: 'success',
+ title: s__('Runners|Online runners'),
+ metaText: s__('Runners|online'),
+ };
+ case STATUS_OFFLINE:
+ return {
+ variant: 'muted',
+ title: s__('Runners|Offline runners'),
+ metaText: s__('Runners|offline'),
+ };
+ case STATUS_STALE:
+ return {
+ variant: 'warning',
+ title: s__('Runners|Stale runners'),
+ metaText: s__('Runners|stale'),
+ };
+ default:
+ return {
+ title: s__('Runners|Runners'),
+ };
+ }
+ },
+ },
+};
+</script>
+<template>
+ <gl-single-stat
+ v-if="stat"
+ :value="formattedValue"
+ :variant="stat.variant"
+ :title="stat.title"
+ :meta-text="stat.metaText"
+ />
+</template>
diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js
index 355f3054917..ce8019ffaa0 100644
--- a/app/assets/javascripts/runner/constants.js
+++ b/app/assets/javascripts/runner/constants.js
@@ -18,8 +18,8 @@ export const I18N_PROJECT_RUNNER_DESCRIPTION = s__('Runners|Associated with one
export const I18N_ONLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
'Runners|Runner is online; last contact was %{timeAgo}',
);
-export const I18N_NOT_CONNECTED_RUNNER_DESCRIPTION = s__(
- 'Runners|This runner has never connected to this instance',
+export const I18N_NEVER_CONTACTED_RUNNER_DESCRIPTION = s__(
+ 'Runners|This runner has never contacted this instance',
);
export const I18N_OFFLINE_RUNNER_TIMEAGO_DESCRIPTION = s__(
'Runners|No recent contact from this runner; last contact was %{timeAgo}',
@@ -60,7 +60,6 @@ export const STATUS_ACTIVE = 'ACTIVE';
export const STATUS_PAUSED = 'PAUSED';
export const STATUS_ONLINE = 'ONLINE';
-export const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
export const STATUS_NEVER_CONTACTED = 'NEVER_CONTACTED';
export const STATUS_OFFLINE = 'OFFLINE';
export const STATUS_STALE = 'STALE';
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
index 6da9e276f74..f7bcd683718 100644
--- a/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_group_runners.query.graphql
@@ -13,7 +13,7 @@ query getGroupRunners(
$sort: CiRunnerSort
) {
group(fullPath: $groupFullPath) {
- id
+ id # Apollo required
runners(
membership: DESCENDANTS
before: $before
diff --git a/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql
new file mode 100644
index 00000000000..554eb09e372
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_group_runners_count.query.graphql
@@ -0,0 +1,20 @@
+query getGroupRunnersCount(
+ $groupFullPath: ID!
+ $status: CiRunnerStatus
+ $type: CiRunnerType
+ $tagList: [String!]
+ $search: String
+) {
+ group(fullPath: $groupFullPath) {
+ id # Apollo required
+ runners(
+ membership: DESCENDANTS
+ status: $status
+ type: $type
+ tagList: $tagList
+ search: $search
+ ) {
+ count
+ }
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/get_runners.query.graphql b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
index 51a91b9eb96..05df399fa6a 100644
--- a/app/assets/javascripts/runner/graphql/get_runners.query.graphql
+++ b/app/assets/javascripts/runner/graphql/get_runners.query.graphql
@@ -26,6 +26,7 @@ query getRunners(
nodes {
...RunnerNode
adminUrl
+ editAdminUrl
}
pageInfo {
...PageInfo
diff --git a/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql
new file mode 100644
index 00000000000..181a4495cae
--- /dev/null
+++ b/app/assets/javascripts/runner/graphql/get_runners_count.query.graphql
@@ -0,0 +1,10 @@
+query getRunnersCount(
+ $status: CiRunnerStatus
+ $type: CiRunnerType
+ $tagList: [String!]
+ $search: String
+) {
+ runners(status: $status, type: $type, tagList: $tagList, search: $search) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
index 8c50cba7de3..8e968343b9b 100644
--- a/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_details_shared.fragment.graphql
@@ -9,4 +9,6 @@ fragment RunnerDetailsShared on CiRunner {
description
maximumTimeout
tagList
+ createdAt
+ status(legacyMode: null)
}
diff --git a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
index 169f6ffd2ea..4a771d779dc 100644
--- a/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
+++ b/app/assets/javascripts/runner/graphql/runner_node.fragment.graphql
@@ -12,4 +12,8 @@ fragment RunnerNode on CiRunner {
tagList
contactedAt
status(legacyMode: null)
+ userPermissions {
+ updateRunner
+ deleteRunner
+ }
}
diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
index a58a53a6a0d..3a7b58e3dc9 100644
--- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue
@@ -1,6 +1,6 @@
<script>
import { GlLink } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { createAlert } from '~/flash';
import { fetchPolicies } from '~/lib/graphql';
import { updateHistory } from '~/lib/utils/url_utility';
import { formatNumber, sprintf, s__ } from '~/locale';
@@ -9,7 +9,7 @@ import RegistrationDropdown from '../components/registration/registration_dropdo
import RunnerFilteredSearchBar from '../components/runner_filtered_search_bar.vue';
import RunnerList from '../components/runner_list.vue';
import RunnerName from '../components/runner_name.vue';
-import RunnerOnlineStat from '../components/stat/runner_online_stat.vue';
+import RunnerStats from '../components/stat/runner_stats.vue';
import RunnerPagination from '../components/runner_pagination.vue';
import RunnerTypeTabs from '../components/runner_type_tabs.vue';
@@ -19,8 +19,12 @@ import {
GROUP_FILTERED_SEARCH_NAMESPACE,
GROUP_TYPE,
GROUP_RUNNER_COUNT_LIMIT,
+ STATUS_ONLINE,
+ STATUS_OFFLINE,
+ STATUS_STALE,
} from '../constants';
import getGroupRunnersQuery from '../graphql/get_group_runners.query.graphql';
+import getGroupRunnersCountQuery from '../graphql/get_group_runners_count.query.graphql';
import {
fromUrlQueryToSearch,
fromSearchToUrl,
@@ -28,6 +32,17 @@ import {
} from '../runner_search_utils';
import { captureException } from '../sentry_utils';
+const runnersCountSmartQuery = {
+ query: getGroupRunnersCountQuery,
+ fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
+ update(data) {
+ return data?.group?.runners?.count;
+ },
+ error(error) {
+ this.reportToSentry(error);
+ },
+};
+
export default {
name: 'GroupRunnersApp',
components: {
@@ -36,7 +51,7 @@ export default {
RunnerFilteredSearchBar,
RunnerList,
RunnerName,
- RunnerOnlineStat,
+ RunnerStats,
RunnerPagination,
RunnerTypeTabs,
},
@@ -84,11 +99,38 @@ export default {
};
},
error(error) {
- createFlash({ message: I18N_FETCH_ERROR });
+ createAlert({ message: I18N_FETCH_ERROR });
this.reportToSentry(error);
},
},
+ onlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_ONLINE,
+ };
+ },
+ },
+ offlineRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_OFFLINE,
+ };
+ },
+ },
+ staleRunnersTotal: {
+ ...runnersCountSmartQuery,
+ variables() {
+ return {
+ groupFullPath: this.groupFullPath,
+ status: STATUS_STALE,
+ };
+ },
+ },
},
computed: {
variables() {
@@ -147,7 +189,11 @@ export default {
<template>
<div>
- <runner-online-stat class="gl-py-6 gl-px-5" :value="groupRunnersCount" />
+ <runner-stats
+ :online-runners-count="onlineRunnersTotal"
+ :offline-runners-count="offlineRunnersTotal"
+ :stale-runners-count="staleRunnersTotal"
+ />
<div class="gl-display-flex gl-align-items-center">
<runner-type-tabs
diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js
index b88023720e8..c80a73948b8 100644
--- a/app/assets/javascripts/runner/runner_search_utils.js
+++ b/app/assets/javascripts/runner/runner_search_utils.js
@@ -16,6 +16,7 @@ import {
PARAM_KEY_BEFORE,
DEFAULT_SORT,
RUNNER_PAGE_SIZE,
+ STATUS_NEVER_CONTACTED,
} from './constants';
/**
@@ -79,6 +80,33 @@ const getPaginationFromParams = (params) => {
};
};
+// Outdated URL parameters
+const STATUS_NOT_CONNECTED = 'NOT_CONNECTED';
+
+/**
+ * Returns an updated URL for old (or deprecated) admin runner URLs.
+ *
+ * Use for redirecting users to currently used URLs.
+ *
+ * @param {String?} URL
+ * @returns Updated URL if outdated, `null` otherwise
+ */
+export const updateOutdatedUrl = (url = window.location.href) => {
+ const urlObj = new URL(url);
+ const query = urlObj.search;
+
+ const params = queryToObject(query, { gatherArrays: true });
+
+ const runnerType = params[PARAM_KEY_STATUS]?.[0] || null;
+ if (runnerType === STATUS_NOT_CONNECTED) {
+ const updatedParams = {
+ [PARAM_KEY_STATUS]: [STATUS_NEVER_CONTACTED],
+ };
+ return setUrlParams(updatedParams, url, false, true, true);
+ }
+ return null;
+};
+
/**
* Takes a URL query and transforms it into a "search" object
* @param {String?} query
diff --git a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js b/app/assets/javascripts/runner/runner_update_form_utils.js
index 3b519fa7d71..3b519fa7d71 100644
--- a/app/assets/javascripts/runner/runner_details/runner_update_form_utils.js
+++ b/app/assets/javascripts/runner/runner_update_form_utils.js
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 75d2b324623..d228f77f27d 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -27,6 +27,9 @@ export const i18n = {
securityConfiguration: __('Security Configuration'),
vulnerabilityManagement: s__('SecurityConfiguration|Vulnerability Management'),
securityTraining: s__('SecurityConfiguration|Security training'),
+ securityTrainingDescription: s__(
+ 'SecurityConfiguration|Enable security training to help your developers learn how to fix vulnerabilities. Developers can view security training from selected educational providers, relevant to the detected vulnerability.',
+ ),
};
export default {
@@ -160,8 +163,12 @@ export default {
</template>
</user-callout-dismisser>
- <gl-tabs content-class="gl-pt-0">
- <gl-tab data-testid="security-testing-tab" :title="$options.i18n.securityTesting">
+ <gl-tabs content-class="gl-pt-0" sync-active-tab-with-query-params lazy>
+ <gl-tab
+ data-testid="security-testing-tab"
+ :title="$options.i18n.securityTesting"
+ query-param-value="security-testing"
+ >
<auto-dev-ops-enabled-alert
v-if="shouldShowAutoDevopsEnabledAlert"
class="gl-mt-3"
@@ -185,9 +192,12 @@ export default {
{{ $options.i18n.description }}
</p>
<p v-if="canViewCiHistory">
- <gl-link data-testid="security-view-history-link" :href="gitlabCiHistoryPath">{{
- $options.i18n.configurationHistory
- }}</gl-link>
+ <gl-link
+ data-testid="security-view-history-link"
+ data-qa-selector="security_configuration_history_link"
+ :href="gitlabCiHistoryPath"
+ >{{ $options.i18n.configurationHistory }}</gl-link
+ >
</p>
</template>
@@ -203,7 +213,11 @@ export default {
</template>
</section-layout>
</gl-tab>
- <gl-tab data-testid="compliance-testing-tab" :title="$options.i18n.compliance">
+ <gl-tab
+ data-testid="compliance-testing-tab"
+ :title="$options.i18n.compliance"
+ query-param-value="compliance-testing"
+ >
<section-layout :heading="$options.i18n.compliance">
<template #description>
<p>
@@ -241,8 +255,14 @@ export default {
v-if="glFeatures.secureVulnerabilityTraining"
data-testid="vulnerability-management-tab"
:title="$options.i18n.vulnerabilityManagement"
+ query-param-value="vulnerability-management"
>
<section-layout :heading="$options.i18n.securityTraining">
+ <template #description>
+ <p>
+ {{ $options.i18n.securityTrainingDescription }}
+ </p>
+ </template>
<template #features>
<training-provider-list />
</template>
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
index ce6a1b4888b..315f676e659 100644
--- a/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
+++ b/app/assets/javascripts/security_configuration/components/auto_dev_ops_alert.vue
@@ -28,6 +28,7 @@ export default {
variant="info"
:primary-button-link="autoDevopsPath"
:primary-button-text="$options.i18n.primaryButtonText"
+ data-qa-selector="autodevops_container"
@dismiss="dismissMethod"
>
<gl-sprintf :message="$options.i18n.body">
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
index dd8ba72ad1f..034dba29196 100644
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ b/app/assets/javascripts/security_configuration/components/constants.js
@@ -254,7 +254,7 @@ export const securityFeatures = [
helpPath: COVERAGE_FUZZING_HELP_PATH,
configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH,
type: REPORT_TYPE_COVERAGE_FUZZING,
- secondary: gon?.features?.corpusManagement
+ secondary: gon?.features?.corpusManagementUi
? {
type: REPORT_TYPE_CORPUS_MANAGEMENT,
name: CORPUS_MANAGEMENT_NAME,
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index 509377a63e8..ca4596e16b3 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -1,21 +1,39 @@
<script>
-import { GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlCard, GlToggle, GlLink, GlSkeletonLoader } from '@gitlab/ui';
+import { __ } from '~/locale';
import securityTrainingProvidersQuery from '../graphql/security_training_providers.query.graphql';
+import configureSecurityTrainingProvidersMutation from '../graphql/configure_security_training_providers.mutation.graphql';
+
+const i18n = {
+ providerQueryErrorMessage: __(
+ 'Could not fetch training providers. Please refresh the page, or try again later.',
+ ),
+ configMutationErrorMessage: __(
+ 'Could not save configuration. Please refresh the page, or try again later.',
+ ),
+};
export default {
components: {
+ GlAlert,
GlCard,
GlToggle,
GlLink,
GlSkeletonLoader,
},
+ inject: ['projectPath'],
apollo: {
securityTrainingProviders: {
query: securityTrainingProvidersQuery,
+ error() {
+ this.errorMessage = this.$options.i18n.providerQueryErrorMessage;
+ },
},
},
data() {
return {
+ errorMessage: '',
+ toggleLoading: false,
securityTrainingProviders: [],
};
},
@@ -24,38 +42,92 @@ export default {
return this.$apollo.queries.securityTrainingProviders.loading;
},
},
+ methods: {
+ toggleProvider(selectedProviderId) {
+ const toggledProviders = this.securityTrainingProviders.map((provider) => ({
+ ...provider,
+ ...(provider.id === selectedProviderId && { isEnabled: !provider.isEnabled }),
+ }));
+
+ const enabledProviderIds = toggledProviders
+ .filter(({ isEnabled }) => isEnabled)
+ .map(({ id }) => id);
+
+ this.storeEnabledProviders(toggledProviders, enabledProviderIds);
+ },
+ async storeEnabledProviders(toggledProviders, enabledProviderIds) {
+ this.toggleLoading = true;
+
+ try {
+ const {
+ data: {
+ configureSecurityTrainingProviders: { errors = [] },
+ },
+ } = await this.$apollo.mutate({
+ mutation: configureSecurityTrainingProvidersMutation,
+ variables: {
+ input: {
+ enabledProviders: enabledProviderIds,
+ fullPath: this.projectPath,
+ },
+ },
+ });
+
+ if (errors.length > 0) {
+ // throwing an error here means we can handle scenarios within the `catch` block below
+ throw new Error();
+ }
+ } catch {
+ this.errorMessage = this.$options.i18n.configMutationErrorMessage;
+ } finally {
+ this.toggleLoading = false;
+ }
+ },
+ },
+ i18n,
};
</script>
<template>
- <div
- v-if="isLoading"
- class="gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
- >
- <gl-skeleton-loader :width="350" :height="44">
- <rect width="200" height="8" x="10" y="0" rx="4" />
- <rect width="300" height="8" x="10" y="15" rx="4" />
- <rect width="100" height="8" x="10" y="35" rx="4" />
- </gl-skeleton-loader>
- </div>
- <ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
- <li
- v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
- :key="id"
- class="gl-mb-6"
+ <div>
+ <gl-alert v-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-6">
+ {{ errorMessage }}
+ </gl-alert>
+ <div
+ v-if="isLoading"
+ class="gl-bg-white gl-py-6 gl-rounded-base gl-border-1 gl-border-solid gl-border-gray-100"
>
- <gl-card>
- <div class="gl-display-flex">
- <gl-toggle :value="isEnabled" :label="__('Training mode')" label-position="hidden" />
- <div class="gl-ml-5">
- <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
- <p>
- {{ description }}
- <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
- </p>
+ <gl-skeleton-loader :width="350" :height="44">
+ <rect width="200" height="8" x="10" y="0" rx="4" />
+ <rect width="300" height="8" x="10" y="15" rx="4" />
+ <rect width="100" height="8" x="10" y="35" rx="4" />
+ </gl-skeleton-loader>
+ </div>
+ <ul v-else class="gl-list-style-none gl-m-0 gl-p-0">
+ <li
+ v-for="{ id, isEnabled, name, description, url } in securityTrainingProviders"
+ :key="id"
+ class="gl-mb-6"
+ >
+ <gl-card>
+ <div class="gl-display-flex">
+ <gl-toggle
+ :value="isEnabled"
+ :label="__('Training mode')"
+ label-position="hidden"
+ :is-loading="toggleLoading"
+ @change="toggleProvider(id)"
+ />
+ <div class="gl-ml-5">
+ <h3 class="gl-font-lg gl-m-0 gl-mb-2">{{ name }}</h3>
+ <p>
+ {{ description }}
+ <gl-link :href="url" target="_blank">{{ __('Learn more.') }}</gl-link>
+ </p>
+ </div>
</div>
- </div>
- </gl-card>
- </li>
- </ul>
+ </gl-card>
+ </li>
+ </ul>
+ </div>
</template>
diff --git a/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql
new file mode 100644
index 00000000000..660e0fadafb
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/graphql/configure_security_training_providers.mutation.graphql
@@ -0,0 +1,9 @@
+mutation configureSecurityTrainingProviders($input: configureSecurityTrainingProvidersInput!) {
+ configureSecurityTrainingProviders(input: $input) @client {
+ errors
+ securityTrainingProviders {
+ id
+ isEnabled
+ }
+ }
+}
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index c86ff1a58f2..24c0585e077 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -2,38 +2,10 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
-import { __ } from '~/locale';
import SecurityConfigurationApp from './components/app.vue';
import { securityFeatures, complianceFeatures } from './components/constants';
import { augmentFeatures } from './utils';
-
-// Note: this is behind a feature flag and only a placeholder
-// until the actual GraphQL fields have been added
-// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
-export const tempResolvers = {
- Query: {
- securityTrainingProviders() {
- return [
- {
- __typename: 'SecurityTrainingProvider',
- id: 101,
- name: __('Kontra'),
- description: __('Interactive developer security education.'),
- url: 'https://application.security/',
- isEnabled: false,
- },
- {
- __typename: 'SecurityTrainingProvider',
- id: 102,
- name: __('SecureCodeWarrior'),
- description: __('Security training with guide and learning pathways.'),
- url: 'https://www.securecodewarrior.com/',
- isEnabled: true,
- },
- ];
- },
- },
-};
+import tempResolvers from './resolver';
export const initSecurityConfiguration = (el) => {
if (!el) {
diff --git a/app/assets/javascripts/security_configuration/resolver.js b/app/assets/javascripts/security_configuration/resolver.js
new file mode 100644
index 00000000000..93175d4a3d1
--- /dev/null
+++ b/app/assets/javascripts/security_configuration/resolver.js
@@ -0,0 +1,56 @@
+import produce from 'immer';
+import { __ } from '~/locale';
+import securityTrainingProvidersQuery from './graphql/security_training_providers.query.graphql';
+
+// Note: this is behind a feature flag and only a placeholder
+// until the actual GraphQL fields have been added
+// https://gitlab.com/gitlab-org/gi tlab/-/issues/346480
+export default {
+ Query: {
+ securityTrainingProviders() {
+ return [
+ {
+ __typename: 'SecurityTrainingProvider',
+ id: 101,
+ name: __('Kontra'),
+ description: __('Interactive developer security education.'),
+ url: 'https://application.security/',
+ isEnabled: false,
+ },
+ {
+ __typename: 'SecurityTrainingProvider',
+ id: 102,
+ name: __('SecureCodeWarrior'),
+ description: __('Security training with guide and learning pathways.'),
+ url: 'https://www.securecodewarrior.com/',
+ isEnabled: true,
+ },
+ ];
+ },
+ },
+
+ Mutation: {
+ configureSecurityTrainingProviders: (
+ _,
+ { input: { enabledProviders, primaryProvider } },
+ { cache },
+ ) => {
+ const sourceData = cache.readQuery({
+ query: securityTrainingProvidersQuery,
+ });
+
+ const data = produce(sourceData.securityTrainingProviders, (draftData) => {
+ /* eslint-disable no-param-reassign */
+ draftData.forEach((provider) => {
+ provider.isPrimary = provider.id === primaryProvider;
+ provider.isEnabled =
+ provider.id === primaryProvider || enabledProviders.includes(provider.id);
+ });
+ });
+ return {
+ __typename: 'configureSecurityTrainingProvidersPayload',
+ securityTrainingProviders: data,
+ };
+ },
+ },
+};
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index e41f3aa5c9d..a746642c191 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -267,6 +267,8 @@ export default {
v-if="glFeatures.improvedEmojiPicker"
dropdown-class="gl-h-full"
toggle-class="btn emoji-menu-toggle-button gl-px-4! gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ boundary="viewport"
+ :right="false"
@click="setEmoji"
>
<template #button-content>
diff --git a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
index 6d4da104952..950647f1cb2 100644
--- a/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
+++ b/app/assets/javascripts/sidebar/components/crm_contacts/crm_contacts.vue
@@ -1,5 +1,5 @@
<script>
-import { GlIcon, GlPopover, GlTooltipDirective } from '@gitlab/ui';
+import { GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
import createFlash from '~/flash';
import { convertToGraphQLId } from '~/graphql_shared/utils';
@@ -10,6 +10,7 @@ import issueCrmContactsSubscription from './queries/issue_crm_contacts.subscript
export default {
components: {
GlIcon,
+ GlLink,
GlPopover,
},
directives: {
@@ -85,9 +86,6 @@ export default {
);
},
},
- i18n: {
- help: __('Work in progress- click here to find out more'),
- },
};
</script>
@@ -97,11 +95,10 @@ export default {
<gl-icon name="users" />
<span> {{ contactCount }} </span>
</div>
- <div
- v-gl-tooltip.left.viewport="$options.i18n.help"
- class="hide-collapsed help-button float-right"
- >
- <a href="https://gitlab.com/gitlab-org/gitlab/-/issues/2256"><gl-icon name="question-o" /></a>
+ <div class="hide-collapsed help-button gl-float-right">
+ <gl-link href="https://docs.gitlab.com/ee/user/crm/" target="_blank"
+ ><gl-icon name="question-o"
+ /></gl-link>
</div>
<div class="title hide-collapsed gl-mb-2 gl-line-height-20">
{{ contactsLabel }}
diff --git a/app/assets/javascripts/sidebar/mount_sidebar.js b/app/assets/javascripts/sidebar/mount_sidebar.js
index cbe40d0bfbe..6363422259e 100644
--- a/app/assets/javascripts/sidebar/mount_sidebar.js
+++ b/app/assets/javascripts/sidebar/mount_sidebar.js
@@ -26,6 +26,7 @@ import trackShowInviteMemberLink from '~/sidebar/track_invite_members';
import { DropdownVariant } from '~/vue_shared/components/sidebar/labels_select_vue/constants';
import LabelsSelectWidget from '~/vue_shared/components/sidebar/labels_select_widget/labels_select_root.vue';
import { LabelType } from '~/vue_shared/components/sidebar/labels_select_widget/constants';
+import eventHub from '~/sidebar/event_hub';
import Translate from '../vue_shared/translate';
import SidebarAssignees from './components/assignees/sidebar_assignees.vue';
import CopyEmailToClipboard from './components/copy_email_to_clipboard.vue';
@@ -600,6 +601,12 @@ export function mountSidebar(mediator, store) {
mountTimeTrackingComponent();
mountSeverityComponent();
+
+ if (window.gon?.features?.mrAttentionRequests) {
+ eventHub.$on('removeCurrentUserAttentionRequested', () =>
+ mediator.removeCurrentUserAttentionRequested(),
+ );
+ }
}
export { getSidebarOptions };
diff --git a/app/assets/javascripts/sidebar/sidebar_mediator.js b/app/assets/javascripts/sidebar/sidebar_mediator.js
index a49ddac8c89..25468d4a697 100644
--- a/app/assets/javascripts/sidebar/sidebar_mediator.js
+++ b/app/assets/javascripts/sidebar/sidebar_mediator.js
@@ -30,7 +30,7 @@ export default class SidebarMediator {
this.store.addAssignee(this.store.currentUser);
}
- saveAssignees(field) {
+ async saveAssignees(field) {
const selected = this.store.assignees.map((u) => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
@@ -38,10 +38,22 @@ export default class SidebarMediator {
const assignees = selected.length === 0 ? [0] : selected;
const data = { assignee_ids: assignees };
- return this.service.update(field, data);
+ try {
+ const res = await this.service.update(field, data);
+
+ this.store.overwrite('assignees', res.data.assignees);
+
+ if (res.data.reviewers) {
+ this.store.overwrite('reviewers', res.data.reviewers);
+ }
+
+ return Promise.resolve(res);
+ } catch (e) {
+ return Promise.reject(e);
+ }
}
- saveReviewers(field) {
+ async saveReviewers(field) {
const selected = this.store.reviewers.map((u) => u.id);
// If there are no ids, that means we have to unassign (which is id = 0)
@@ -49,7 +61,16 @@ export default class SidebarMediator {
const reviewers = selected.length === 0 ? [0] : selected;
const data = { reviewer_ids: reviewers };
- return this.service.update(field, data);
+ try {
+ const res = await this.service.update(field, data);
+
+ this.store.overwrite('reviewers', res.data.reviewers);
+ this.store.overwrite('assignees', res.data.assignees);
+
+ return Promise.resolve(res);
+ } catch (e) {
+ return Promise.reject();
+ }
}
requestReview({ userId, callback }) {
@@ -63,6 +84,19 @@ export default class SidebarMediator {
.catch(() => callback(userId, false));
}
+ removeCurrentUserAttentionRequested() {
+ const currentUserId = gon.current_user_id;
+
+ const currentUserReviewer = this.store.findReviewer({ id: currentUserId });
+ const currentUserAssignee = this.store.findAssignee({ id: currentUserId });
+
+ if (currentUserReviewer?.attention_requested || currentUserAssignee?.attention_requested) {
+ // Update current users attention_requested state
+ this.store.updateReviewer(currentUserId, 'attention_requested');
+ this.store.updateAssignee(currentUserId, 'attention_requested');
+ }
+ }
+
async toggleAttentionRequested(type, { user, callback }) {
try {
const isReviewer = type === 'reviewer';
@@ -82,15 +116,7 @@ export default class SidebarMediator {
const currentUserId = gon.current_user_id;
if (currentUserId !== user.id) {
- const currentUserReviewerOrAssignee = isReviewer
- ? this.store.findReviewer({ id: currentUserId })
- : this.store.findAssignee({ id: currentUserId });
-
- if (currentUserReviewerOrAssignee?.attention_requested) {
- // Update current users attention_requested state
- this.store.updateReviewer(currentUserId, 'attention_requested');
- this.store.updateAssignee(currentUserId, 'attention_requested');
- }
+ this.removeCurrentUserAttentionRequested();
}
toast(sprintf(__('Requested attention from @%{username}'), { username: user.username }));
diff --git a/app/assets/javascripts/sidebar/stores/sidebar_store.js b/app/assets/javascripts/sidebar/stores/sidebar_store.js
index 5376791469e..2caa6f4f0a0 100644
--- a/app/assets/javascripts/sidebar/stores/sidebar_store.js
+++ b/app/assets/javascripts/sidebar/stores/sidebar_store.js
@@ -98,6 +98,10 @@ export default class SidebarStore {
}
}
+ overwrite(key, newData) {
+ this[key] = newData;
+ }
+
findAssignee(findAssignee) {
return this.assignees.find(({ id }) => id === findAssignee.id);
}
diff --git a/app/assets/javascripts/tracking/index.js b/app/assets/javascripts/tracking/index.js
index 7e99ecb4f4e..d60eb37a9a2 100644
--- a/app/assets/javascripts/tracking/index.js
+++ b/app/assets/javascripts/tracking/index.js
@@ -46,7 +46,10 @@ export function initDefaultTrackers() {
// must be after enableActivityTracking
const standardContext = getStandardContext();
const experimentContexts = getAllExperimentContexts();
- window.snowplow('trackPageView', null, [standardContext, ...experimentContexts]);
+ // To not expose personal identifying information, the page title is hardcoded as `GitLab`
+ // See: https://gitlab.com/gitlab-org/gitlab/-/issues/345243
+ window.snowplow('trackPageView', 'GitLab', [standardContext, ...experimentContexts]);
+ window.snowplow('setDocumentTitle', 'GitLab');
if (window.snowplowOptions.formTracking) {
Tracking.enableFormTracking(opts.formTrackingConfig);
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
deleted file mode 100644
index 58bff370fa5..00000000000
--- a/app/assets/javascripts/tree.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* eslint-disable func-names, consistent-return, one-var, class-methods-use-this */
-
-import $ from 'jquery';
-import { visitUrl } from './lib/utils/url_utility';
-
-export default class TreeView {
- constructor() {
- this.initKeyNav();
- // Code browser tree slider
- // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
- $('.tree-content-holder .tree-item').on('click', function (e) {
- const $clickedEl = $(e.target);
- const path = $('.tree-item-file-name a', this).attr('href');
- if (!$clickedEl.is('a') && !$clickedEl.is('.str-truncated')) {
- if (e.metaKey || e.which === 2) {
- e.preventDefault();
- return window.open(path, '_blank');
- }
- return visitUrl(path);
- }
- });
- // Show the "Loading commit data" for only the first element
- $('span.log_loading').first().removeClass('hide');
- }
-
- initKeyNav() {
- const li = $('tr.tree-item');
- let liSelected = null;
- return $('body').keydown((e) => {
- let next, path;
- if ($('input:focus').length > 0 && (e.which === 38 || e.which === 40)) {
- return false;
- }
- if (e.which === 40) {
- if (liSelected) {
- next = liSelected.next();
- if (next.length > 0) {
- liSelected.removeClass('selected');
- liSelected = next.addClass('selected');
- }
- } else {
- liSelected = li.eq(0).addClass('selected');
- }
- return $(liSelected).focus();
- } else if (e.which === 38) {
- if (liSelected) {
- next = liSelected.prev();
- if (next.length > 0) {
- liSelected.removeClass('selected');
- liSelected = next.addClass('selected');
- }
- } else {
- liSelected = li.last().addClass('selected');
- }
- return $(liSelected).focus();
- } else if (e.which === 13) {
- path = $('.tree-item.selected .tree-item-file-name a').attr('href');
- if (path) {
- return visitUrl(path);
- }
- }
- });
- }
-}
diff --git a/app/assets/javascripts/version_check_image.js b/app/assets/javascripts/version_check_image.js
deleted file mode 100644
index 4e00e0f11f7..00000000000
--- a/app/assets/javascripts/version_check_image.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export default class VersionCheckImage {
- static bindErrorEvent(imageElement) {
- // eslint-disable-next-line @gitlab/no-global-event-off
- imageElement.off('error').on('error', () => imageElement.hide());
- }
-}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
index 386ba2e2d77..24cefd63ce3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue
@@ -3,6 +3,7 @@ import { GlButton } from '@gitlab/ui';
import createFlash from '~/flash';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
+import sidebarEventHub from '~/sidebar/event_hub';
import eventHub from '../../event_hub';
import approvalsMixin from '../../mixins/approvals';
import MrWidgetContainer from '../mr_widget_container.vue';
@@ -172,6 +173,7 @@ export default {
this.mr.setApprovals(data);
eventHub.$emit('MRWidgetUpdateRequested');
eventHub.$emit('ApprovalUpdated');
+ sidebarEventHub.$emit('removeCurrentUserAttentionRequested');
this.$emit('updated');
})
.catch(errFn)
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
index 33a83aef057..d878a1fa2e0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/actions.vue
@@ -70,6 +70,7 @@ export default {
variant="confirm"
size="small"
class="gl-display-none gl-md-display-block gl-float-left"
+ data-testid="extension-actions-button"
@click="onClickAction(btn)"
>
{{ btn.text }}
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 549cf64fb08..7322958e6df 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
@@ -13,6 +13,7 @@ import * as Sentry from '@sentry/browser';
import api from '~/api';
import { sprintf, s__, __ } from '~/locale';
import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
+import Poll from '~/lib/utils/poll';
import { EXTENSION_ICON_CLASS, EXTENSION_ICONS } from '../../constants';
import StatusIcon from './status_icon.vue';
import Actions from './actions.vue';
@@ -132,19 +133,50 @@ export default {
this.triggerRedisTracking();
},
+ initExtensionPolling() {
+ const poll = new Poll({
+ resource: {
+ fetchData: () => this.fetchCollapsedData(this.$props),
+ },
+ method: 'fetchData',
+ successCallback: (data) => {
+ if (Object.keys(data).length > 0) {
+ poll.stop();
+ this.setCollapsedData(data);
+ }
+ },
+ errorCallback: (e) => {
+ poll.stop();
+
+ this.setCollapsedError(e);
+ },
+ });
+
+ poll.makeRequest();
+ },
loadCollapsedData() {
this.loadingState = LOADING_STATES.collapsedLoading;
- this.fetchCollapsedData(this.$props)
- .then((data) => {
- this.collapsedData = data;
- this.loadingState = null;
- })
- .catch((e) => {
- this.loadingState = LOADING_STATES.collapsedError;
+ if (this.$options.enablePolling) {
+ this.initExtensionPolling();
+ } else {
+ this.fetchCollapsedData(this.$props)
+ .then((data) => {
+ this.setCollapsedData(data);
+ })
+ .catch((e) => {
+ this.setCollapsedError(e);
+ });
+ }
+ },
+ setCollapsedData(data) {
+ this.collapsedData = data;
+ this.loadingState = null;
+ },
+ setCollapsedError(e) {
+ this.loadingState = LOADING_STATES.collapsedError;
- Sentry.captureException(e);
- });
+ Sentry.captureException(e);
},
loadAllData() {
if (this.hasFullData) return;
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
index ec6e6ed2620..8438f3492b2 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/index.js
@@ -13,6 +13,7 @@ export const registerExtension = (extension) => {
props: extension.props,
i18n: extension.i18n,
expandEvent: extension.expandEvent,
+ enablePolling: extension.enablePolling,
computed: {
...Object.keys(extension.computed).reduce(
(acc, computedKey) => ({
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 235a200b747..8cdaa3316ee 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
@@ -119,6 +119,8 @@ export default {
:show-gitpod-button="mr.showGitpodButton"
:gitpod-url="mr.gitpodUrl"
:gitpod-enabled="mr.gitpodEnabled"
+ :user-preferences-gitpod-path="mr.userPreferencesGitpodPath"
+ :user-profile-enable-gitpod-path="mr.userProfileEnableGitpodPath"
:gitpod-text="$options.i18n.gitpodText"
class="gl-display-none gl-md-display-inline-block gl-mr-3"
data-placement="bottom"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
index 677c50ed930..2e3a02b1712 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ciIcon from '../../vue_shared/components/ci_icon.vue';
export default {
@@ -8,6 +9,7 @@ export default {
GlButton,
GlLoadingIcon,
},
+ mixins: [glFeatureFlagMixin()],
props: {
status: {
type: String,
@@ -42,7 +44,7 @@ export default {
</div>
<gl-button
- v-if="showDisabledButton"
+ v-if="!glFeatures.restructuredMrWidget && showDisabledButton"
category="primary"
variant="success"
data-testid="disabled-merge-button"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
index ce572f8b0bf..701ef89304c 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/merge_checks_failed.vue
@@ -1,20 +1,16 @@
<script>
-import { GlButton } from '@gitlab/ui';
import { s__ } from '~/locale';
-import notesEventHub from '~/notes/event_hub';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
i18n: {
- pipelineFailed: s__(
- 'mrWidget|The pipeline for this merge request did not complete. Push a new commit to fix the failure.',
- ),
approvalNeeded: s__('mrWidget|Merge blocked: this merge request must be approved.'),
- unresolvedDiscussions: s__('mrWidget|Merge blocked: all threads must be resolved.'),
+ blockingMergeRequests: s__(
+ 'mrWidget|Merge blocked: you can only merge after the above items are resolved.',
+ ),
},
components: {
StatusIcon,
- GlButton,
},
props: {
mr: {
@@ -24,22 +20,15 @@ export default {
},
computed: {
failedText() {
- if (this.mr.isPipelineFailed) {
- return this.$options.i18n.pipelineFailed;
- } else if (this.mr.approvals && !this.mr.isApproved) {
+ if (this.mr.approvals && !this.mr.isApproved) {
return this.$options.i18n.approvalNeeded;
- } else if (this.mr.hasMergeableDiscussionsState) {
- return this.$options.i18n.unresolvedDiscussions;
+ } else if (this.mr.blockingMergeRequests?.total_count > 0) {
+ return this.$options.i18n.blockingMergeRequests;
}
return null;
},
},
- methods: {
- jumpToFirstUnresolvedDiscussion() {
- notesEventHub.$emit('jumpToFirstUnresolvedDiscussion');
- },
- },
};
</script>
@@ -48,28 +37,6 @@ export default {
<status-icon status="warning" />
<p class="media-body gl-m-0! gl-font-weight-bold gl-text-black-normal!">
{{ failedText }}
- <template v-if="failedText == $options.i18n.unresolvedDiscussions">
- <gl-button
- class="gl-ml-3"
- size="small"
- variant="confirm"
- data-testid="jumpToUnresolved"
- @click="jumpToFirstUnresolvedDiscussion"
- >
- {{ s__('mrWidget|Jump to first unresolved thread') }}
- </gl-button>
- <gl-button
- v-if="mr.createIssueToResolveDiscussionsPath"
- :href="mr.createIssueToResolveDiscussionsPath"
- class="gl-ml-3"
- size="small"
- variant="confirm"
- category="secondary"
- data-testid="resolveIssue"
- >
- {{ s__('mrWidget|Create issue to resolve all threads') }}
- </gl-button>
- </template>
</p>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
index 13b1e49f44e..071920856a8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_archived.vue
@@ -1,25 +1,22 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import statusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetArchived',
components: {
- GlButton,
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
<div class="space-children">
- <status-icon status="warning" />
- <gl-button category="secondary" variant="success" :disabled="true">
- {{ s__('mrWidget|Merge') }}
- </gl-button>
+ <status-icon status="warning" show-disabled-button />
</div>
<div class="media-body">
- <span class="bold">
+ <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold">
{{ s__('mrWidget|Merge unavailable: merge requests are read-only on archived projects.') }}
</span>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
index 10b93d7849f..fd42fa0421f 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_checking.vue
@@ -12,9 +12,11 @@ export default {
</script>
<template>
<div class="mr-widget-body media">
- <status-icon :show-disabled-button="!glFeatures.restructuredMrWidget" status="loading" />
+ <status-icon :show-disabled-button="true" status="loading" />
<div class="media-body space-children">
- <span class="bold"> {{ s__('mrWidget|Checking if merge request can be merged…') }} </span>
+ <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold">
+ {{ s__('mrWidget|Checking if merge request can be merged…') }}
+ </span>
</div>
</div>
</template>
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 7a002d41ac0..a2c9cfe53cc 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
@@ -109,14 +109,18 @@ export default {
</gl-skeleton-loader>
</div>
<div v-else class="media-body space-children gl-display-flex gl-align-items-center">
- <span v-if="shouldBeRebased" class="bold">
+ <span
+ v-if="shouldBeRebased"
+ :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
+ class="bold"
+ >
{{
s__(`mrWidget|Merge blocked: fast-forward merge is not possible.
To merge this request, first rebase locally.`)
}}
</span>
<template v-else>
- <span class="bold">
+ <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold">
{{ s__('mrWidget|Merge blocked: merge conflicts must be resolved.') }}
<span v-if="!canMerge">
{{
@@ -129,6 +133,7 @@ export default {
<gl-button
v-if="showResolveButton"
:href="mr.conflictResolutionPath"
+ :size="glFeatures.restructuredMrWidget ? 'small' : 'medium'"
data-testid="resolve-conflicts-button"
>
{{ s__('mrWidget|Resolve conflicts') }}
@@ -136,6 +141,7 @@ export default {
<gl-button
v-if="canMerge"
v-gl-modal-directive="'modal-merge-info'"
+ :size="glFeatures.restructuredMrWidget ? 'small' : 'medium'"
data-testid="merge-locally-button"
>
{{ s__('mrWidget|Merge locally') }}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
index f91350d4a82..5b03eda2eac 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue
@@ -74,10 +74,21 @@ export default {
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
- <span class="bold js-branch-text">
+ <span
+ :class="{
+ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget,
+ }"
+ class="bold js-branch-text"
+ >
<span class="capitalize" data-testid="missingBranchName"> {{ missingBranchName }} </span>
{{ s__('mrWidget|branch does not exist.') }} {{ missingBranchNameMessage }}
- <gl-icon v-gl-tooltip :title="message" :aria-label="message" name="question-o" />
+ <gl-icon
+ v-gl-tooltip
+ :title="message"
+ :aria-label="message"
+ name="question-o"
+ class="gl-text-blue-600 gl-cursor-pointer"
+ />
</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
index 68ffca9cd68..34c5a2ff2c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue
@@ -1,4 +1,5 @@
<script>
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import StatusIcon from '../mr_widget_status_icon.vue';
export default {
@@ -6,13 +7,14 @@ export default {
components: {
StatusIcon,
},
+ mixins: [glFeatureFlagMixin()],
};
</script>
<template>
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
- <span class="bold">
+ <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold">
{{
s__(
`mrWidget|Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.`,
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 01e8303f513..bb0fb410d3e 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
@@ -3,11 +3,13 @@ import { GlButton, GlSkeletonLoader } from '@gitlab/ui';
import createFlash from '~/flash';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import ActionsButton from '~/vue_shared/components/actions_button.vue';
import simplePoll from '../../../lib/utils/simple_poll';
import eventHub from '../../event_hub';
import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
import rebaseQuery from '../../queries/states/rebase.query.graphql';
import statusIcon from '../mr_widget_status_icon.vue';
+import { REBASE_BUTTON_KEY, REBASE_WITHOUT_CI_BUTTON_KEY } from '../../constants';
export default {
name: 'MRWidgetRebase',
@@ -25,8 +27,9 @@ export default {
},
components: {
statusIcon,
- GlButton,
GlSkeletonLoader,
+ ActionsButton,
+ GlButton,
},
mixins: [glFeatureFlagMixin(), mergeRequestQueryVariablesMixin],
props: {
@@ -44,12 +47,16 @@ export default {
state: {},
isMakingRequest: false,
rebasingError: null,
+ selectedRebaseAction: REBASE_BUTTON_KEY,
};
},
computed: {
isLoading() {
return this.glFeatures.mergeRequestWidgetGraphql && this.$apollo.queries.state.loading;
},
+ showRebaseWithoutCi() {
+ return this.glFeatures?.rebaseWithoutCiUi;
+ },
rebaseInProgress() {
if (this.glFeatures.mergeRequestWidgetGraphql) {
return this.state.rebaseInProgress;
@@ -86,14 +93,36 @@ export default {
fastForwardMergeText() {
return __('Merge blocked: the source branch must be rebased onto the target branch.');
},
+ actions() {
+ return [this.rebaseAction, this.rebaseWithoutCiAction].filter((action) => action);
+ },
+ rebaseAction() {
+ return {
+ key: REBASE_BUTTON_KEY,
+ text: __('Rebase'),
+ secondaryText: __('Rebases and triggers a pipeline'),
+ attrs: {
+ 'data-qa-selector': 'mr_rebase_button',
+ },
+ handle: () => this.rebase(),
+ };
+ },
+ rebaseWithoutCiAction() {
+ return {
+ key: REBASE_WITHOUT_CI_BUTTON_KEY,
+ text: __('Rebase without CI'),
+ secondaryText: __('Performs a rebase but skips triggering a new pipeline'),
+ handle: () => this.rebase({ skipCi: true }),
+ };
+ },
},
methods: {
- rebase() {
+ rebase({ skipCi = false } = {}) {
this.isMakingRequest = true;
this.rebasingError = null;
this.service
- .rebase()
+ .rebase({ skipCi })
.then(() => {
simplePoll(this.checkRebaseStatus);
})
@@ -109,6 +138,9 @@ export default {
}
});
},
+ selectRebaseAction(key) {
+ this.selectedRebaseAction = key;
+ },
checkRebaseStatus(continuePolling, stopPolling) {
this.service
.poll()
@@ -152,12 +184,14 @@ export default {
<div class="rebase-state-find-class-convention media media-body space-children">
<span
v-if="rebaseInProgress || isMakingRequest"
+ :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
class="gl-font-weight-bold"
data-testid="rebase-message"
>{{ __('Rebase in progress') }}</span
>
<span
v-if="!rebaseInProgress && !canPushToSourceBranch"
+ :class="{ 'gl-text-body!': glFeatures.restructuredMrWidget }"
class="gl-font-weight-bold gl-ml-0!"
data-testid="rebase-message"
>{{ fastForwardMergeText }}</span
@@ -167,15 +201,26 @@ export default {
class="accept-merge-holder clearfix js-toggle-container accept-action media space-children"
>
<gl-button
+ v-if="!glFeatures.restructuredMrWidget && !showRebaseWithoutCi"
:loading="isMakingRequest"
variant="confirm"
data-qa-selector="mr_rebase_button"
+ data-testid="standard-rebase-button"
@click="rebase"
>
{{ __('Rebase') }}
</gl-button>
+ <actions-button
+ v-if="!glFeatures.restructuredMrWidget && showRebaseWithoutCi"
+ :actions="actions"
+ :selected-key="selectedRebaseAction"
+ variant="confirm"
+ category="primary"
+ @select="selectRebaseAction"
+ />
<span
v-if="!rebasingError"
+ :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
class="gl-font-weight-bold"
data-testid="rebase-message"
data-qa-selector="no_fast_forward_message_content"
@@ -186,6 +231,17 @@ export default {
<span v-else class="gl-font-weight-bold danger" data-testid="rebase-message">{{
rebasingError
}}</span>
+ <gl-button
+ v-if="glFeatures.restructuredMrWidget"
+ :loading="isMakingRequest"
+ variant="confirm"
+ size="small"
+ data-qa-selector="mr_rebase_button"
+ class="gl-ml-3!"
+ @click="rebase"
+ >
+ {{ __('Rebase') }}
+ </gl-button>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
index 2d704d3b07a..e43319d42ca 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -62,8 +62,8 @@ export default {
<gl-button
v-if="mr.newBlobPath"
:href="mr.newBlobPath"
- category="secondary"
- variant="success"
+ category="primary"
+ variant="confirm"
data-testid="createFileButton"
@click="onClickNewFile"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
index b5d2f91c637..d88dad2e086 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/pipeline_failed.vue
@@ -1,6 +1,7 @@
<script>
import { GlLink, GlSprintf } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { s__ } from '~/locale';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -11,6 +12,7 @@ export default {
GlSprintf,
statusIcon,
},
+ mixins: [glFeatureFlagMixin()],
computed: {
troubleshootingDocsPath() {
return helpPagePath('ci/troubleshooting', { anchor: 'merge-request-status-messages' });
@@ -28,7 +30,7 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="true" status="warning" />
<div class="media-body space-children">
- <span class="bold">
+ <span :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }" class="bold">
<gl-sprintf :message="$options.i18n.failedMessage">
<template #link="{ content }">
<gl-link :href="troubleshootingDocsPath" target="_blank">
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 8830128b7d6..06ce312bd4c 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
@@ -529,7 +529,7 @@ export default {
<template>
<div
:class="{
- 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7':
+ 'gl-border-t-1 gl-border-t-solid gl-border-gray-100 gl-bg-gray-10 gl-pl-7 gl-rounded-bottom-left-base gl-rounded-bottom-right-base':
glFeatures.restructuredMrWidget,
}"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
index 7eeba8d8f89..b1fbe150fcf 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { I18N_SHA_MISMATCH } from '../../i18n';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -12,6 +13,7 @@ export default {
i18n: {
I18N_SHA_MISMATCH,
},
+ mixins: [glFeatureFlagMixin()],
props: {
mr: {
type: Object,
@@ -25,7 +27,11 @@ export default {
<div class="mr-widget-body media">
<status-icon :show-disabled-button="false" status="warning" />
<div class="media-body">
- <span class="gl-font-weight-bold" data-qa-selector="head_mismatch_content">
+ <span
+ :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
+ class="gl-font-weight-bold"
+ data-qa-selector="head_mismatch_content"
+ >
{{ $options.i18n.I18N_SHA_MISMATCH.warningMessage }}
</span>
<gl-button
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
index 69e4df0ca11..8cf6383c26a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/unresolved_discussions.vue
@@ -1,5 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '~/notes/event_hub';
import statusIcon from '../mr_widget_status_icon.vue';
@@ -9,6 +10,7 @@ export default {
statusIcon,
GlButton,
},
+ mixins: [glFeatureFlagMixin()],
props: {
mr: {
type: Object,
@@ -25,16 +27,24 @@ export default {
<template>
<div class="mr-widget-body media gl-flex-wrap">
- <status-icon :show-disabled-button="true" status="warning" />
+ <status-icon show-disabled-button status="warning" />
<div class="media-body">
- <span class="gl-ml-3 gl-font-weight-bold gl-display-block gl-w-100">{{
- s__('mrWidget|Merge blocked: all threads must be resolved.')
- }}</span>
+ <span
+ :class="{
+ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget,
+ 'gl-display-block': !glFeatures.restructuredMrWidget,
+ }"
+ class="gl-ml-3 gl-font-weight-bold gl-w-100"
+ >
+ {{ s__('mrWidget|Merge blocked: all threads must be resolved.') }}
+ </span>
<gl-button
data-testid="jump-to-first"
class="gl-ml-3"
size="small"
- icon="comment-next"
+ :icon="glFeatures.restructuredMrWidget ? undefined : 'comment-next'"
+ :variant="glFeatures.restructuredMrWidget && 'confirm'"
+ :category="glFeatures.restructuredMrWidget && 'secondary'"
@click="jumpToFirstUnresolvedDiscussion"
>
{{ s__('mrWidget|Jump to first unresolved thread') }}
@@ -44,7 +54,7 @@ export default {
:href="mr.createIssueToResolveDiscussionsPath"
class="js-create-issue gl-ml-3"
size="small"
- icon="issue-new"
+ :icon="glFeatures.restructuredMrWidget ? undefined : 'issue-new'"
>
{{ s__('mrWidget|Create issue to resolve all threads') }}
</gl-button>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
index ba831a33b73..e0e19094c40 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue
@@ -166,7 +166,10 @@ export default {
<status-icon :show-disabled-button="canUpdate" status="warning" />
<div class="media-body">
<div class="float-left">
- <span class="gl-font-weight-bold">
+ <span
+ :class="{ 'gl-ml-0! gl-text-body!': glFeatures.restructuredMrWidget }"
+ class="gl-font-weight-bold"
+ >
{{
__("Merge blocked: merge request must be marked as ready. It's still marked as draft.")
}}
diff --git a/app/assets/javascripts/vue_merge_request_widget/constants.js b/app/assets/javascripts/vue_merge_request_widget/constants.js
index 2edccce7f4e..32effb91043 100644
--- a/app/assets/javascripts/vue_merge_request_widget/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/constants.js
@@ -162,3 +162,6 @@ export const EXTENSION_SUMMARY_FAILED_CLASS = 'gl-text-red-500';
export const EXTENSION_SUMMARY_NEUTRAL_CLASS = 'gl-text-gray-700';
export { STATE_MACHINE };
+
+export const REBASE_BUTTON_KEY = 'rebase';
+export const REBASE_WITHOUT_CI_BUTTON_KEY = 'rebaseWithoutCi';
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js
new file mode 100644
index 00000000000..a564acada02
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/extensions/terraform/index.js
@@ -0,0 +1,173 @@
+import { __, n__, s__, sprintf } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+import { EXTENSION_ICONS } from '../../constants';
+
+export default {
+ name: 'WidgetTerraform',
+ enablePolling: true,
+ i18n: {
+ label: s__('Terraform|Terraform reports'),
+ loading: s__('Terraform|Loading Terraform reports...'),
+ error: s__('Terraform|Failed to load Terraform reports'),
+ reportGenerated: s__('Terraform|A Terraform report was generated in your pipelines.'),
+ namedReportGenerated: s__(
+ 'Terraform|The job %{strong_start}%{name}%{strong_end} generated a report.',
+ ),
+ reportChanges: s__(
+ 'Terraform|Reported Resource Changes: %{addNum} to add, %{changeNum} to change, %{deleteNum} to delete',
+ ),
+ reportFailed: s__('Terraform|A Terraform report failed to generate.'),
+ namedReportFailed: s__(
+ 'Terraform|The job %{strong_start}%{name}%{strong_end} failed to generate a report.',
+ ),
+ reportErrored: s__('Terraform|Generating the report caused an error.'),
+ fullLog: __('Full log'),
+ },
+ expandEvent: 'i_testing_terraform_widget_total',
+ props: ['terraformReportsPath'],
+ computed: {
+ // Extension computed props
+ statusIcon() {
+ return EXTENSION_ICONS.warning;
+ },
+ },
+ methods: {
+ // Extension methods
+ summary({ valid = [], invalid = [] }) {
+ let title;
+ let subtitle = '';
+
+ const validText = sprintf(
+ n__(
+ 'Terraform|%{strong_start}%{number}%{strong_end} Terraform report was generated in your pipelines',
+ 'Terraform|%{strong_start}%{number}%{strong_end} Terraform reports were generated in your pipelines',
+ valid.length,
+ ),
+ {
+ number: valid.length,
+ },
+ false,
+ );
+
+ const invalidText = sprintf(
+ n__(
+ 'Terraform|%{strong_start}%{number}%{strong_end} Terraform report failed to generate',
+ 'Terraform|%{strong_start}%{number}%{strong_end} Terraform reports failed to generate',
+ invalid.length,
+ ),
+ {
+ number: invalid.length,
+ },
+ false,
+ );
+
+ if (valid.length) {
+ title = validText;
+ if (invalid.length) {
+ subtitle = sprintf(`<br>%{small_start}${invalidText}%{small_end}`);
+ }
+ } else {
+ title = invalidText;
+ }
+
+ return `${title}${subtitle}`;
+ },
+ fetchCollapsedData() {
+ return Promise.resolve(this.fetchPlans().then(this.prepareReports));
+ },
+ fetchFullData() {
+ const { valid, invalid } = this.collapsedData;
+ return Promise.resolve([...valid, ...invalid]);
+ },
+ // Custom methods
+ fetchPlans() {
+ return axios
+ .get(this.terraformReportsPath)
+ .then(({ data }) => {
+ return Object.keys(data).map((key) => {
+ return data[key];
+ });
+ })
+ .catch(() => {
+ const invalidData = { tf_report_error: 'api_error' };
+ return [invalidData];
+ });
+ },
+ createReportRow(report, iconName) {
+ const addNum = Number(report.create);
+ const changeNum = Number(report.update);
+ const deleteNum = Number(report.delete);
+ const validPlanValues = addNum + changeNum + deleteNum >= 0;
+
+ const actions = [];
+
+ let title;
+ let subtitle;
+
+ if (report.job_path) {
+ const action = {
+ href: report.job_path,
+ text: this.$options.i18n.fullLog,
+ target: '_blank',
+ };
+ actions.push(action);
+ }
+
+ if (validPlanValues) {
+ if (report.job_name) {
+ title = sprintf(
+ this.$options.i18n.namedReportGenerated,
+ {
+ name: report.job_name,
+ },
+ false,
+ );
+ } else {
+ title = this.$options.i18n.reportGenerated;
+ }
+
+ subtitle = sprintf(`%{small_start}${this.$options.i18n.reportChanges}%{small_end}`, {
+ addNum,
+ changeNum,
+ deleteNum,
+ });
+ } else {
+ if (report.job_name) {
+ title = sprintf(
+ this.$options.i18n.namedReportFailed,
+ {
+ name: report.job_name,
+ },
+ false,
+ );
+ } else {
+ title = this.$options.i18n.reportFailed;
+ }
+
+ subtitle = sprintf(`%{small_start}${this.$options.i18n.reportErrored}%{small_end}`);
+ }
+
+ return {
+ text: `${title}
+ <br>
+ ${subtitle}`,
+ icon: { name: iconName },
+ actions,
+ };
+ },
+ prepareReports(reports) {
+ const valid = [];
+ const invalid = [];
+
+ reports.forEach((report) => {
+ if (report.tf_report_error) {
+ invalid.push(this.createReportRow(report, EXTENSION_ICONS.error));
+ } else {
+ valid.push(this.createReportRow(report, EXTENSION_ICONS.success));
+ }
+ });
+
+ return { valid, invalid };
+ },
+ },
+};
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 c98dc426224..83a07240403 100644
--- a/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue
@@ -1,6 +1,7 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import { isEmpty } from 'lodash';
+import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import MrWidgetApprovals from 'ee_else_ce/vue_merge_request_widget/components/approvals/approvals.vue';
import MRWidgetService from 'ee_else_ce/vue_merge_request_widget/services/mr_widget_service';
import MRWidgetStore from 'ee_else_ce/vue_merge_request_widget/stores/mr_widget_store';
@@ -43,6 +44,7 @@ import { STATE_MACHINE, stateToComponentMap } from './constants';
import eventHub from './event_hub';
import mergeRequestQueryVariablesMixin from './mixins/merge_request_query_variables';
import getStateQuery from './queries/get_state.query.graphql';
+import terraformExtension from './extensions/terraform';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -184,6 +186,9 @@ export default {
shouldRenderSecurityReport() {
return Boolean(this.mr.pipeline.id);
},
+ shouldRenderTerraformPlans() {
+ return Boolean(this.mr?.terraformReportsPath);
+ },
mergeError() {
let { mergeError } = this.mr;
@@ -230,6 +235,11 @@ export default {
this.initPostMergeDeploymentsPolling();
}
},
+ shouldRenderTerraformPlans(newVal) {
+ if (newVal) {
+ this.registerTerraformPlans();
+ }
+ },
},
mounted() {
MRWidgetService.fetchInitialData()
@@ -463,6 +473,11 @@ export default {
dismissSuggestPipelines() {
this.mr.isDismissedSuggestPipeline = true;
},
+ registerTerraformPlans() {
+ if (this.shouldRenderTerraformPlans && this.shouldShowExtension) {
+ registerExtension(terraformExtension);
+ }
+ },
},
};
</script>
@@ -542,7 +557,10 @@ export default {
:pipeline-path="mr.pipeline.path"
/>
- <terraform-plan v-if="mr.terraformReportsPath" :endpoint="mr.terraformReportsPath" />
+ <terraform-plan
+ v-if="mr.terraformReportsPath && !shouldShowExtension"
+ :endpoint="mr.terraformReportsPath"
+ />
<grouped-accessibility-reports-app
v-if="shouldShowAccessibilityReport"
diff --git a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
index 7dcb4881e7f..7b803b0fcbb 100644
--- a/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
+++ b/app/assets/javascripts/vue_merge_request_widget/services/mr_widget_service.js
@@ -55,8 +55,9 @@ export default class MRWidgetService {
return axios.get(this.endpoints.mergeActionsContentPath);
}
- rebase() {
- return axios.post(this.endpoints.rebasePath);
+ rebase({ skipCi = false } = {}) {
+ const path = `${this.endpoints.rebasePath}?skip_ci=${Boolean(skipCi)}`;
+ return axios.post(path);
}
fetchApprovals() {
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 57af869a0ba..5378dabf638 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
@@ -222,6 +222,8 @@ export default class MergeRequestStore {
this.showGitpodButton = data.show_gitpod_button;
this.gitpodUrl = data.gitpod_url;
this.gitpodEnabled = data.gitpod_enabled;
+ this.userPreferencesGitpodPath = data.user_preferences_gitpod_path;
+ this.userProfileEnableGitpodPath = data.user_profile_enable_gitpod_path;
}
setState() {
@@ -357,15 +359,13 @@ export default class MergeRequestStore {
setApprovals(data) {
this.approvals = data;
this.isApproved = data.approved || false;
+
+ this.setState();
}
+ // eslint-disable-next-line class-methods-use-this
get hasMergeChecksFailed() {
- if (!window.gon?.features?.restructuredMrWidget) return false;
-
- return (
- this.hasMergeableDiscussionsState ||
- (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed)
- );
+ return false;
}
// Because the state machine doesn't yet handle every state and transition,
diff --git a/app/assets/javascripts/vue_shared/components/actions_button.vue b/app/assets/javascripts/vue_shared/components/actions_button.vue
index bab13fe7c75..6db18afe51c 100644
--- a/app/assets/javascripts/vue_shared/components/actions_button.vue
+++ b/app/assets/javascripts/vue_shared/components/actions_button.vue
@@ -66,6 +66,7 @@ export default {
:variant="variant"
:category="category"
split
+ data-qa-selector="action_dropdown"
@click="handleClick(selectedAction, $event)"
>
<template #button-content>
@@ -79,6 +80,7 @@ export default {
:is-check-item="true"
:is-checked="action.key === selectedAction.key"
:secondary-text="action.secondaryText"
+ :data-qa-selector="`${action.key}_menu_item`"
:data-testid="`action_${action.key}`"
@click="handleItemClick(action)"
>
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 400be3ef688..f907b64608c 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -13,9 +13,23 @@
* />
*/
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { uniqueId } from 'lodash';
+
+import { __ } from '~/locale';
+import {
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
+ I18N_ERROR_MESSAGE,
+} from '~/behaviors/copy_to_clipboard';
export default {
name: 'ClipboardButton',
+ i18n: {
+ copied: __('Copied'),
+ error: I18N_ERROR_MESSAGE,
+ },
+ CLIPBOARD_SUCCESS_EVENT,
+ CLIPBOARD_ERROR_EVENT,
directives: {
GlTooltip: GlTooltipDirective,
},
@@ -72,6 +86,13 @@ export default {
default: 'default',
},
},
+ data() {
+ return {
+ localTitle: this.title,
+ titleTimeout: null,
+ id: null,
+ };
+ },
computed: {
clipboardText() {
if (this.gfm !== null) {
@@ -79,25 +100,50 @@ export default {
}
return this.text;
},
+ tooltipDirectiveOptions() {
+ return {
+ placement: this.tooltipPlacement,
+ container: this.tooltipContainer,
+ boundary: this.tooltipBoundary,
+ };
+ },
+ },
+ created() {
+ this.id = uniqueId('clipboard-button-');
+ },
+ methods: {
+ updateTooltip(title) {
+ this.localTitle = title;
+ this.$root.$emit('bv::show::tooltip', this.id);
+
+ clearTimeout(this.titleTimeout);
+
+ this.titleTimeout = setTimeout(() => {
+ this.localTitle = this.title;
+ this.$root.$emit('bv::hide::tooltip', this.id);
+ }, 1000);
+ },
},
};
</script>
<template>
<gl-button
- v-gl-tooltip.hover.blur.viewport="{
- placement: tooltipPlacement,
- container: tooltipContainer,
- boundary: tooltipBoundary,
- }"
+ :id="id"
+ ref="copyButton"
+ v-gl-tooltip.hover.focus.click.viewport="tooltipDirectiveOptions"
:class="cssClass"
- :title="title"
+ :title="localTitle"
:data-clipboard-text="clipboardText"
+ data-clipboard-handle-tooltip="false"
:category="category"
:size="size"
icon="copy-to-clipboard"
- :aria-label="__('Copy this value')"
:variant="variant"
+ :aria-label="localTitle"
+ aria-live="polite"
+ @[$options.CLIPBOARD_SUCCESS_EVENT]="updateTooltip($options.i18n.copied)"
+ @[$options.CLIPBOARD_ERROR_EVENT]="updateTooltip($options.i18n.error)"
v-on="$listeners"
>
<slot></slot>
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
index f93415ced45..e12e06a2454 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: 'confirm-danger-button',
},
+ buttonVariant: {
+ type: String,
+ required: false,
+ default: 'danger',
+ },
},
modalId: CONFIRM_DANGER_MODAL_ID,
};
@@ -45,7 +50,7 @@ export default {
<gl-button
v-gl-modal="$options.modalId"
:class="buttonClass"
- variant="danger"
+ :variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
>{{ buttonText }}</gl-button
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
index 18fa297da87..6629b293eb9 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.stories.js
@@ -11,7 +11,9 @@ const Template = (args, { argTypes }) => ({
props: Object.keys(argTypes),
template: '<confirm-danger v-bind="$props" />',
provide: {
- confirmDangerMessage: 'You require more Vespene Gas',
+ additionalInformation: args.additionalInformation || null,
+ confirmDangerMessage: args.confirmDangerMessage || 'You require more Vespene Gas',
+ htmlConfirmationMessage: args.confirmDangerMessage || false,
},
});
@@ -26,3 +28,16 @@ Disabled.args = {
...Default.args,
disabled: true,
};
+
+export const AdditionalInformation = Template.bind({});
+AdditionalInformation.args = {
+ ...Default.args,
+ additionalInformation: 'This replaces the default warning information',
+};
+
+export const HtmlMessage = Template.bind({});
+HtmlMessage.args = {
+ ...Default.args,
+ confirmDangerMessage: 'You strongly require more <strong>Vespene Gas</strong>',
+ htmlConfirmationMessage: true,
+};
diff --git a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
index 5bbe44b20b3..88890b3332d 100644
--- a/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue
@@ -1,5 +1,12 @@
<script>
-import { GlAlert, GlModal, GlFormGroup, GlFormInput, GlSprintf } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlModal,
+ GlFormGroup,
+ GlFormInput,
+ GlSafeHtmlDirective as SafeHtml,
+ GlSprintf,
+} from '@gitlab/ui';
import {
CONFIRM_DANGER_MODAL_BUTTON,
CONFIRM_DANGER_MODAL_TITLE,
@@ -17,13 +24,22 @@ export default {
GlFormInput,
GlSprintf,
},
+ directives: {
+ SafeHtml,
+ },
inject: {
+ htmlConfirmationMessage: {
+ default: false,
+ },
confirmDangerMessage: {
default: '',
},
confirmButtonText: {
default: CONFIRM_DANGER_MODAL_BUTTON,
},
+ additionalInformation: {
+ default: CONFIRM_DANGER_WARNING,
+ },
},
props: {
modalId: {
@@ -81,9 +97,12 @@ export default {
:dismissible="false"
class="gl-mb-4"
>
- {{ confirmDangerMessage }}
+ <span v-if="htmlConfirmationMessage" v-safe-html="confirmDangerMessage"></span>
+ <span v-else>
+ {{ confirmDangerMessage }}
+ </span>
</gl-alert>
- <p data-testid="confirm-danger-warning">{{ $options.i18n.CONFIRM_DANGER_WARNING }}</p>
+ <p data-testid="confirm-danger-warning">{{ additionalInformation }}</p>
<p data-testid="confirm-danger-phrase">
<gl-sprintf :message="$options.i18n.CONFIRM_DANGER_PHRASE_TEXT">
<template #phrase_code>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
index 7c1828f2294..5cdf7b6a3b2 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue
@@ -332,7 +332,7 @@ export default {
v-if="showCheckbox"
class="gl-align-self-center"
:checked="checkboxChecked"
- @input="$emit('checked-input', $event)"
+ @change="$emit('checked-input', $event)"
>
<span class="gl-sr-only">{{ __('Select all') }}</span>
</gl-form-checkbox>
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 06478a89721..b70317b2ec4 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
@@ -4,7 +4,7 @@ import { compact } from 'lodash';
import createFlash from '~/flash';
import { __ } from '~/locale';
-import { DEFAULT_LABEL_ANY } from '../constants';
+import { DEFAULT_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -36,7 +36,7 @@ export default {
},
computed: {
defaultAuthors() {
- return this.config.defaultAuthors || [DEFAULT_LABEL_ANY];
+ return this.config.defaultAuthors || DEFAULT_NONE_ANY;
},
preloadedAuthors() {
return this.config.preloadedAuthors || [];
diff --git a/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
new file mode 100644
index 00000000000..acddf16bd27
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/gitlab_version_check.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlBadge } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import axios from '~/lib/utils/axios_utils';
+
+const STATUS_TYPES = {
+ SUCCESS: 'success',
+ WARNING: 'warning',
+ DANGER: 'danger',
+};
+
+export default {
+ name: 'GitlabVersionCheck',
+ components: {
+ GlBadge,
+ },
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
+ data() {
+ return {
+ status: null,
+ };
+ },
+ computed: {
+ title() {
+ if (this.status === STATUS_TYPES.SUCCESS) {
+ return s__('VersionCheck|Up to date');
+ } else if (this.status === STATUS_TYPES.WARNING) {
+ return s__('VersionCheck|Update available');
+ } else if (this.status === STATUS_TYPES.DANGER) {
+ return s__('VersionCheck|Update ASAP');
+ }
+
+ return null;
+ },
+ },
+ created() {
+ this.checkGitlabVersion();
+ },
+ methods: {
+ checkGitlabVersion() {
+ axios
+ .get('/admin/version_check.json')
+ .then((res) => {
+ if (res.data) {
+ this.status = res.data.severity;
+ }
+ })
+ .catch(() => {
+ // Silently fail
+ this.status = null;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-badge v-if="status" class="version-check-badge" :variant="status" :size="size">{{
+ title
+ }}</gl-badge>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/line_numbers.vue b/app/assets/javascripts/vue_shared/components/line_numbers.vue
index 7e17cca3dcc..11caf3be00a 100644
--- a/app/assets/javascripts/vue_shared/components/line_numbers.vue
+++ b/app/assets/javascripts/vue_shared/components/line_numbers.vue
@@ -12,31 +12,6 @@ export default {
required: true,
},
},
- data() {
- return {
- currentlyHighlightedLine: null,
- };
- },
- mounted() {
- this.scrollToLine();
- },
- methods: {
- scrollToLine(hash = window.location.hash) {
- const lineToHighlight = hash && this.$el.querySelector(hash);
-
- if (!lineToHighlight) {
- return;
- }
-
- if (this.currentlyHighlightedLine) {
- this.currentlyHighlightedLine.classList.remove('hll');
- }
-
- lineToHighlight.classList.add('hll');
- this.currentlyHighlightedLine = lineToHighlight;
- lineToHighlight.scrollIntoView({ behavior: 'smooth', block: 'center' });
- },
- },
};
</script>
<template>
@@ -45,10 +20,9 @@ export default {
v-for="line in lines"
:id="`L${line}`"
:key="line"
- class="diff-line-num"
- :href="`#L${line}`"
+ class="diff-line-num gl-shadow-none!"
+ :to="`#LC${line}`"
:data-line-number="line"
- @click="scrollToLine(`#L${line}`)"
>
<gl-icon :size="12" name="link" />
{{ line }}
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
index ce7cbafb97d..709d3592828 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue
@@ -67,7 +67,7 @@ export default {
<gl-button
class="gl-w-auto! gl-mt-3 gl-text-center! gl-hover-text-white! gl-transition-medium! float-right"
category="primary"
- variant="success"
+ variant="confirm"
data-qa-selector="commit_with_custom_message_button"
@click="onApply"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 86f04c78ebe..5c86c928ce3 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -2,7 +2,7 @@
import { GlIcon } from '@gitlab/ui';
import $ from 'jquery';
import '~/behaviors/markdown/render_gfm';
-import { unescape } from 'lodash';
+import { debounce, unescape } from 'lodash';
import createFlash from '~/flash';
import GLForm from '~/gl_form';
import axios from '~/lib/utils/axios_utils';
@@ -110,7 +110,7 @@ export default {
return {
markdownPreview: '',
referencedCommands: '',
- referencedUsers: '',
+ referencedUsers: [],
hasSuggestion: false,
markdownPreviewLoading: false,
previewMarkdown: false,
@@ -188,6 +188,24 @@ export default {
});
}
},
+
+ textareaValue: {
+ immediate: true,
+ handler(textareaValue, oldVal) {
+ const all = /@all([^\w._-]|$)/;
+ const hasAll = all.test(textareaValue);
+ const hadAll = all.test(oldVal);
+
+ const justAddedAll = !hadAll && hasAll;
+ const justRemovedAll = hadAll && !hasAll;
+
+ if (justAddedAll) {
+ this.debouncedFetchMarkdown();
+ } else if (justRemovedAll) {
+ this.referencedUsers = [];
+ }
+ },
+ },
},
mounted() {
// GLForm class handles all the toolbar buttons
@@ -222,9 +240,9 @@ export default {
if (this.textareaValue) {
this.markdownPreviewLoading = true;
this.markdownPreview = __('Loading…');
- axios
- .post(this.markdownPreviewPath, { text: this.textareaValue })
- .then((response) => this.renderMarkdown(response.data))
+
+ this.fetchMarkdown()
+ .then((data) => this.renderMarkdown(data))
.catch(() =>
createFlash({
message: __('Error loading markdown preview'),
@@ -239,17 +257,28 @@ export default {
this.previewMarkdown = false;
},
+ fetchMarkdown() {
+ return axios.post(this.markdownPreviewPath, { text: this.textareaValue }).then(({ data }) => {
+ const { references } = data;
+ if (references) {
+ this.referencedCommands = references.commands;
+ this.referencedUsers = references.users;
+ this.hasSuggestion = references.suggestions?.length > 0;
+ this.suggestions = references.suggestions;
+ }
+
+ return data;
+ });
+ },
+
+ debouncedFetchMarkdown: debounce(function debouncedFetchMarkdown() {
+ return this.fetchMarkdown();
+ }, 400),
+
renderMarkdown(data = {}) {
this.markdownPreviewLoading = false;
this.markdownPreview = data.body || __('Nothing to preview.');
- if (data.references) {
- this.referencedCommands = data.references.commands;
- this.referencedUsers = data.references.users;
- this.hasSuggestion = data.references.suggestions && data.references.suggestions.length;
- this.suggestions = data.references.suggestions;
- }
-
this.$nextTick()
.then(() => $(this.$refs['markdown-preview']).renderGFM())
.catch(() =>
@@ -326,18 +355,14 @@ export default {
v-html="markdownPreview /* eslint-disable-line vue/no-v-html */"
></div>
</template>
- <template v-if="previewMarkdown && !markdownPreviewLoading">
- <div
- v-if="referencedCommands"
- class="referenced-commands"
- v-html="referencedCommands /* eslint-disable-line vue/no-v-html */"
- ></div>
- <div v-if="shouldShowReferencedUsers" class="referenced-users">
- <gl-icon name="warning-solid" />
- <span
- v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"
- ></span>
- </div>
- </template>
+ <div
+ v-if="referencedCommands && previewMarkdown && !markdownPreviewLoading"
+ class="referenced-commands"
+ v-html="referencedCommands /* eslint-disable-line vue/no-v-html */"
+ ></div>
+ <div v-if="shouldShowReferencedUsers" class="referenced-users">
+ <gl-icon name="warning-solid" />
+ <span v-html="addMultipleToDiscussionWarning /* eslint-disable-line vue/no-v-html */"></span>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
index 38afd56bae6..d4f50e347cb 100644
--- a/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
+++ b/app/assets/javascripts/vue_shared/components/modal_copy_button.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import Clipboard from 'clipboard';
+import ClipboardJS from 'clipboard';
import { uniqueId } from 'lodash';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
@@ -69,7 +69,7 @@ export default {
},
mounted() {
this.$nextTick(() => {
- this.clipboard = new Clipboard(this.$el, {
+ this.clipboard = new ClipboardJS(this.$el, {
container:
document.querySelector(`${this.modalDomId} div.modal-content`) ||
document.getElementById(this.container) ||
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
index 13a6dd43207..0fa64a29b3a 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue
@@ -179,6 +179,9 @@ export default {
this.searchKey = '';
this.setFocus();
},
+ selectFirstItem() {
+ this.$refs.dropdownContentsView.selectFirstItem();
+ },
},
};
</script>
@@ -204,11 +207,13 @@ export default {
@toggleDropdownContentsCreateView="toggleDropdownContent"
@closeDropdown="$emit('closeDropdown')"
@input="debouncedSearchKeyUpdate"
+ @searchEnter="selectFirstItem"
/>
</template>
<template #default>
<component
:is="dropdownContentsView"
+ ref="dropdownContentsView"
v-model="localSelectedLabels"
:search-key="searchKey"
:allow-multiselect="allowMultiselect"
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 da626a21b14..b99083713a8 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
@@ -1,5 +1,12 @@
<script>
-import { GlTooltipDirective, GlButton, GlFormInput, GlLink, GlLoadingIcon } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlTooltipDirective,
+ GlButton,
+ GlFormInput,
+ GlLink,
+ GlLoadingIcon,
+} from '@gitlab/ui';
import produce from 'immer';
import createFlash from '~/flash';
import { __ } from '~/locale';
@@ -11,6 +18,7 @@ const errorMessage = __('Error creating label.');
export default {
components: {
+ GlAlert,
GlButton,
GlFormInput,
GlLink,
@@ -42,6 +50,7 @@ export default {
labelTitle: '',
selectedColor: '',
labelCreateInProgress: false,
+ error: undefined,
};
},
computed: {
@@ -111,13 +120,14 @@ export default {
) => this.updateLabelsInCache(store, label),
});
if (labelCreate.errors.length) {
- createFlash({ message: errorMessage });
+ [this.error] = labelCreate.errors;
+ } else {
+ this.$emit('hideCreateView');
}
} catch {
createFlash({ message: errorMessage });
}
this.labelCreateInProgress = false;
- this.$emit('hideCreateView');
},
},
};
@@ -126,6 +136,9 @@ export default {
<template>
<div class="labels-select-contents-create js-labels-create">
<div class="dropdown-input">
+ <gl-alert v-if="error" variant="danger" :dismissible="false" class="gl-mb-3">
+ {{ error }}
+ </gl-alert>
<gl-form-input
v-model.trim="labelTitle"
:placeholder="__('Name new label')"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
index e9a2d7747e2..ae179ef93c7 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents_labels_view.vue
@@ -84,6 +84,9 @@ export default {
showNoMatchingResultsMessage() {
return Boolean(this.searchKey) && this.visibleLabels.length === 0;
},
+ shouldHighlightFirstItem() {
+ return this.searchKey !== '' && this.visibleLabels.length > 0;
+ },
},
methods: {
isLabelSelected(label) {
@@ -128,6 +131,11 @@ export default {
onDropdownAppear() {
this.isVisible = true;
},
+ selectFirstItem() {
+ if (this.shouldHighlightFirstItem) {
+ this.handleLabelClick(this.visibleLabels[0]);
+ }
+ },
},
};
</script>
@@ -143,11 +151,13 @@ export default {
/>
<template v-else>
<gl-dropdown-item
- v-for="label in visibleLabels"
+ v-for="(label, index) in visibleLabels"
:key="label.id"
:is-checked="isLabelSelected(label)"
:is-check-centered="true"
:is-check-item="true"
+ :active="shouldHighlightFirstItem && index === 0"
+ active-class="is-focused"
data-testid="labels-list"
@click.native.capture.stop="handleLabelClick(label)"
>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
index 7a0f20b0c83..faad69732dd 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_header.vue
@@ -83,6 +83,7 @@ export default {
data-qa-selector="dropdown_input_field"
data-testid="dropdown-input-field"
@input="$emit('input', $event)"
+ @keydown.enter="$emit('searchEnter', $event)"
/>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
index f27f0b4e34c..caeee2df7e5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/label_item.vue
@@ -10,7 +10,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-align-items-center">
<span
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ 'background-color': label.color }"
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 3adda69b892..f53b75df4eb 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
@@ -289,6 +289,7 @@ export default {
'is-standalone': isDropdownVariantStandalone(variant),
'is-embedded': isDropdownVariantEmbedded(variant),
}"
+ data-testid="sidebar-labels"
data-qa-selector="labels_block"
>
<template v-if="isDropdownVariantSidebar(variant)">
diff --git a/app/assets/javascripts/vue_shared/components/source_editor.vue b/app/assets/javascripts/vue_shared/components/source_editor.vue
index 8a0fef36079..011cad4267c 100644
--- a/app/assets/javascripts/vue_shared/components/source_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/source_editor.vue
@@ -97,7 +97,7 @@ export default {
ref="editor"
data-editor-loading
data-qa-selector="source_editor_container"
- @[$options.readyEvent]="$emit($options.readyEvent)"
+ @[$options.readyEvent]="$emit($options.readyEvent, $event)"
>
<pre class="editor-loading-content">{{ value }}</pre>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer.vue b/app/assets/javascripts/vue_shared/components/source_viewer.vue
index 8f0d051543f..99895926653 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer.vue
@@ -1,6 +1,9 @@
<script>
import { GlSafeHtmlDirective } from '@gitlab/ui';
import LineNumbers from '~/vue_shared/components/line_numbers.vue';
+import { sanitize } from '~/lib/dompurify';
+
+const LINE_SELECT_CLASS_NAME = 'hll';
export default {
components: {
@@ -46,7 +49,15 @@ export default {
}
}
- return highlightedContent;
+ return this.wrapLines(highlightedContent);
+ },
+ },
+ watch: {
+ highlightedContent() {
+ this.$nextTick(() => this.selectLine());
+ },
+ $route() {
+ this.selectLine();
},
},
async mounted() {
@@ -73,16 +84,40 @@ export default {
return languageDefinition;
},
+ wrapLines(content) {
+ return (
+ content &&
+ content
+ .split('\n')
+ .map((line, i) => `<span id="LC${i + 1}" class="line">${line}</span>`)
+ .join('\r\n')
+ );
+ },
+ selectLine() {
+ const hash = sanitize(this.$route.hash);
+ const lineToSelect = hash && this.$el.querySelector(hash);
+
+ if (!lineToSelect) {
+ return;
+ }
+
+ if (this.$options.currentlySelectedLine) {
+ this.$options.currentlySelectedLine.classList.remove(LINE_SELECT_CLASS_NAME);
+ }
+
+ lineToSelect.classList.add(LINE_SELECT_CLASS_NAME);
+ this.$options.currentlySelectedLine = lineToSelect;
+ lineToSelect.scrollIntoView({ behavior: 'smooth', block: 'center' });
+ },
},
userColorScheme: window.gon.user_color_scheme,
+ currentlySelectedLine: null,
};
</script>
<template>
- <div class="file-content code" :class="$options.userColorScheme">
+ <div class="file-content code js-syntax-highlight" :class="$options.userColorScheme">
<line-numbers :lines="lineNumbers" />
- <pre
- class="code gl-pl-3!"
- ><code v-safe-html="highlightedContent" class="gl-white-space-pre-wrap!"></code>
+ <pre class="code"><code v-safe-html="highlightedContent"></code>
</pre>
</div>
</template>
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 6da2d39a95a..f02cd5c4e2e 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -1,6 +1,7 @@
<script>
import $ from 'jquery';
-import { __ } from '~/locale';
+import { GlModal, GlSprintf, GlLink } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -12,6 +13,19 @@ export default {
components: {
ActionsButton,
LocalStorageSync,
+ GlModal,
+ GlSprintf,
+ GlLink,
+ },
+ i18n: {
+ modal: {
+ title: __('Enable Gitpod?'),
+ content: s__(
+ 'Gitpod|To use Gitpod you must first enable the feature in the integrations section of your %{linkStart}user preferences%{linkEnd}.',
+ ),
+ actionCancelText: __('Cancel'),
+ actionPrimaryText: __('Enable Gitpod'),
+ },
},
props: {
isFork: {
@@ -49,6 +63,16 @@ export default {
required: false,
default: false,
},
+ userPreferencesGitpodPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ userProfileEnableGitpodPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
editUrl: {
type: String,
required: false,
@@ -74,10 +98,16 @@ export default {
required: false,
default: '',
},
+ disableForkModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
selection: KEY_WEB_IDE,
+ showEnableGitpodModal: false,
};
},
computed: {
@@ -93,8 +123,12 @@ export default {
? {
href: '#modal-confirm-fork-edit',
handle: () => {
- this.$emit('edit', 'simple');
- this.showModal('#modal-confirm-fork-edit');
+ if (this.disableForkModal) {
+ this.$emit('edit', 'simple');
+ return;
+ }
+
+ this.showJQueryModal('#modal-confirm-fork-edit');
},
}
: { href: this.editUrl };
@@ -132,8 +166,12 @@ export default {
? {
href: '#modal-confirm-fork-webide',
handle: () => {
- this.$emit('edit', 'ide');
- this.showModal('#modal-confirm-fork-webide');
+ if (this.disableForkModal) {
+ this.$emit('edit', 'ide');
+ return;
+ }
+
+ this.showJQueryModal('#modal-confirm-fork-webide');
},
}
: { href: this.webIdeUrl };
@@ -154,14 +192,23 @@ export default {
gitpodActionText() {
return this.gitpodText || __('Gitpod');
},
+ computedShowGitpodButton() {
+ return (
+ this.showGitpodButton && this.userPreferencesGitpodPath && this.userProfileEnableGitpodPath
+ );
+ },
gitpodAction() {
- if (!this.showGitpodButton) {
+ if (!this.computedShowGitpodButton) {
return null;
}
const handleOptions = this.gitpodEnabled
? { href: this.gitpodUrl }
- : { href: '#modal-enable-gitpod', handle: () => this.showModal('#modal-enable-gitpod') };
+ : {
+ handle: () => {
+ this.showModal('showEnableGitpodModal');
+ },
+ };
const secondaryText = __('Launch a ready-to-code development environment for your project.');
@@ -176,14 +223,36 @@ export default {
...handleOptions,
};
},
+ enableGitpodModalProps() {
+ return {
+ 'modal-id': 'enable-gitpod-modal',
+ size: 'sm',
+ title: this.$options.i18n.modal.title,
+ 'action-cancel': {
+ text: this.$options.i18n.modal.actionCancelText,
+ },
+ 'action-primary': {
+ text: this.$options.i18n.modal.actionPrimaryText,
+ attributes: {
+ variant: 'confirm',
+ category: 'primary',
+ href: this.userProfileEnableGitpodPath,
+ 'data-method': 'put',
+ },
+ },
+ };
+ },
},
methods: {
select(key) {
this.selection = key;
},
- showModal(id) {
+ showJQueryModal(id) {
$(id).modal('show');
},
+ showModal(dataKey) {
+ this[dataKey] = true;
+ },
},
};
</script>
@@ -202,5 +271,16 @@ export default {
:value="selection"
@input="select"
/>
+ <gl-modal
+ v-if="computedShowGitpodButton && !gitpodEnabled"
+ v-model="showEnableGitpodModal"
+ v-bind="enableGitpodModalProps"
+ >
+ <gl-sprintf :message="$options.i18n.modal.content">
+ <template #link="{ content }">
+ <gl-link :href="userPreferencesGitpodPath">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-modal>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
index 5ca9e50d854..bcc889495bd 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_bulk_edit_sidebar.vue
@@ -13,6 +13,7 @@ export default {
if (layoutPageEl) {
layoutPageEl.classList.toggle('right-sidebar-expanded', value);
layoutPageEl.classList.toggle('right-sidebar-collapsed', !value);
+ layoutPageEl.classList.toggle('issuable-bulk-update-sidebar', !value);
}
},
},
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index 0bb0e0d9fb0..af0235bfc69 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -193,7 +193,13 @@ export default {
:title="__('This issue is hidden because its author has been banned')"
:aria-label="__('Hidden')"
/>
- <gl-link class="issue-title-text" dir="auto" :href="webUrl" v-bind="issuableTitleProps">
+ <gl-link
+ class="issue-title-text"
+ dir="auto"
+ :href="webUrl"
+ data-qa-selector="issuable_title_link"
+ v-bind="issuableTitleProps"
+ >
{{ issuable.title }}
<gl-icon v-if="isIssuableUrlExternal" name="external-link" class="gl-ml-2" />
</gl-link>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
index 3ff87ba3c4f..9bf54e98cc4 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_tabs.vue
@@ -1,5 +1,6 @@
<script>
import { GlTabs, GlTab, GlBadge } from '@gitlab/ui';
+import { formatNumber } from '~/locale';
export default {
components: {
@@ -29,6 +30,9 @@ export default {
isTabCountNumeric(tab) {
return Number.isInteger(this.tabCounts[tab.name]);
},
+ formatNumber(count) {
+ return formatNumber(count);
+ },
},
};
</script>
@@ -55,7 +59,7 @@ export default {
size="sm"
class="gl-tab-counter-badge"
>
- {{ tabCounts[tab.name] }}
+ {{ formatNumber(tabCounts[tab.name]) }}
</gl-badge>
</template>
</gl-tab>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/constants.js b/app/assets/javascripts/vue_shared/issuable/list/constants.js
index 773ad0f8e93..c6dce6a51c2 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/constants.js
+++ b/app/assets/javascripts/vue_shared/issuable/list/constants.js
@@ -38,7 +38,7 @@ export const AvailableSortOptions = [
},
{
id: 2,
- title: __('Last updated'),
+ title: __('Updated date'),
sortDirection: {
descending: 'updated_desc',
ascending: 'updated_asc',
diff --git a/app/assets/javascripts/vue_shared/issuable/show/constants.js b/app/assets/javascripts/vue_shared/issuable/show/constants.js
deleted file mode 100644
index 346f45c7d90..00000000000
--- a/app/assets/javascripts/vue_shared/issuable/show/constants.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const IssuableType = {
- Issue: 'issue',
- Incident: 'incident',
- TestCase: 'test_case',
-};
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 114f60c96ee..f67e590e2ce 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
@@ -10,10 +10,17 @@ export default {
GlIcon,
WelcomePage,
LegacyContainer,
+ CreditCardVerification: () =>
+ import('ee_component/pages/groups/new/components/credit_card_verification.vue'),
},
directives: {
SafeHtml,
},
+ inject: {
+ verificationRequired: {
+ default: false,
+ },
+ },
props: {
title: {
type: String,
@@ -41,6 +48,7 @@ export default {
data() {
return {
activePanelName: null,
+ verificationCompleted: false,
};
},
@@ -67,6 +75,10 @@ export default {
{ text: this.activePanel.title, href: `#${this.activePanel.name}` },
];
},
+
+ shouldVerify() {
+ return this.verificationRequired && !this.verificationCompleted;
+ },
},
created() {
@@ -93,12 +105,16 @@ export default {
localStorage.setItem(this.persistenceKey, this.activePanelName);
}
},
+ onVerified() {
+ this.verificationCompleted = true;
+ },
},
};
</script>
<template>
- <welcome-page v-if="!activePanelName" :panels="panels" :title="title">
+ <credit-card-verification v-if="shouldVerify" @verified="onVerified" />
+ <welcome-page v-else-if="!activePanelName" :panels="panels" :title="title">
<template #footer>
<slot name="welcome-footer"> </slot>
</template>
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 5e9e50a94f0..79840cc4f0f 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -2,7 +2,10 @@
import { escape } from 'lodash';
import { __ } from '~/locale';
+import { WI_TITLE_TRACK_LABEL } from '../constants';
+
export default {
+ WI_TITLE_TRACK_LABEL,
props: {
initialTitle: {
type: String,
@@ -56,6 +59,7 @@ export default {
role="textbox"
:aria-label="__('Title')"
:data-placeholder="placeholder"
+ :data-track-label="$options.WI_TITLE_TRACK_LABEL"
:contenteditable="!disabled"
class="gl-pseudo-placeholder"
@blur="handleBlur"
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index b39f68abf74..995c02a2c5b 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -1,3 +1,5 @@
export const widgetTypes = {
title: 'TITLE',
};
+
+export const WI_TITLE_TRACK_LABEL = 'item_title';
diff --git a/app/assets/javascripts/work_items/pages/work_item_root.vue b/app/assets/javascripts/work_items/pages/work_item_root.vue
index 479274baf3a..4262e169655 100644
--- a/app/assets/javascripts/work_items/pages/work_item_root.vue
+++ b/app/assets/javascripts/work_items/pages/work_item_root.vue
@@ -1,16 +1,21 @@
<script>
import { GlAlert } from '@gitlab/ui';
+import Tracking from '~/tracking';
import workItemQuery from '../graphql/work_item.query.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import { widgetTypes } from '../constants';
+import { widgetTypes, WI_TITLE_TRACK_LABEL } from '../constants';
import ItemTitle from '../components/item_title.vue';
+const trackingMixin = Tracking.mixin();
+
export default {
+ titleUpdatedEvent: 'updated_title',
components: {
ItemTitle,
GlAlert,
},
+ mixins: [trackingMixin],
props: {
id: {
type: String,
@@ -34,6 +39,14 @@ export default {
},
},
computed: {
+ tracking() {
+ return {
+ category: 'workItems:show',
+ action: 'updated_title',
+ label: WI_TITLE_TRACK_LABEL,
+ property: '[type_work_item]',
+ };
+ },
titleWidgetData() {
return this.workItem?.widgets?.nodes?.find((widget) => widget.type === widgetTypes.title);
},
@@ -50,6 +63,7 @@ export default {
},
},
});
+ this.track();
} catch {
this.error = true;
}