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>2023-12-19 14:01:45 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-12-19 14:01:45 +0300
commit9297025d0b7ddf095eb618dfaaab2ff8f2018d8b (patch)
tree865198c01d1824a9b098127baa3ab980c9cd2c06 /app/assets
parent6372471f43ee03c05a7c1f8b0c6ac6b8a7431dbe (diff)
Add latest changes from gitlab-org/gitlab@16-7-stable-eev16.7.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/logos/shimo.svg1
-rw-r--r--app/assets/javascripts/access_tokens/components/access_token_table_app.vue2
-rw-r--r--app/assets/javascripts/access_tokens/components/expires_at_field.vue2
-rw-r--r--app/assets/javascripts/access_tokens/components/new_access_token_app.vue4
-rw-r--r--app/assets/javascripts/add_context_commits_modal/store/actions.js10
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue15
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue2
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue156
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue133
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue22
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue98
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue56
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue49
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_actions.vue1
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/report_header.vue4
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/reported_content.vue4
-rw-r--r--app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql2
-rw-r--r--app/assets/javascripts/admin/abuse_report/index.js1
-rw-r--r--app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue2
-rw-r--r--app/assets/javascripts/analytics/shared/constants.js47
-rw-r--r--app/assets/javascripts/api/bulk_imports_api.js14
-rw-r--r--app/assets/javascripts/api/user_api.js20
-rw-r--r--app/assets/javascripts/authentication/password/components/password_input.vue6
-rw-r--r--app/assets/javascripts/authentication/password/index.js3
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue1
-rw-r--r--app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue12
-rw-r--r--app/assets/javascripts/badges/components/badge_form.vue12
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue6
-rw-r--r--app/assets/javascripts/badges/components/badge_settings.vue4
-rw-r--r--app/assets/javascripts/batch_comments/mixins/resolved_status.js7
-rw-r--r--app/assets/javascripts/behaviors/index.js3
-rw-r--r--app/assets/javascripts/behaviors/markdown/copy_as_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/shortcuts.js36
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/index.js16
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/keybindings.js5
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts.js84
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js8
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js10
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js9
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js9
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js10
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js10
-rw-r--r--app/assets/javascripts/blob/filepath_form/components/template_selector.vue1
-rw-r--r--app/assets/javascripts/blob_edit/blob_bundle.js2
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js19
-rw-r--r--app/assets/javascripts/boards/components/board_add_new_column.vue54
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue27
-rw-r--r--app/assets/javascripts/boards/components/board_card.vue29
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_card_move_to_position.vue23
-rw-r--r--app/assets/javascripts/boards/components/board_column.vue47
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue47
-rw-r--r--app/assets/javascripts/boards/components/board_content_sidebar.vue81
-rw-r--r--app/assets/javascripts/boards/components/board_filtered_search.vue37
-rw-r--r--app/assets/javascripts/boards/components/board_form.vue26
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue97
-rw-r--r--app/assets/javascripts/boards/components/board_list_header.vue92
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue28
-rw-r--r--app/assets/javascripts/boards/components/board_settings_sidebar.vue45
-rw-r--r--app/assets/javascripts/boards/components/board_top_bar.vue6
-rw-r--r--app/assets/javascripts/boards/components/boards_selector.vue50
-rw-r--r--app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue35
-rw-r--r--app/assets/javascripts/boards/index.js2
-rw-r--r--app/assets/javascripts/breadcrumb.js4
-rw-r--r--app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue7
-rw-r--r--app/assets/javascripts/ci/artifacts/constants.js1
-rw-r--r--app/assets/javascripts/ci/artifacts/index.js3
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue2
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue11
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue19
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue4
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue14
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_search.vue81
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue84
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/empty_state.vue64
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue13
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue91
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js40
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql7
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql23
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql5
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql4
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql18
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js44
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/typedefs.graphql11
-rw-r--r--app/assets/javascripts/ci/catalog/index.js3
-rw-r--r--app/assets/javascripts/ci/catalog/router/routes.js2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue125
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js4
-rw-r--r--app/assets/javascripts/ci/common/private/job_name_component.vue10
-rw-r--r--app/assets/javascripts/ci/job_details/components/environments_block.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_header.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/job_log_controllers.vue71
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue71
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_header.vue11
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/line_number.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/log/log.vue86
-rw-r--r--app/assets/javascripts/ci/job_details/components/manual_variables_form.vue5
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue38
-rw-r--r--app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue2
-rw-r--r--app/assets/javascripts/ci/job_details/index.js25
-rw-r--r--app/assets/javascripts/ci/job_details/job_app.vue19
-rw-r--r--app/assets/javascripts/ci/job_details/store/actions.js168
-rw-r--r--app/assets/javascripts/ci/job_details/store/getters.js3
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutation_types.js10
-rw-r--r--app/assets/javascripts/ci/job_details/store/mutations.js54
-rw-r--r--app/assets/javascripts/ci/job_details/store/state.js13
-rw-r--r--app/assets/javascripts/ci/job_details/store/utils.js258
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue5
-rw-r--r--app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue2
-rw-r--r--app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql1
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/constants.js9
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql18
-rw-r--r--app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue109
-rw-r--r--app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js44
-rw-r--r--app/assets/javascripts/ci/pipeline_details/pipelines_index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js23
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue15
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue42
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue5
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue7
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue62
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/constants.js11
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/index.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/options.js4
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue16
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue3
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue4
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue2
-rw-r--r--app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue97
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue2
-rw-r--r--app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue8
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/actions.js30
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/getters.js63
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/index.js18
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js5
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/mutations.js27
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/store/state.js16
-rw-r--r--app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js (renamed from app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js)0
-rw-r--r--app/assets/javascripts/ci/reports/components/report_section.vue1
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_count.vue36
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_jobs_table.vue2
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list.vue5
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_list_header.vue2
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql1
-rw-r--r--app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql6
-rw-r--r--app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue2
-rw-r--r--app/assets/javascripts/ci_secure_files/components/secure_files_list.vue1
-rw-r--r--app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue1
-rw-r--r--app/assets/javascripts/clone_panel.js2
-rw-r--r--app/assets/javascripts/clusters_list/components/agents.vue6
-rw-r--r--app/assets/javascripts/clusters_list/components/clusters.vue7
-rw-r--r--app/assets/javascripts/commons/index.js3
-rw-r--r--app/assets/javascripts/commons/nav/user_merge_requests.js93
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_toolbar.vue151
-rw-r--r--app/assets/javascripts/content_editor/components/suggestions_dropdown.vue128
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/code_block.vue7
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue2
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue3
-rw-r--r--app/assets/javascripts/content_editor/extensions/copy_paste.js34
-rw-r--r--app/assets/javascripts/content_editor/extensions/emoji.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/reference.js21
-rw-r--r--app/assets/javascripts/content_editor/extensions/suggestions.js171
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_list.js7
-rw-r--r--app/assets/javascripts/content_editor/services/asset_resolver.js9
-rw-r--r--app/assets/javascripts/content_editor/services/create_content_editor.js9
-rw-r--r--app/assets/javascripts/content_editor/services/data_source_factory.js213
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_sourcemap.js6
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js50
-rw-r--r--app/assets/javascripts/contextual_sidebar.js112
-rw-r--r--app/assets/javascripts/contributors/components/contributor_area_chart.vue68
-rw-r--r--app/assets/javascripts/contributors/components/contributors.vue12
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/client.js47
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/mutations/confirm_action.mutation.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/mutations/disable_key.mutation.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/mutations/enable_key.mutation.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/mutations/update_current_page.mutation.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/mutations/update_current_scope.mutation.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/queries/confirm_remove_key.query.graphql5
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/queries/current_page.query.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/queries/current_scope.query.graphql3
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/queries/deploy_keys.query.graphql26
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/resolvers.js106
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/typedefs.graphql45
-rw-r--r--app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue2
-rw-r--r--app/assets/javascripts/deprecated_notes.js2
-rw-r--r--app/assets/javascripts/diffs/components/app.vue32
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue20
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue13
-rw-r--r--app/assets/javascripts/diffs/components/merge_conflict_warning.vue62
-rw-r--r--app/assets/javascripts/diffs/components/shared/findings_drawer.vue103
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/diffs/i18n.js3
-rw-r--r--app/assets/javascripts/diffs/index.js4
-rw-r--r--app/assets/javascripts/diffs/store/actions.js31
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js5
-rw-r--r--app/assets/javascripts/drawio/drawio_editor.js4
-rw-r--r--app/assets/javascripts/dropzone_input.js2
-rw-r--r--app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js84
-rw-r--r--app/assets/javascripts/editor/schema/ci.json60
-rw-r--r--app/assets/javascripts/emoji/components/category.vue2
-rw-r--r--app/assets/javascripts/emoji/components/emoji_group.vue37
-rw-r--r--app/assets/javascripts/emoji/components/picker.vue25
-rw-r--r--app/assets/javascripts/emoji/index.js33
-rw-r--r--app/assets/javascripts/emoji/queries/custom_emoji.query.graphql2
-rw-r--r--app/assets/javascripts/ensure_data.js2
-rw-r--r--app/assets/javascripts/environments/components/environment_form.vue87
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue4
-rw-r--r--app/assets/javascripts/environments/components/environment_namespace_selector.vue136
-rw-r--r--app/assets/javascripts/environments/components/environments_app.vue5
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_overview.vue15
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_pods.vue53
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue2
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_summary.vue5
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue7
-rw-r--r--app/assets/javascripts/environments/constants.js7
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_app.vue256
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js61
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue3
-rw-r--r--app/assets/javascripts/environments/graphql/client.js2
-rw-r--r--app/assets/javascripts/environments/graphql/queries/folder.query.graphql11
-rw-r--r--app/assets/javascripts/environments/graphql/queries/page_info.query.graphql8
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/base.js34
-rw-r--r--app/assets/javascripts/environments/graphql/resolvers/kubernetes.js123
-rw-r--r--app/assets/javascripts/environments/graphql/typedefs.graphql16
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js70
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details.vue10
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue4
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags.vue3
-rw-r--r--app/assets/javascripts/feature_flags/components/feature_flags_table.vue2
-rw-r--r--app/assets/javascripts/feature_highlight/constants.js1
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_helper.js19
-rw-r--r--app/assets/javascripts/feature_highlight/feature_highlight_popover.vue95
-rw-r--r--app/assets/javascripts/feature_highlight/index.js28
-rw-r--r--app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js73
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js4
-rw-r--r--app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js136
-rw-r--r--app/assets/javascripts/fly_out_nav.js205
-rw-r--r--app/assets/javascripts/forks/components/forks_button.vue2
-rw-r--r--app/assets/javascripts/frequent_items/components/app.vue183
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list.vue90
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue132
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_mixin.js23
-rw-r--r--app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue63
-rw-r--r--app/assets/javascripts/frequent_items/constants.js54
-rw-r--r--app/assets/javascripts/frequent_items/event_hub.js3
-rw-r--r--app/assets/javascripts/frequent_items/store/actions.js112
-rw-r--r--app/assets/javascripts/frequent_items/store/getters.js1
-rw-r--r--app/assets/javascripts/frequent_items/store/index.js29
-rw-r--r--app/assets/javascripts/frequent_items/store/mutation_types.js12
-rw-r--r--app/assets/javascripts/frequent_items/store/mutations.js88
-rw-r--r--app/assets/javascripts/frequent_items/store/state.js11
-rw-r--r--app/assets/javascripts/frequent_items/utils.js67
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js46
-rw-r--r--app/assets/javascripts/graphql_shared/client/page_info.query.graphql8
-rw-r--r--app/assets/javascripts/graphql_shared/client/page_info.typedefs.graphql10
-rw-r--r--app/assets/javascripts/graphql_shared/constants.js5
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js2
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json10
-rw-r--r--app/assets/javascripts/group_settings/constants.js2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue4
-rw-r--r--app/assets/javascripts/groups/components/groups.vue2
-rw-r--r--app/assets/javascripts/groups/components/item_stats.vue2
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue2
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/service/archived_projects_service.js2
-rw-r--r--app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue154
-rw-r--r--app/assets/javascripts/groups_projects/components/transfer_locations.vue12
-rw-r--r--app/assets/javascripts/groups_projects/init_more_actions_dropdown.js36
-rw-r--r--app/assets/javascripts/header.js145
-rw-r--r--app/assets/javascripts/header_search/components/app.vue306
-rw-r--r--app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue168
-rw-r--r--app/assets/javascripts/header_search/components/header_search_default_items.vue59
-rw-r--r--app/assets/javascripts/header_search/components/header_search_scoped_items.vue88
-rw-r--r--app/assets/javascripts/header_search/constants.js35
-rw-r--r--app/assets/javascripts/header_search/index.js47
-rw-r--r--app/assets/javascripts/header_search/init.js39
-rw-r--r--app/assets/javascripts/header_search/store/actions.js45
-rw-r--r--app/assets/javascripts/header_search/store/getters.js220
-rw-r--r--app/assets/javascripts/header_search/store/index.js26
-rw-r--r--app/assets/javascripts/header_search/store/mutation_types.js6
-rw-r--r--app/assets/javascripts/header_search/store/mutations.js30
-rw-r--r--app/assets/javascripts/header_search/store/state.js19
-rw-r--r--app/assets/javascripts/ide/components/activity_bar.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/actions.vue1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/form.vue2
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue9
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue6
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/message_field.vue1
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue1
-rw-r--r--app/assets/javascripts/ide/components/error_message.vue1
-rw-r--r--app/assets/javascripts/ide/components/file_templates/bar.vue4
-rw-r--r--app/assets/javascripts/ide/components/ide.vue1
-rw-r--r--app/assets/javascripts/ide/components/ide_project_header.vue6
-rw-r--r--app/assets/javascripts/ide/components/ide_sidebar_nav.vue1
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue3
-rw-r--r--app/assets/javascripts/ide/components/ide_tree.vue2
-rw-r--r--app/assets/javascripts/ide/components/ide_tree_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/jobs/detail/description.vue2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue3
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue7
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue1
-rw-r--r--app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue6
-rw-r--r--app/assets/javascripts/ide/components/pipelines/list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/ide/components/shared/commit_message_field.vue1
-rw-r--r--app/assets/javascripts/ide/components/terminal/empty_state.vue8
-rw-r--r--app/assets/javascripts/ide/components/terminal/terminal.vue3
-rw-r--r--app/assets/javascripts/ide/init_gitlab_web_ide.js25
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js12
-rw-r--r--app/assets/javascripts/ide/lib/gitlab_web_ide/index.js2
-rw-r--r--app/assets/javascripts/ide/mount_oauth_callback.js12
-rw-r--r--app/assets/javascripts/import/constants.js3
-rw-r--r--app/assets/javascripts/import/details/components/bulk_import_details_app.vue23
-rw-r--r--app/assets/javascripts/import/details/components/import_details_table.vue3
-rw-r--r--app/assets/javascripts/import_entities/constants.js4
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue31
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue54
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js10
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql1
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql5
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql3
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue5
-rw-r--r--app/assets/javascripts/import_entities/import_projects/index.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/actions.js16
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/index.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/mutations.js4
-rw-r--r--app/assets/javascripts/import_entities/import_projects/store/state.js2
-rw-r--r--app/assets/javascripts/integrations/edit/components/dynamic_field.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form.vue2
-rw-r--r--app/assets/javascripts/integrations/edit/components/integration_form_actions.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue4
-rw-r--r--app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue6
-rw-r--r--app/assets/javascripts/integrations/edit/components/sections/configuration.vue1
-rw-r--r--app/assets/javascripts/integrations/index/components/integrations_table.vue6
-rw-r--r--app/assets/javascripts/invite_members/components/invite_group_trigger.vue7
-rw-r--r--app/assets/javascripts/invite_members/components/invite_groups_modal.vue6
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue28
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue9
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue62
-rw-r--r--app/assets/javascripts/invite_members/components/members_token_select.vue82
-rw-r--r--app/assets/javascripts/invite_members/constants.js3
-rw-r--r--app/assets/javascripts/invite_members/init_invite_members_modal.js1
-rw-r--r--app/assets/javascripts/issuable/components/locked_badge.vue2
-rw-r--r--app/assets/javascripts/issuable/components/related_issuable_item.vue2
-rw-r--r--app/assets/javascripts/issuable/issuable_label_selector.js3
-rw-r--r--app/assets/javascripts/issuable/popover/components/mr_popover.vue2
-rw-r--r--app/assets/javascripts/issues/constants.js1
-rw-r--r--app/assets/javascripts/issues/dashboard/index.js2
-rw-r--r--app/assets/javascripts/issues/index.js5
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue10
-rw-r--r--app/assets/javascripts/issues/list/constants.js6
-rw-r--r--app/assets/javascripts/issues/list/index.js2
-rw-r--r--app/assets/javascripts/issues/list/utils.js36
-rw-r--r--app/assets/javascripts/issues/show/components/app.vue9
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue18
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue2
-rw-r--r--app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue3
-rw-r--r--app/assets/javascripts/issues/show/components/title.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/components/feedback_banner.vue2
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/constants.js6
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/dot_com_alert.vue34
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue12
-rw-r--r--app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue3
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/page_title.vue28
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue81
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_details_item.vue19
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue78
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue27
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue85
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/constants.js49
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/client.js108
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js116
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql16
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql14
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql14
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql17
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql17
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js7
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js169
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js60
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js48
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/app.vue13
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/daemon_sets_page.vue80
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/deployments_page.vue84
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue94
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/replica_sets_page.vue80
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/stateful_sets_page.vue81
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/constants.js11
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/index.js15
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/routes.js61
-rw-r--r--app/assets/javascripts/language_switcher/components/app.vue5
-rw-r--r--app/assets/javascripts/layout_nav.js28
-rw-r--r--app/assets/javascripts/lib/graphql.js14
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js43
-rw-r--r--app/assets/javascripts/lib/utils/datetime/constants.js7
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime/date_format_utility.js46
-rw-r--r--app/assets/javascripts/lib/utils/datetime/locale_dateformat.js273
-rw-r--r--app/assets/javascripts/lib/utils/datetime/timeago_utility.js54
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js2
-rw-r--r--app/assets/javascripts/lib/utils/regexp.js9
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js4
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js6
-rw-r--r--app/assets/javascripts/lib/utils/vuex_module_mappers.js92
-rw-r--r--app/assets/javascripts/logo.js17
-rw-r--r--app/assets/javascripts/main.js23
-rw-r--r--app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue2
-rw-r--r--app/assets/javascripts/members/components/modals/remove_group_link_modal.vue4
-rw-r--r--app/assets/javascripts/members/components/modals/remove_member_modal.vue4
-rw-r--r--app/assets/javascripts/members/components/table/max_role.vue134
-rw-r--r--app/assets/javascripts/members/components/table/members_table.vue8
-rw-r--r--app/assets/javascripts/members/components/table/role_dropdown.vue120
-rw-r--r--app/assets/javascripts/members/index.js4
-rw-r--r--app/assets/javascripts/members/store/actions.js12
-rw-r--r--app/assets/javascripts/members/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/members/store/mutations.js9
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue8
-rw-r--r--app/assets/javascripts/merge_request_tabs.js9
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue206
-rw-r--r--app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js26
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue22
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue22
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue18
-rw-r--r--app/assets/javascripts/ml/model_registry/components/candidate_detail.vue213
-rw-r--r--app/assets/javascripts/ml/model_registry/components/candidate_detail_row.vue (renamed from app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue)0
-rw-r--r--app/assets/javascripts/ml/model_registry/components/candidate_list.vue139
-rw-r--r--app/assets/javascripts/ml/model_registry/components/candidate_list_row.vue49
-rw-r--r--app/assets/javascripts/ml/model_registry/components/empty_state.vue58
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_version_detail.vue61
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_version_list.vue137
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_version_row.vue49
-rw-r--r--app/assets/javascripts/ml/model_registry/constants.js7
-rw-r--r--app/assets/javascripts/ml/model_registry/graphql/queries/get_model_candidates.query.graphql28
-rw-r--r--app/assets/javascripts/ml/model_registry/graphql/queries/get_model_versions.query.graphql22
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js35
-rw-r--r--app/assets/javascripts/nav/components/new_nav_toggle.vue105
-rw-r--r--app/assets/javascripts/nav/components/responsive_app.vue95
-rw-r--r--app/assets/javascripts/nav/components/responsive_header.vue37
-rw-r--r--app/assets/javascripts/nav/components/responsive_home.vue63
-rw-r--r--app/assets/javascripts/nav/components/top_nav_app.vue61
-rw-r--r--app/assets/javascripts/nav/components/top_nav_container_view.vue81
-rw-r--r--app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue107
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_item.vue52
-rw-r--r--app/assets/javascripts/nav/components/top_nav_menu_sections.vue82
-rw-r--r--app/assets/javascripts/nav/components/top_nav_new_dropdown.vue73
-rw-r--r--app/assets/javascripts/nav/index.js31
-rw-r--r--app/assets/javascripts/nav/mount.js30
-rw-r--r--app/assets/javascripts/nav/stores/index.js5
-rw-r--r--app/assets/javascripts/nav/utils/index.js1
-rw-r--r--app/assets/javascripts/nav/utils/reset_menu_items_active.js14
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue8
-rw-r--r--app/assets/javascripts/notes/components/discussion_locked_widget.vue2
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_activity_header.vue6
-rw-r--r--app/assets/javascripts/notifications/constants.js1
-rw-r--r--app/assets/javascripts/observability/client.js63
-rw-r--r--app/assets/javascripts/organizations/constants.js2
-rw-r--r--app/assets/javascripts/organizations/index/components/app.vue43
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_list.vue42
-rw-r--r--app/assets/javascripts/organizations/index/components/organizations_view.vue13
-rw-r--r--app/assets/javascripts/organizations/index/graphql/organizations.query.graphql14
-rw-r--r--app/assets/javascripts/organizations/index/index.js3
-rw-r--r--app/assets/javascripts/organizations/mock_data.js54
-rw-r--r--app/assets/javascripts/organizations/profile/preferences/index.js4
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue26
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/app.vue4
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/change_url.vue139
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue34
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql10
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql9
-rw-r--r--app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql5
-rw-r--r--app/assets/javascripts/organizations/settings/general/index.js3
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue61
-rw-r--r--app/assets/javascripts/organizations/shared/components/organization_url_field.vue58
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js11
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/fragments/organization.fragment.graphql7
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql12
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/queries/organizations.query.graphql16
-rw-r--r--app/assets/javascripts/organizations/shared/graphql/resolvers.js24
-rw-r--r--app/assets/javascripts/organizations/users/components/app.vue52
-rw-r--r--app/assets/javascripts/organizations/users/components/users_view.vue48
-rw-r--r--app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql21
-rw-r--r--app/assets/javascripts/organizations/users/index.js5
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue8
-rw-r--r--app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue2
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue19
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql3
-rw-r--r--app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue3
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue5
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue15
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js2
-rw-r--r--app/assets/javascripts/packages_and_registries/shared/utils.js5
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue8
-rw-r--r--app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js3
-rw-r--r--app/assets/javascripts/pages/admin/deploy_keys/new/index.js3
-rw-r--r--app/assets/javascripts/pages/clusters/agents/dashboard/index.js3
-rw-r--r--app/assets/javascripts/pages/dashboard/issues/index.js6
-rw-r--r--app/assets/javascripts/pages/dashboard/merge_requests/index.js12
-rw-r--r--app/assets/javascripts/pages/groups/boards/index.js3
-rw-r--r--app/assets/javascripts/pages/groups/shared/group_details.js3
-rw-r--r--app/assets/javascripts/pages/groups/show/index.js4
-rw-r--r--app/assets/javascripts/pages/ide/index/index.js (renamed from app/assets/javascripts/pages/ide/index.js)0
-rw-r--r--app/assets/javascripts/pages/ide/oauth_redirect/index.js3
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue42
-rw-r--r--app/assets/javascripts/pages/import/history/components/import_history_app.vue6
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/activity/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/browse/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/artifacts/file/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/commits/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/find_file/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue5
-rw-r--r--app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue3
-rw-r--r--app/assets/javascripts/pages/projects/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/init_blob.js7
-rw-r--r--app/assets/javascripts/pages/projects/issues/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/index/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js3
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js3
-rw-r--r--app/assets/javascripts/pages/projects/ml/model_versions/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/network/show/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue39
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue107
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/settings_panel.vue35
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/constants.js2
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql5
-rw-r--r--app/assets/javascripts/pages/projects/shared/web_ide_link/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/show/index.js8
-rw-r--r--app/assets/javascripts/pages/projects/tree/show/index.js3
-rw-r--r--app/assets/javascripts/pages/shared/nav/sidebar_tracking.js44
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue5
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue1
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_export.vue40
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue15
-rw-r--r--app/assets/javascripts/pages/shared/wikis/components/wiki_more_dropdown.vue58
-rw-r--r--app/assets/javascripts/pages/shared/wikis/show.js11
-rw-r--r--app/assets/javascripts/pages/shared/wikis/wikis.js3
-rw-r--r--app/assets/javascripts/pages/user_settings/personal_access_tokens/index.js (renamed from app/assets/javascripts/pages/profiles/personal_access_tokens/index.js)0
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/performance_bar_app.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/request_selector.vue4
-rw-r--r--app/assets/javascripts/persistent_user_callout.js5
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/profile/account/components/delete_account_modal.vue4
-rw-r--r--app/assets/javascripts/profile/edit/components/profile_edit_app.vue21
-rw-r--r--app/assets/javascripts/profile/edit/components/user_avatar.vue2
-rw-r--r--app/assets/javascripts/profile/gl_crop.js36
-rw-r--r--app/assets/javascripts/profile/profile.js11
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue2
-rw-r--r--app/assets/javascripts/projects/components/shared/delete_modal.vue4
-rw-r--r--app/assets/javascripts/projects/details/upload_button.vue3
-rw-r--r--app/assets/javascripts/projects/new/components/app.vue2
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue40
-rw-r--r--app/assets/javascripts/projects/pipelines/charts/constants.js2
-rw-r--r--app/assets/javascripts/projects/settings/components/default_branch_selector.vue6
-rw-r--r--app/assets/javascripts/projects/settings/mount_default_branch_selector.js4
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue4
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue12
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue1
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue12
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue30
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js20
-rw-r--r--app/assets/javascripts/projects/settings_service_desk/index.js2
-rw-r--r--app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue4
-rw-r--r--app/assets/javascripts/ref/components/ref_selector.vue12
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue15
-rw-r--r--app/assets/javascripts/repository/commits_service.js5
-rw-r--r--app/assets/javascripts/repository/components/blob_content_viewer.vue14
-rw-r--r--app/assets/javascripts/repository/components/blob_controls.vue6
-rw-r--r--app/assets/javascripts/repository/components/blob_viewers/index.js8
-rw-r--r--app/assets/javascripts/repository/components/commit_info.vue45
-rw-r--r--app/assets/javascripts/repository/components/delete_blob_modal.vue2
-rw-r--r--app/assets/javascripts/repository/components/last_commit.vue4
-rw-r--r--app/assets/javascripts/repository/components/table/index.vue27
-rw-r--r--app/assets/javascripts/repository/components/tree_content.vue8
-rw-r--r--app/assets/javascripts/repository/mixins/highlight_mixin.js27
-rw-r--r--app/assets/javascripts/search/index.js53
-rw-r--r--app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue19
-rw-r--r--app/assets/javascripts/search/sidebar/components/app.vue64
-rw-r--r--app/assets/javascripts/search/sidebar/components/archived_filter/index.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/blobs_filters.vue16
-rw-r--r--app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue5
-rw-r--r--app/assets/javascripts/search/sidebar/components/filters_template.vue12
-rw-r--r--app/assets/javascripts/search/sidebar/components/group_filter.vue (renamed from app/assets/javascripts/search/topbar/components/group_filter.vue)63
-rw-r--r--app/assets/javascripts/search/sidebar/components/issues_filters.vue14
-rw-r--r--app/assets/javascripts/search/sidebar/components/label_filter/index.vue19
-rw-r--r--app/assets/javascripts/search/sidebar/components/language_filter/index.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue11
-rw-r--r--app/assets/javascripts/search/sidebar/components/project_filter.vue94
-rw-r--r--app/assets/javascripts/search/sidebar/components/radio_filter.vue6
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue85
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue3
-rw-r--r--app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue222
-rw-r--r--app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue61
-rw-r--r--app/assets/javascripts/search/sidebar/components/status_filter/index.vue2
-rw-r--r--app/assets/javascripts/search/sidebar/constants/index.js24
-rw-r--r--app/assets/javascripts/search/sidebar/index.js20
-rw-r--r--app/assets/javascripts/search/store/constants.js4
-rw-r--r--app/assets/javascripts/search/store/mutations.js4
-rw-r--r--app/assets/javascripts/search/store/state.js15
-rw-r--r--app/assets/javascripts/search/topbar/components/app.vue99
-rw-r--r--app/assets/javascripts/search/topbar/components/project_filter.vue70
-rw-r--r--app/assets/javascripts/search/topbar/components/search_type_indicator.vue120
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown.vue195
-rw-r--r--app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue78
-rw-r--r--app/assets/javascripts/search/topbar/constants.js28
-rw-r--r--app/assets/javascripts/search/topbar/index.js14
-rw-r--r--app/assets/javascripts/search/under_topbar/index.js1
-rw-r--r--app/assets/javascripts/security_configuration/components/app.vue30
-rw-r--r--app/assets/javascripts/security_configuration/components/constants.js332
-rw-r--r--app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue127
-rw-r--r--app/assets/javascripts/security_configuration/components/feature_card.vue7
-rw-r--r--app/assets/javascripts/security_configuration/components/training_provider_list.vue3
-rw-r--r--app/assets/javascripts/security_configuration/constants.js353
-rw-r--r--app/assets/javascripts/security_configuration/index.js4
-rw-r--r--app/assets/javascripts/security_configuration/utils.js2
-rw-r--r--app/assets/javascripts/set_status_modal/constants.js2
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_form.vue6
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue18
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue1
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue13
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue19
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql2
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue11
-rw-r--r--app/assets/javascripts/sidebar/components/participants/participants.vue4
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue8
-rw-r--r--app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue101
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown.vue22
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue104
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue2
-rw-r--r--app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql5
-rw-r--r--app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql5
-rw-r--r--app/assets/javascripts/snippets/components/snippet_blob_edit.vue1
-rw-r--r--app/assets/javascripts/snippets/components/snippet_title.vue40
-rw-r--r--app/assets/javascripts/sortable/constants.js1
-rw-r--r--app/assets/javascripts/super_sidebar/components/counter.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/extra_info.vue7
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue19
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue25
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item_skeleton.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue70
-rw-r--r--app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue19
-rw-r--r--app/assets/javascripts/super_sidebar/components/help_center.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/menu_section.vue17
-rw-r--r--app/assets/javascripts/super_sidebar/components/nav_item.vue11
-rw-r--r--app/assets/javascripts/super_sidebar/components/scroll_scrim.vue72
-rw-r--r--app/assets/javascripts/super_sidebar/components/sidebar_menu.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/super_sidebar.vue75
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_bar.vue2
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue75
-rw-r--r--app/assets/javascripts/super_sidebar/constants.js17
-rw-r--r--app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql9
-rw-r--r--app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql9
-rw-r--r--app/assets/javascripts/super_sidebar/super_sidebar_bundle.js11
-rw-r--r--app/assets/javascripts/super_sidebar/user_counts_fetch.js10
-rw-r--r--app/assets/javascripts/super_sidebar/utils.js49
-rw-r--r--app/assets/javascripts/task_list.js4
-rw-r--r--app/assets/javascripts/terms/components/app.vue4
-rw-r--r--app/assets/javascripts/terraform/components/states_table.vue2
-rw-r--r--app/assets/javascripts/token_access/components/inbound_token_access.vue2
-rw-r--r--app/assets/javascripts/token_access/components/outbound_token_access.vue2
-rw-r--r--app/assets/javascripts/tracking/internal_events.js27
-rw-r--r--app/assets/javascripts/tracking/tracking.js9
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js2
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue11
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue8
-rw-r--r--app/assets/javascripts/usage_quotas/storage/constants.js3
-rw-r--r--app/assets/javascripts/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql23
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/approvals/approvals.vue62
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js74
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue169
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue30
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_status_icon.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue1
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue60
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/work_in_progress.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js110
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue24
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.subscription.graphql14
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js16
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js12
-rw-r--r--app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/awards_list.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/changed_file_icon.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js31
-rw-r--r--app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue (renamed from app/assets/javascripts/vue_shared/components/ci_icon.vue)0
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue46
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/group_select.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue61
-rw-r--r--app/assets/javascripts/vue_shared/components/entity_select/project_select.vue25
-rw-r--r--app/assets/javascripts/vue_shared/components/file_icon.vue8
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js6
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue114
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/keep_alive_slots.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/constants.js24
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue51
-rw-r--r--app/assets/javascripts/vue_shared/components/list_selector/index.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/field.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue513
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header_divider.vue16
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js34
-rw-r--r--app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue48
-rw-r--r--app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/details_row.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/registry/list_item.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/constants.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue20
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js36
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/user_access_role_badge.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/vuex_module_provider.vue18
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue7
-rw-r--r--app/assets/javascripts/vue_shared/global_search/constants.js13
-rw-r--r--app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue2
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue3
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue5
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue9
-rw-r--r--app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue6
-rw-r--r--app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue55
-rw-r--r--app/assets/javascripts/vue_shared/mixins/timeago.js4
-rw-r--r--app/assets/javascripts/vue_shared/new_namespace/new_namespace_page.vue8
-rw-r--r--app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue3
-rw-r--r--app/assets/javascripts/webhooks/components/form_url_app.vue24
-rw-r--r--app/assets/javascripts/webpack_non_compiled_placeholder.js20
-rw-r--r--app/assets/javascripts/whats_new/index.js24
-rw-r--r--app/assets/javascripts/whats_new/utils/notification.js20
-rw-r--r--app/assets/javascripts/work_items/components/item_title.vue21
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_add_note.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue3
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue2
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue14
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue13
-rw-r--r--app/assets/javascripts/work_items/components/update_work_item.js23
-rw-r--r--app/assets/javascripts/work_items/components/work_item_actions.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue127
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue61
-rw-r--r--app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue95
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue121
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue210
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue72
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_actions_split_button.vue24
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue4
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue6
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue8
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue125
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone.vue9
-rw-r--r--app/assets/javascripts/work_items/components/work_item_notes.vue12
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent_inline.vue (renamed from app/assets/javascripts/work_items/components/work_item_parent.vue)1
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent_with_edit.vue295
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue27
-rw-r--r--app/assets/javascripts/work_items/components/work_item_sticky_header.vue136
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title.vue38
-rw-r--r--app/assets/javascripts/work_items/constants.js8
-rw-r--r--app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql13
-rw-r--r--app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql32
-rw-r--r--app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql13
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_allowed_children.query.graphql20
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql33
-rw-r--r--app/assets/javascripts/work_items/notes/award_utils.js5
-rw-r--r--app/assets/javascripts/work_items/utils.js13
-rw-r--r--app/assets/stylesheets/application_utilities.scss2
-rw-r--r--app/assets/stylesheets/components/content_editor.scss2
-rw-r--r--app/assets/stylesheets/components/detail_page.scss21
-rw-r--r--app/assets/stylesheets/framework.scss2
-rw-r--r--app/assets/stylesheets/framework/animations.scss7
-rw-r--r--app/assets/stylesheets/framework/awards.scss9
-rw-r--r--app/assets/stylesheets/framework/blocks.scss8
-rw-r--r--app/assets/stylesheets/framework/brand_logo.scss13
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss20
-rw-r--r--app/assets/stylesheets/framework/contextual_sidebar.scss32
-rw-r--r--app/assets/stylesheets/framework/diffs.scss10
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss97
-rw-r--r--app/assets/stylesheets/framework/emojis.scss27
-rw-r--r--app/assets/stylesheets/framework/feature_highlight.scss53
-rw-r--r--app/assets/stylesheets/framework/files.scss4
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/header.scss488
-rw-r--r--app/assets/stylesheets/framework/icons.scss2
-rw-r--r--app/assets/stylesheets/framework/job_log.scss47
-rw-r--r--app/assets/stylesheets/framework/layout.scss7
-rw-r--r--app/assets/stylesheets/framework/lists.scss6
-rw-r--r--app/assets/stylesheets/framework/modal.scss2
-rw-r--r--app/assets/stylesheets/framework/responsive_tables.scss4
-rw-r--r--app/assets/stylesheets/framework/secondary_navigation_elements.scss1
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss23
-rw-r--r--app/assets/stylesheets/framework/snippets.scss9
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss219
-rw-r--r--app/assets/stylesheets/framework/typography.scss6
-rw-r--r--app/assets/stylesheets/framework/variables.scss398
-rw-r--r--app/assets/stylesheets/highlight/common.scss2
-rw-r--r--app/assets/stylesheets/highlight/themes/none.scss20
-rw-r--r--app/assets/stylesheets/highlight/themes/solarized-light.scss2
-rw-r--r--app/assets/stylesheets/highlight/white_base.scss2
-rw-r--r--app/assets/stylesheets/mailers/highlighted_diff_email.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss14
-rw-r--r--app/assets/stylesheets/page_bundles/boards.scss13
-rw-r--r--app/assets/stylesheets/page_bundles/build.scss66
-rw-r--r--app/assets/stylesheets/page_bundles/group.scss31
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss63
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/merge_request.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss26
-rw-r--r--app/assets/stylesheets/page_bundles/milestone.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/pipelines.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss91
-rw-r--r--app/assets/stylesheets/page_bundles/projects.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/search.scss20
-rw-r--r--app/assets/stylesheets/page_bundles/terms.scss10
-rw-r--r--app/assets/stylesheets/page_bundles/tree.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/users.scss43
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss27
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss127
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/events.scss32
-rw-r--r--app/assets/stylesheets/pages/groups.scss6
-rw-r--r--app/assets/stylesheets/pages/issues.scss26
-rw-r--r--app/assets/stylesheets/pages/note_form.scss3
-rw-r--r--app/assets/stylesheets/pages/notes.scss40
-rw-r--r--app/assets/stylesheets/print.scss4
-rw-r--r--app/assets/stylesheets/snippets.scss8
-rw-r--r--app/assets/stylesheets/themes/_dark.scss113
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss160
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss8
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss345
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss104
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss10
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss10
-rw-r--r--app/assets/stylesheets/tmp_utilities.scss32
-rw-r--r--app/assets/stylesheets/utilities.scss2
-rw-r--r--app/assets/stylesheets/vendors/atwho.scss47
914 files changed, 14175 insertions, 12729 deletions
diff --git a/app/assets/images/logos/shimo.svg b/app/assets/images/logos/shimo.svg
deleted file mode 100644
index 65bd1cc7167..00000000000
--- a/app/assets/images/logos/shimo.svg
+++ /dev/null
@@ -1 +0,0 @@
-<svg fill="none" height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m7.99985 15.9997c4.41815 0 7.99985-3.5817 7.99985-7.99985 0-4.4182-3.5817-7.99985-7.99985-7.99985-4.4182 0-7.99985 3.58165-7.99985 7.99985 0 4.41815 3.58165 7.99985 7.99985 7.99985z" fill="#3f464a"/><g fill="#fff"><path d="m10.1501 3.13098c.0077.00805.017.01641.025.02601l.2988.34857c.0099.01099.0189.0228.0269.03529.0048.00966.0075.0202.0081.03096.0001.00853-.0027.01685-.0079.02359-.0053.00675-.0126.01153-.0209.01355-.0147.00206-.0295.0032-.0443.00341l-.70117.00991c-.24229.0035-.48457.0067-.72686.00959-.22104.00248-.44207.00372-.6631.00681-.20401.00248-.40801.0065-.61171.00991-.01269 0-.02538 0-.03807.00248-.00527 0-.01115.00959-.00867.01393.00455.00951.00972.01871.01548.02755.0552.07835.12273.14725.19998.204.0356.02662.07368.04984.11051.07523.01242.00804.02403.01727.03468.02755.00948.01029.01475.02377.01475.03777 0 .01399-.00527.02747-.01475.03776-.00419.00471-.00886.00896-.01394.0127-.01157.00943-.02097.02126-.02755.03467-.06748.11309-.13507.2266-.20276.34052-.10524.17487-.22146.34289-.34796.50305-.01089.01016-.01917.0228-.02415.03684-.00017.00237-.00017.00475 0 .00712.00497.00357.01026.00668.01579.00928.06192.0257.126.05077.18884.07678.01379.0055.02849.00834.04334.00836h.05758 1.5045.05108c.00857-.00025.01691-.00276.0242-.00727.00728-.00451.01325-.01087.01728-.01843.00743-.01284.01395-.02618.0195-.03993.02167-.0483.04303-.09721.0647-.1455.00487-.01181.01089-.02311.01796-.03374.00581-.00902.01482-.01551.02521-.01815.0104-.00264.02141-.00125.03082.00391l.02786.01548.33527.19688c.01106.00633.02144.0138.03095.02229.00321.00283.00551.00654.00661.01067s.00096.0085-.00042.01255c-.0034.0099-.00804.0195-.01238.0291-.0291.06191-.05696.12382-.08823.18388-.02023.03975-.03033.08388-.02941.12847v1.02003c0 .09999.00001.19998-.00247.29966-.00185.09338-.01054.1865-.02601.27861-.00686.04206-.01721.08348-.03095.12382-.02828.08507-.07696.16191-.14178.22382-.05123.04802-.10725.09065-.16717.12723-.01271.00783-.0275.01161-.04241.01084-.00639-.00011-.01258-.0022-.01773-.00596-.00516-.00377-.00902-.00904-.01106-.01509-.00248-.00805-.00403-.01641-.00588-.02477-.00918-.0392-.02055-.07785-.03406-.11578-.01986-.05201-.05688-.0957-.10494-.12382-.00743-.00402-.0157-.00625-.02415-.0065-.01485-.00078-.02972-.00078-.04457 0-.15912.00774-.31855.00743-.47798.00866l-.75875.00589c-.0743 0-.14859.00216-.22289.00402-.01637-.00159-.03289.00009-.0486.00495-.00376.00242-.00682.00578-.00888.00974-.00207.00397-.00306.0084-.00289.01286-.00557.03096-.0099.06192-.0164.09287-.01604.07268-.04132.14301-.07523.20927-.05921.11305-.13198.21846-.2167.3139-.0047.00582-.01055.01061-.01719.01407-.00663.00346-.01391.00551-.02137.00603-.00747.00053-.01496-.00049-.02201-.00299s-.01352-.00643-.01899-.01154c-.00892-.00618-.01504-.01564-.01702-.02631-.00186-.01023-.0031-.02057-.00372-.03096 0-.02321 0-.04674 0-.06996v-2.37841c0-.01702 0-.03436 0-.05107 0-.01672.00589-.03096-.0099-.04087-.01393.00279-.01951.01517-.02724.02477-.18029.22601-.37688.43853-.58818.63585-.29527.27883-.61431.53136-.95348.75473-.01764.01176-.0356.02321-.05386.03405-.00952.00504-.02063.00615-.03096.00309-.00515-.00121-.0099-.00373-.01381-.00731-.0039-.00357-.00682-.00809-.00848-.01312-.00253-.00507-.00335-.01082-.00234-.0164.001-.00558.00378-.01068.00792-.01455.01393-.01301.02848-.02539.04334-.03746.11722-.09494.23124-.19369.34207-.29626.3874-.36168.72258-.77551.99588-1.2296.16487-.27522.31301-.56013.44361-.85317.08668-.19162.16717-.38572.2458-.58075.02446-.06191.01207-.06191-.04055-.06191l-.34424.00898c-.20401.00557-.40771.01052-.61171.01671-.13157.00434-.26313.01084-.39501.01672-.01482.00051-.02963-.00115-.04396-.00495-.01457-.00344-.02781-.01109-.03808-.02198-.00588-.00619-.01145-.01239-.01702-.01889-.04768-.05407-.09525-.10845-.14271-.16314-.0068-.00812-.01271-.01694-.01765-.02631 0-.00217.00186-.00991.00372-.01022.0126-.00195.02532-.00299.03807-.00309h.31236.88629l.79033-.00248c.25715 0 .5143-.00093.77145-.00279l1.01383-.00247c.01268-.00078.0254-.00078.03808 0 .01271.00099.02548-.00098.03729-.00576.01182-.00478.02237-.01224.03081-.02179.08947-.08421.18017-.16779.27087-.25168.0105-.00929.0189-.0226.0378-.02446zm-2.54778 3.05791v.8476c0 .02105 0 .04241.00217.06191.00051.00421.00239.00813.00536.01115s.00685.00498.01105.00557c.01888 0 .03808.00185.05727.00185l1.44011-.00433h.03096c.01172.00058.02344-.00133.03438-.0056.01093-.00427.02085-.0108.02908-.01917.0125-.01146.02409-.02388.03467-.03714.03005-.03864.05291-.08237.06749-.12909.01811-.05894.02678-.12038.02569-.18203 0-.46745-.00062-.93489-.00186-1.40234 0-.01703 0-.03405 0-.05077-.0006-.00849-.00247-.01684-.00557-.02476-.00288-.0103-.0089-.01945-.01722-.02616-.00833-.00672-.01854-.01067-.02921-.0113-.01053 0-.02136 0-.03096 0-.10835 0-.2167-.00341-.32505-.00341-.32731 0-.65442 0-.98133 0-.08699 0-.17429.00403-.26127.00557-.01487.00085-.02941.00469-.04276.01129-.01335.00659-.02524.01581-.03495.0271-.01269.013-.0065.03096-.00712.04891-.00062.01796 0 .03096 0 .04458z"/><path d="m6.35047 8.7363c.01814-.00207.03651.00027.05356.00681.09287.02662.18574.05293.28046.08017.0365.01068.07435.016.11238.01579l1.99857.00186h.71695c.08978 0 .06904.0096.11269-.06377.02352-.04025.04674-.08049.07027-.12042.00261-.0047.00697-.00817.01212-.00968.00516-.00151.0107-.00092.01543.00163l.02786.01455c.0904.05077.18079.10154.27364.14766.0369.01827.0585.03096.0273.08389-.0217.03622-.0412.07399-.06195.11021-.01119.01807-.01659.03914-.01548.06037v.03095.686.03808c-.00102.00948-.00015.01906.00256.0282s.00721.01765.01323.02504c.00273.00326.00511.00679.00712.01053.00362.00653.00542.01385.00552.02129 0 .00745-.0018.01479-.00528.02136-.0035.00657-.00858.01217-.01479.01629-.0062.00412-.01333.00663-.02074.00731-.01681.00092-.03366.00092-.05046 0-.22413-.00155-.44857-.00403-.67269-.00496-.26221 0-.52441 0-.78661 0-.01889 0-.03808 0-.05696.00279-.00413.00087-.0079.00296-.01082.00599-.00293.00304-.00488.00688-.00559.01104v.01888.27864c-.00041.0103.00042.0207.00247.0309.00102.0029.00266.0055.00479.0077.00214.0022.00473.0039.0076.005.00802.0025.01639.0035.02476.0031h.91385.07058c.01269-.0118.02538-.0226.03715-.0343l.14704-.1496c.00743-.0074.01517-.0145.02291-.0216.00324-.0027.00729-.0041.01145-.0041.00417 0 .00821.0014.01146.0041.00495.0039.00971.0082.01424.0127l.26003.2513c.00873.0091.0168.0189.02415.0291.00302.0056.00381.0121.0022.0183-.00161.0061-.00549.0114-.01087.0148-.00971.005-.02017.0082-.03096.0096-.01888.0016-.03807 0-.05696 0l-1.36395.0062c-.10401 0-.09287-.0096-.09287.091v.2477c.00004.0104.00097.0207.00279.031.00141.0039.0043.0071.00804.0089.00382.0021.00805.0033.01239.0034h.04426l.69158.0034c.18822 0 .37643.0028.56465.0031.08699 0 .06625.0081.12878-.0489.05325-.0483.10587-.0972.15881-.1458.00864-.0105.02037-.018.03347-.0216.0063.0043.0124.0089.0182.0139.091.0913.1824.1826.2731.2743.0072.0075.0134.0159.0185.025.0016.0034.0025.007.0025.0107.0001.0037-.0007.0074-.0022.0108-.0015.0033-.0037.0063-.0065.0088-.0027.0024-.006.0043-.0095.0053-.0124.0025-.0249.0038-.0375.0041-.0551 0-.1099 0-.165 0-.36591-.0016-.73182-.0041-1.09742-.005-.82552-.0019-1.65041-.0032-2.47469-.004h-.35507c-.01084 0-.02136 0-.03096 0-.01145.0007-.02292-.0011-.03361-.0053s-.02034-.0106-.0283-.0189l-.10773-.1077c-.00121-.0016-.00194-.0035-.00212-.0055s.0002-.004.0011-.0058.00228-.0034.00399-.0044c.00171-.0011.00369-.0016.0057-.0016h.03807l.99619.0059.88815.0065h.05696c.00411-.0009.00789-.0029.01091-.0058s.00514-.0066.00612-.0107c0-.0065.00186-.0126.00186-.0188 0-.1016 0-.2031 0-.3047.00035-.0084-.00091-.0168-.00372-.0247-.00117-.0028-.00298-.0053-.00529-.0073s-.00506-.0034-.00802-.0042c-.02064-.0018-.04128-.0028-.06191-.0028-.20308 0-.40616 0-.60892 0-.22227 0-.44434-.0006-.66619-.0018-.00867 0-.01703 0-.02539 0-.01271.0007-.02541-.0016-.03715-.0065s-.02222-.0124-.03064-.022c-.03251-.0331-.06563-.0659-.09875-.0987-.00253-.0018-.00437-.0045-.00521-.0074-.00085-.003-.00065-.0062.00056-.0091.0031-.0068.00991-.0065.0161-.0065.13745 0 .2749 0 .41234.0016l1.04046.0093h.06346c.00406-.0008.00782-.0027.01088-.0055.00305-.0028.00529-.0063.00646-.0103.00128-.0083.00211-.0167.00247-.0251v-.27859c-.00036-.0084-.00119-.01677-.00247-.02508-.00083-.00414-.00291-.00793-.00595-.01087-.00304-.00293-.00691-.00486-.01108-.00554-.01269 0-.02538-.00185-.03808-.00185-.09503 0-.19038 0-.28542.00185-.29409.0031-.58817 0-.88195 0-.01672 0-.03375 0-.05077 0-.00839.00074-.01641.00373-.02322.00867-.01149.00973-.02244.02006-.03282.03096-.06408.05885-.13032.11455-.19936.16715-.01486.0118-.03095.0226-.04612.0334-.00516.0037-.01065.0068-.01641.0093-.00493.002-.01022.0027-.01549.0023s-.01038-.002-.01496-.0046c-.00457-.0026-.0085-.0063-.01148-.0106-.00298-.0044-.00494-.0094-.00574-.0146-.00155-.0254-.00186-.0508-.00186-.0762 0-.24515 0-.49053 0-.73612 0-.18388 0-.36787 0-.55196-.00287-.02176-.00329-.04377-.00124-.06562zm1.01724 1.09277h.53865.05696c.01888 0 .02693-.00805.02786-.02663.00093-.01857 0-.02941 0-.04426 0-.24085 0-.48169 0-.72253 0-.01703 0-.03375 0-.05077.00002-.00528-.00194-.01037-.0055-.01427-.00355-.0039-.00844-.00632-.01369-.00678-.0192 0-.03808 0-.05696 0h-1.10454-.04427c-.00315-.00036-.00635-.00003-.00937.00095-.00301.00098-.00579.00259-.00813.00473-.00235.00214-.00421.00476-.00546.00767-.00125.00292-.00187.00607-.0018.00925v.05696.70364.04427c.00019.00632.00071.01262.00154.01888.00054.00416.00234.00806.00516.01116.00283.0031.00654.00526.01063.00618.01888 0 .03808.00217.05696.00247zm1.6144-.86679h-.53401c-.07615 0-.07089-.00402-.07089.07337v.71571.04427c0 .02384.00774.03096.02941.03096h.25353c.00527.00006.01048-.00111.01521-.00342.00473-.00232.00885-.0057.01204-.00989l.00774-.00991c.08256-.11785.15112-.2449.20431-.3786.03491-.09285.06447-.18763.08854-.28387.00588-.0226.01083-.0452.01764-.06749.00366-.00982.00866-.0191.01486-.02755.00162-.00133.00353-.00225.00558-.00268s.00417-.00036.00618.00021c.00774.00295.01496.00713.02137.01238.05386.04736.10773.09534.16128.14302.03096.02662.06037.05293.08977.08018.01063.01026.02008.02168.02818.03405.00342.00436.00543.00966.00576.01519s-.00103.01103-.00391.01576c-.03387.06352-.07661.12189-.12692.17336-.03634.03538-.07502.06826-.11578.09844-.09345.06928-.19451.12765-.30121.17398-.01733.00774-.03436.0161-.05139.02446-.00136.00132-.00223.00307-.00247.00495 0 .00495.00186.00743.00743.00774s.01671 0 .02507 0h.75473c.01052 0 .02105 0 .03095-.00186.00413-.00058.00797-.00245.01097-.00534s.00501-.00666.00575-.01076c0-.02105.00217-.0421.00217-.06191 0-.23465 0-.4693 0-.70395 0-.01486 0-.02941 0-.04427 0-.0065 0-.01269-.00155-.01889-.00041-.00417-.00218-.00809-.00503-.01117-.00285-.00307-.00663-.00513-.01076-.00585-.01888 0-.03807-.00186-.05696-.00186z"/><path d="m9.7848 12.8222c.01332-.0133.02849-.0278.04304-.043.07181-.0749.14333-.1498.21666-.2241.0115-.0118.0208-.0273.0378-.031.0102.003.0192.0093.0254.018.1187.1238.237.2476.3551.3715.0056.0062.0108.0128.0154.0198.0021.0036.0034.0076.0036.0118.0003.0042-.0004.0083-.0019.0122-.0016.0038-.0041.0073-.0072.01-.0031.0028-.0068.0048-.0109.0059-.0062.0014-.0125.0023-.0189.0028h-.0507-.53403-2.46788c-.32133 0-.64277.0005-.9643.0015-.12909 0-.25787.0019-.38696.0028-.00836 0-.01703 0-.02539 0-.01167.0002-.02325-.0021-.03393-.0068-.01069-.0047-.02023-.0117-.02798-.0204-.03529-.0393-.0712-.078-.10649-.1173-.0009-.0018-.00137-.0037-.00137-.0056s.00047-.0039.00137-.0056c.00095-.0018.00231-.0033.00398-.0045s.00359-.002.00561-.0023c.01025-.0015.0206-.0022.03096-.0022h.29192c.12383 0 .24518.0016.36808.0019h.73584.64081c.01888 0 .03777 0 .05665-.0022.00412-.0007.0079-.0028.01084-.0057.00293-.003.00488-.0069.00557-.011 0-.0087.00155-.017.00186-.0254 0-.1238 0-.2486 0-.3742-.00017-.0105-.00142-.0208-.00372-.031 0-.0034-.00526-.0065-.00867-.0084-.00357-.0021-.0076-.0034-.01176-.0037-.01486 0-.03096 0-.04458 0-.25157.0023-.50315.0047-.75473.0072-.24332.0024-.48633.0058-.72965.0089h-.09504c-.00851.0005-.01703-.0008-.02503-.0038-.00799-.003-.01527-.0076-.0214-.0135-.04056-.0427-.07987-.0873-.11919-.1313-.00035-.0018-.00025-.0038.00029-.0056s.0015-.0035.00281-.0049c.00357-.0023.00782-.0034.01207-.0031h.05696 1.66208c.08544 0 .07987.0065.07987-.0777v-.5736c-.00217-.0216-.00017-.0433.00588-.0641.00849 0 .01698.0006.02538.0019l.36313.0882c.00433 0 .00804.0028.01207.0037.03436.0102.03808.0207.01641.048l-.03529.0446c-.00161.0026-.00306.0052-.00434.008v.0483.4059.0445c0 .0205.01238.0251.0291.026.01672.001.02539 0 .03808 0l1.10361.0019c.06625 0 .05263.0049.09968-.0406.05944-.0572.11826-.1151.17738-.1727.00898-.0084.01889-.0161.03096-.0251.01084.0093.02074.017.02941.026.08565.0877.17117.1757.25667.2638.0045.0044.0089.009.013.0139.0056.0048.0097.0113.0114.0185.0018.0073.0013.0149-.0015.0218-.003.0055-.0075.0102-.0129.0134-.0055.0031-.0118.0047-.0181.0045-.017 0-.0337 0-.0508 0l-.34884.0025-1.28781.0117c-.01671 0-.03374.0019-.05046.0028-.00407.0011-.00776.0032-.01062.0063s-.00476.0069-.00547.0111c-.00108.0083-.0017.0167-.00186.025v.3808.0189c.00025.0053.00238.0104.00601.0143.00362.0039.00851.0064.0138.0071.01239 0 .02508 0 .03777.0018h.13342l1.1355.0028c.02548.0022.05113.0016.07646-.0019z"/><path d="m10.1624 11.4382c0 .0068 0 .0173-.0034.0275-.0169.096-.0571.1864-.117.2632-.0199.0247-.04399.0456-.0712.0619-.01287.0073-.02646.0132-.04055.0176-.0171.0058-.03544.0069-.05309.0031-.01765-.0037-.03396-.0122-.04721-.0244-.02256-.0192-.04081-.0429-.05356-.0697-.00959-.0189-.01764-.0384-.02631-.0576-.03797-.087-.08086-.1719-.12847-.2541-.03249-.0568-.06912-.1111-.10958-.1625-.03991-.052-.08703-.0979-.13993-.1366-.00681-.0049-.01331-.0105-.01981-.0158-.0065-.0052-.00836-.0185-.0034-.0232.0041-.0044.00954-.0073.01547-.0083.01256-.0007.02514.0003.03746.0028.20435.0435.40349.1087.59405.1944.02293.0108.04583.0216.06813.0337.0148.008.0289.0173.0421.0279.0178.0144.0322.0328.0419.0536.0098.0208.0147.0435.0144.0665z"/><path d="m6.52884 11.8564c-.03474 0-.0692-.0061-.10185-.0179-.05603-.0186-.11082-.0406-.16593-.0619-.02512-.0109-.0466-.0287-.06191-.0514-.01313-.0178-.02058-.0392-.02135-.0613s.00516-.0439.01701-.0625c.01012-.0162.02313-.0304.03839-.0418.01362-.0102.02755-.0195.04148-.0294.06718-.0474.13374-.0957.1972-.1483.04368-.0362.08504-.075.12382-.1164.06313-.0654.11482-.141.15293-.2235.00464-.0096.00898-.0192.01393-.0285.00199-.0037.005-.0068.00867-.0089s.00786-.0031.01207-.0029c.00475.0022.0088.0056.0117.0099s.00453.0094.00471.0146v.0189c-.01207.1154-.02291.2309-.03715.3461-.01021.0817-.02414.1628-.03777.2439-.00493.027-.01207.0535-.02136.0793-.00757.0215-.01721.0422-.02879.0619-.01414.0258-.03538.0471-.06121.0613s-.05518.0208-.08459.0188z"/><path d="m8.77719 11.8183c-.01359 0-.02704-.0027-.03954-.008-.0125-.0054-.0238-.0131-.03321-.0229-.02061-.0215-.03597-.0475-.04489-.0759-.00619-.0176-.00991-.0365-.01486-.0548-.02659-.1042-.06042-.2065-.10123-.3061-.0195-.0469-.04204-.0924-.06749-.1362-.02726-.0477-.06067-.0917-.09937-.1307-.00743-.0074-.01398-.0158-.01951-.0247-.00151-.0038-.00151-.008 0-.0118.00118-.0029.00314-.0054.00565-.0073.00251-.0018.00548-.0029.00859-.0032.01082-.0005.02155.0021.03096.0074.13998.0538.27557.1184.40554.1932.05665.0331.11176.0684.16501.1065.01937.0129.03687.0284.052.0461.01496.0165.0256.0364.031.058.00541.0216.00539.0442-.00004.0658-.00271.0124-.00622.0246-.01052.0365-.0316.0787-.07999.1495-.14179.2074-.02664.0243-.05838.0423-.09287.0527-.01098.0033-.02214.006-.03343.008z"/><path d="m7.56893 11.8338c-.01297-.0002-.02575-.0032-.03742-.0088-.01166-.0057-.02193-.0139-.03006-.024-.01559-.0174-.02731-.0378-.03436-.0601-.00634-.0203-.01161-.041-.01579-.0619-.02196-.105-.05092-.2084-.08668-.3095-.01697-.0478-.03692-.0944-.05975-.1397-.02626-.053-.05981-.102-.09968-.1458-.0061-.0058-.01132-.0124-.01548-.0198-.00121-.0028-.00184-.0058-.00184-.0088s.00063-.006.00184-.0088c.00185-.0025.00417-.0045.00683-.0061.00266-.0015.0056-.0025.00865-.0029.0063-.0001.01251.0015.01796.0046.19823.0843.38537.1925.55722.3223.01674.0129.03266.0269.04767.0418.01878.0188.0319.0425.03787.0684.00596.0259.00453.0529-.00413.078-.00208.0082-.00488.0162-.00835.0239-.03509.0797-.08805.1504-.15479.2064-.02157.0169-.04544.0307-.07089.0409-.0188.007-.03876.0103-.05882.0099z"/><path d="m7.43301 9.75413c-.01598.00163-.0321-.0009-.04681-.00736-.0147-.00645-.02748-.01661-.03708-.02948-.01864-.02588-.03123-.0556-.03684-.08699-.01649-.08282-.03715-.16476-.06192-.24549-.01723-.0545-.03791-.10786-.06191-.15974-.02747-.06198-.06456-.11924-.1099-.16964-.00517-.00655-.00983-.01348-.01393-.02074-.00074-.00302-.00048-.00619.00075-.00904s.00336-.00522.00606-.00675c.0038-.00173.0079-.00268.01207-.00279.0083.00128.01641.00357.02415.00681.17685.07642.34421.17314.49872.28821.02291.01553.04371.03395.06191.0548.01255.0144.02159.0315.02642.04998.00483.01847.00533.03781.00145.05651-.00242.01464-.00647.02897-.01208.04272-.03176.07417-.08042.1399-.14209.19193-.01827.01444-.03836.02641-.05975.0356-.01563.0066-.03228.01048-.04922.01146z"/></g></svg>
diff --git a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
index 9a7296b6b1f..3b71e39d69b 100644
--- a/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
+++ b/app/assets/javascripts/access_tokens/components/access_token_table_app.vue
@@ -158,7 +158,7 @@ export default {
:aria-label="$options.i18n.revokeButton"
:data-confirm="modalMessage(name)"
data-confirm-btn-variant="danger"
- data-qa-selector="revoke_button"
+ data-testid="revoke-button"
data-method="put"
:href="revokePath"
icon="remove"
diff --git a/app/assets/javascripts/access_tokens/components/expires_at_field.vue b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
index 38501d63d3a..65206670a3c 100644
--- a/app/assets/javascripts/access_tokens/components/expires_at_field.vue
+++ b/app/assets/javascripts/access_tokens/components/expires_at_field.vue
@@ -68,7 +68,7 @@ export default {
:input-name="inputAttrs.name"
:input-id="inputAttrs.id"
:placeholder="inputAttrs.placeholder"
- data-qa-selector="expiry_date_field"
+ data-testid="expiry-date-field"
/>
<template #description>
<template v-if="description">
diff --git a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
index 4b51b4333aa..f476503c091 100644
--- a/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
+++ b/app/assets/javascripts/access_tokens/components/new_access_token_app.vue
@@ -45,7 +45,7 @@ export default {
formInputGroupProps() {
return {
id: this.$options.tokenInputId,
- 'data-qa-selector': 'created_access_token_field',
+ 'data-testid': 'created-access-token-field',
name: this.$options.tokenInputId,
};
},
@@ -110,7 +110,7 @@ export default {
@[$options.EVENT_ERROR]="onError"
@[$options.EVENT_SUCCESS]="onSuccess"
>
- <div ref="container" data-testid="access-token-section" data-qa-selector="access_token_section">
+ <div ref="container" data-testid="access-token-section">
<gl-alert
v-if="newToken"
variant="success"
diff --git a/app/assets/javascripts/add_context_commits_modal/store/actions.js b/app/assets/javascripts/add_context_commits_modal/store/actions.js
index 890db374160..b2d8791ee54 100644
--- a/app/assets/javascripts/add_context_commits_modal/store/actions.js
+++ b/app/assets/javascripts/add_context_commits_modal/store/actions.js
@@ -1,4 +1,4 @@
-import _ from 'lodash';
+import { uniqBy, orderBy } from 'lodash';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Api from '~/api';
import { createAlert } from '~/alert';
@@ -52,8 +52,8 @@ export const searchCommits = ({ dispatch, commit, state }, search = {}) => {
};
export const setCommits = ({ commit }, { commits: data, silentAddition = false }) => {
- let commits = _.uniqBy(data, 'short_id');
- commits = _.orderBy(data, (c) => new Date(c.committed_date), ['desc']);
+ let commits = uniqBy(data, 'short_id');
+ commits = orderBy(data, (c) => new Date(c.committed_date), ['desc']);
if (silentAddition) {
commit(types.SET_COMMITS_SILENT, commits);
} else {
@@ -125,8 +125,8 @@ export const removeContextCommits = ({ state }, forceReload = false) =>
});
export const setSelectedCommits = ({ commit }, selected) => {
- let selectedCommits = _.uniqBy(selected, 'short_id');
- selectedCommits = _.orderBy(
+ let selectedCommits = uniqBy(selected, 'short_id');
+ selectedCommits = orderBy(
selectedCommits,
(selectedCommit) => new Date(selectedCommit.committed_date),
['desc'],
diff --git a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
index 80af7d7400a..29de7e1ad1d 100644
--- a/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/abuse_report_notes.vue
@@ -6,6 +6,7 @@ import SkeletonLoadingContainer from '~/vue_shared/components/notes/skeleton_not
import { SKELETON_NOTES_COUNT } from '~/admin/abuse_report/constants';
import abuseReportNotesQuery from '../graphql/notes/abuse_report_notes.query.graphql';
import AbuseReportDiscussion from './notes/abuse_report_discussion.vue';
+import AbuseReportAddNote from './notes/abuse_report_add_note.vue';
export default {
name: 'AbuseReportNotes',
@@ -16,6 +17,7 @@ export default {
components: {
SkeletonLoadingContainer,
AbuseReportDiscussion,
+ AbuseReportAddNote,
},
props: {
abuseReportId: {
@@ -60,6 +62,9 @@ export default {
const discussionId = discussion.notes.nodes[0].id;
return discussionId.split('/')[discussionId.split('/').length - 1];
},
+ updateKey() {
+ this.addNoteKey = uniqueId(`abuse-report-add-note-${this.abuseReportId}`);
+ },
},
};
</script>
@@ -86,6 +91,16 @@ export default {
:abuse-report-id="abuseReportId"
/>
</ul>
+ <div class="js-comment-form">
+ <ul class="notes notes-form timeline">
+ <abuse-report-add-note
+ :key="addNoteKey"
+ :is-new-discussion="true"
+ :abuse-report-id="abuseReportId"
+ @cancelEditing="updateKey"
+ />
+ </ul>
+ </div>
</template>
</div>
</div>
diff --git a/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
index 5962203c382..ac7eeece694 100644
--- a/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/activity_history_item.vue
@@ -31,7 +31,7 @@ export default {
<template>
<history-item icon="warning">
- <div class="gl-display-flex gl-xs-flex-direction-column">
+ <div class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row">
<gl-sprintf :message="$options.i18n.reportedByForCategory">
<template #name>{{ reporterName }}</template>
<template #category>{{ report.category }}</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
new file mode 100644
index 00000000000..610b34a466f
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_add_note.vue
@@ -0,0 +1,156 @@
+<script>
+import { sprintf, __ } from '~/locale';
+import { createAlert } from '~/alert';
+import { clearDraft } from '~/lib/utils/autosave';
+import createNoteMutation from '../../graphql/notes/create_abuse_report_note.mutation.graphql';
+import AbuseReportCommentForm from './abuse_report_comment_form.vue';
+
+export default {
+ name: 'AbuseReportAddNote',
+ i18n: {
+ reply: __('Reply…'),
+ replyToComment: __('Reply to comment'),
+ commentError: __('Your comment could not be submitted because %{reason}.'),
+ genericError: __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ ),
+ },
+ components: {
+ AbuseReportCommentForm,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ discussionId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ isNewDiscussion: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showCommentForm: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isEditing: this.isNewDiscussion,
+ isSubmitting: false,
+ };
+ },
+ computed: {
+ autosaveKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return this.discussionId ? `${this.discussionId}-comment` : `${this.abuseReportId}-comment`;
+ },
+ timelineEntryClasses() {
+ return this.isNewDiscussion
+ ? 'timeline-entry note-form'
+ : // eslint-disable-next-line @gitlab/require-i18n-strings
+ 'note note-wrapper note-comment discussion-reply-holder gl-border-t-0! clearfix';
+ },
+ timelineEntryInnerClasses() {
+ return this.isNewDiscussion ? 'timeline-entry-inner' : '';
+ },
+ commentFormWrapperClasses() {
+ return !this.isEditing
+ ? 'gl-relative gl-display-flex gl-align-items-flex-start gl-flex-nowrap'
+ : '';
+ },
+ commentButtonText() {
+ return this.isNewDiscussion ? __('Comment') : __('Reply');
+ },
+ },
+ watch: {
+ showCommentForm: {
+ immediate: true,
+ handler(focus) {
+ if (focus) {
+ this.isEditing = true;
+ }
+ },
+ },
+ },
+ methods: {
+ async addNote({ commentText }) {
+ this.isSubmitting = true;
+
+ this.$apollo
+ .mutate({
+ mutation: createNoteMutation,
+ variables: {
+ input: {
+ noteableId: this.abuseReportId,
+ body: commentText,
+ discussionId: this.discussionId || null,
+ },
+ },
+ })
+ .then(() => {
+ clearDraft(this.autosaveKey);
+ this.cancelEditing();
+ })
+ .catch((error) => {
+ const errorMessage = error?.message
+ ? sprintf(this.$options.i18n.commentError, { reason: error.message.toLowerCase() })
+ : this.$options.i18n.genericError;
+
+ createAlert({
+ message: errorMessage,
+ parent: this.$el,
+ captureError: true,
+ });
+ })
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ },
+ cancelEditing() {
+ this.isEditing = this.isNewDiscussion;
+ this.$emit('cancelEditing');
+ },
+ showReplyForm() {
+ this.isEditing = true;
+ },
+ },
+};
+</script>
+
+<template>
+ <li :class="timelineEntryClasses" data-testid="abuse-report-note-timeline-entry">
+ <div :class="timelineEntryInnerClasses" data-testid="abuse-report-note-timeline-entry-inner">
+ <div class="timeline-content">
+ <div class="flash-container"></div>
+ <div :class="commentFormWrapperClasses" data-testid="abuse-report-comment-form-wrapper">
+ <abuse-report-comment-form
+ v-if="isEditing"
+ :abuse-report-id="abuseReportId"
+ :is-submitting="isSubmitting"
+ :autosave-key="autosaveKey"
+ :comment-button-text="commentButtonText"
+ @submitForm="addNote"
+ @cancelEditing="cancelEditing"
+ />
+ <textarea
+ v-else
+ ref="textarea"
+ rows="1"
+ class="reply-placeholder-text-field"
+ data-testid="abuse-report-note-reply-textarea"
+ :placeholder="$options.i18n.reply"
+ :aria-label="$options.i18n.replyToComment"
+ @focus="showReplyForm"
+ @click="showReplyForm"
+ ></textarea>
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue
new file mode 100644
index 00000000000..e7ee916fe5d
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_comment_form.vue
@@ -0,0 +1,133 @@
+<script>
+import { GlButton } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { getDraft, clearDraft, updateDraft } from '~/lib/utils/autosave';
+import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+
+export default {
+ name: 'AbuseReportCommentForm',
+ i18n: {
+ addReplyText: __('Add a reply'),
+ placeholderText: __('Write a comment or drag your files here…'),
+ cancelButtonText: __('Cancel'),
+ confirmText: s__('Notes|Are you sure you want to cancel creating this comment?'),
+ discardText: __('Discard changes'),
+ continueEditingText: __('Continue editing'),
+ },
+ components: {
+ GlButton,
+ MarkdownEditor,
+ },
+ inject: ['uploadNoteAttachmentPath'],
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ isSubmitting: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ autosaveKey: {
+ type: String,
+ required: true,
+ },
+ initialValue: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ commentButtonText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ commentText: getDraft(this.autosaveKey) || this.initialValue || '',
+ };
+ },
+ computed: {
+ formFieldProps() {
+ return {
+ 'aria-label': this.$options.i18n.addReplyText,
+ placeholder: this.$options.i18n.placeholderText,
+ id: 'abuse-report-add-or-edit-comment',
+ name: 'abuse-report-add-or-edit-comment',
+ };
+ },
+ markdownDocsPath() {
+ return helpPagePath('user/markdown');
+ },
+ },
+ methods: {
+ setCommentText(newText) {
+ if (!this.isSubmitting) {
+ this.commentText = newText;
+ updateDraft(this.autosaveKey, this.commentText);
+ }
+ },
+ async cancelEditing() {
+ if (this.commentText && this.commentText !== this.initialValue) {
+ const confirmed = await confirmAction(this.$options.i18n.confirmText, {
+ primaryBtnText: this.$options.i18n.discardText,
+ cancelBtnText: this.$options.i18n.continueEditingText,
+ primaryBtnVariant: 'danger',
+ });
+
+ if (!confirmed) {
+ return;
+ }
+ }
+
+ this.$emit('cancelEditing');
+ clearDraft(this.autosaveKey);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="timeline-discussion-body gl-overflow-visible!">
+ <div class="note-body gl-p-0! gl-overflow-visible!">
+ <form class="common-note-form gfm-form js-main-target-form gl-flex-grow-1 new-note">
+ <markdown-editor
+ :value="commentText"
+ :enable-content-editor="false"
+ render-markdown-path=""
+ :uploads-path="uploadNoteAttachmentPath"
+ :markdown-docs-path="markdownDocsPath"
+ :form-field-props="formFieldProps"
+ :autofocus="true"
+ @input="setCommentText"
+ @keydown.meta.enter="$emit('submitForm', { commentText })"
+ @keydown.ctrl.enter="$emit('submitForm', { commentText })"
+ @keydown.esc.stop="cancelEditing"
+ />
+ <div class="note-form-actions">
+ <gl-button
+ category="primary"
+ variant="confirm"
+ data-testid="comment-button"
+ :disabled="!commentText.length"
+ :loading="isSubmitting"
+ @click="$emit('submitForm', { commentText })"
+ >
+ {{ commentButtonText }}
+ </gl-button>
+ <gl-button
+ data-testid="cancel-button"
+ category="primary"
+ class="gl-ml-3"
+ @click="cancelEditing"
+ >{{ $options.i18n.cancelButtonText }}
+ </gl-button>
+ </div>
+ </form>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
index 4d24471fa43..4f28ec65e87 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_discussion.vue
@@ -4,6 +4,7 @@ import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item
import DiscussionNotesRepliesWrapper from '~/notes/components/discussion_notes_replies_wrapper.vue';
import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
import AbuseReportNote from './abuse_report_note.vue';
+import AbuseReportAddNote from './abuse_report_add_note.vue';
export default {
name: 'AbuseReportDiscussion',
@@ -12,6 +13,7 @@ export default {
DiscussionNotesRepliesWrapper,
ToggleRepliesWidget,
AbuseReportNote,
+ AbuseReportAddNote,
},
props: {
abuseReportId: {
@@ -26,6 +28,7 @@ export default {
data() {
return {
isExpanded: true,
+ showCommentForm: false,
};
},
computed: {
@@ -52,16 +55,24 @@ export default {
toggleDiscussion() {
this.isExpanded = !this.isExpanded;
},
+ startReplying() {
+ this.showCommentForm = true;
+ },
+ stopReplying() {
+ this.showCommentForm = false;
+ },
},
};
</script>
<template>
<abuse-report-note
- v-if="!hasReplies"
+ v-if="!hasReplies && !showCommentForm"
:note="note"
:abuse-report-id="abuseReportId"
+ show-reply-button
class="gl-mb-4"
+ @startReplying="startReplying"
/>
<timeline-entry-item v-else :data-note-id="noteId" class="note note-discussion gl-px-0">
<div class="timeline-content">
@@ -74,7 +85,9 @@ export default {
:note="note"
:discussion-id="discussionId"
:abuse-report-id="abuseReportId"
+ show-reply-button
class="gl-mb-4"
+ @startReplying="startReplying"
/>
<discussion-notes-replies-wrapper>
<toggle-replies-widget
@@ -92,6 +105,13 @@ export default {
:abuse-report-id="abuseReportId"
/>
</template>
+ <abuse-report-add-note
+ :discussion-id="discussionId"
+ :is-new-discussion="false"
+ :show-comment-form="showCommentForm"
+ :abuse-report-id="abuseReportId"
+ @cancelEditing="stopReplying"
+ />
</template>
</discussion-notes-replies-wrapper>
</ul>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue
new file mode 100644
index 00000000000..e2c348f8079
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_edit_note.vue
@@ -0,0 +1,98 @@
+<script>
+import { sprintf, __ } from '~/locale';
+import { createAlert } from '~/alert';
+import { clearDraft } from '~/lib/utils/autosave';
+import updateNoteMutation from '../../graphql/notes/update_abuse_report_note.mutation.graphql';
+
+import AbuseReportCommentForm from './abuse_report_comment_form.vue';
+
+export default {
+ name: 'AbuseReportEditNote',
+ i18n: {
+ updateError: __('Your comment could not be updated because %{reason}.'),
+ genericError: __('Something went wrong while editing your comment. Please try again.'),
+ },
+ components: {
+ AbuseReportCommentForm,
+ },
+ props: {
+ abuseReportId: {
+ type: String,
+ required: true,
+ },
+ discussionId: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ isSubmitting: false,
+ };
+ },
+ computed: {
+ autosaveKey() {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ return `${this.note.id}-comment`;
+ },
+ commentButtonText() {
+ return __('Save comment');
+ },
+ },
+ methods: {
+ async updateNote({ commentText }) {
+ this.isSubmitting = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateNoteMutation,
+ variables: {
+ input: {
+ id: this.note.id,
+ body: commentText,
+ },
+ },
+ })
+ .then(() => {
+ clearDraft(this.autosaveKey);
+ this.$emit('cancelEditing');
+ })
+ .catch((error) => {
+ const errorMessage = error?.message
+ ? sprintf(this.$options.i18n.updateError, { reason: error.message.toLowerCase() })
+ : this.$options.i18n.genericError;
+
+ createAlert({
+ message: errorMessage,
+ parent: this.$el,
+ captureError: true,
+ });
+ })
+ .finally(() => {
+ this.isSubmitting = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="flash-container"></div>
+ <abuse-report-comment-form
+ :abuse-report-id="abuseReportId"
+ :initial-value="note.body"
+ :is-submitting="isSubmitting"
+ :autosave-key="autosaveKey"
+ :comment-button-text="commentButtonText"
+ class="gl-pl-3 gl-mt-3"
+ @submitForm="updateNote"
+ @cancelEditing="$emit('cancelEditing')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
index 6da3017e11e..4423eb9e7b2 100644
--- a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note.vue
@@ -4,7 +4,10 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteHeader from '~/notes/components/note_header.vue';
+import EditedAt from '~/issues/show/components/edited.vue';
+import AbuseReportEditNote from './abuse_report_edit_note.vue';
import NoteBody from './abuse_report_note_body.vue';
+import AbuseReportNoteActions from './abuse_report_note_actions.vue';
export default {
name: 'AbuseReportNote',
@@ -15,8 +18,11 @@ export default {
GlAvatarLink,
GlAvatar,
TimelineEntryItem,
+ AbuseReportEditNote,
NoteHeader,
NoteBody,
+ AbuseReportNoteActions,
+ EditedAt,
},
props: {
abuseReportId: {
@@ -27,6 +33,16 @@ export default {
type: Object,
required: true,
},
+ showReplyButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ };
},
computed: {
noteAnchorId() {
@@ -38,6 +54,20 @@ export default {
authorId() {
return getIdFromGraphQLId(this.author.id);
},
+ showEditButton() {
+ return this.note.userPermissions.resolveNote;
+ },
+ editedAtClasses() {
+ return this.showReplyButton ? 'gl-text-secondary gl-pl-3' : 'gl-text-secondary gl-pl-8';
+ },
+ },
+ methods: {
+ startEditing() {
+ this.isEditing = true;
+ },
+ cancelEditing() {
+ this.isEditing = false;
+ },
},
};
</script>
@@ -59,8 +89,14 @@ export default {
/>
</gl-avatar-link>
</div>
- <div class="timeline-content">
- <div data-testid="note-wrapper">
+ <div class="timeline-content gl-pb-4!">
+ <abuse-report-edit-note
+ v-if="isEditing"
+ :abuse-report-id="abuseReportId"
+ :note="note"
+ @cancelEditing="cancelEditing"
+ />
+ <div v-else data-testid="note-wrapper">
<div class="note-header">
<note-header
:author="author"
@@ -70,11 +106,27 @@ export default {
>
<span v-if="note.createdAt" class="d-none d-sm-inline">&middot;</span>
</note-header>
+ <div class="gl-display-inline-flex">
+ <abuse-report-note-actions
+ :show-reply-button="showReplyButton"
+ :show-edit-button="showEditButton"
+ @startReplying="$emit('startReplying')"
+ @startEditing="startEditing"
+ />
+ </div>
</div>
<div class="timeline-discussion-body">
<note-body ref="noteBody" :note="note" />
</div>
+
+ <edited-at
+ v-if="note.lastEditedBy"
+ :updated-at="note.lastEditedAt"
+ :updated-by-name="note.lastEditedBy.name"
+ :updated-by-path="note.lastEditedBy.webPath"
+ :class="editedAtClasses"
+ />
</div>
</div>
</timeline-entry-item>
diff --git a/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue
new file mode 100644
index 00000000000..e35e231fc1b
--- /dev/null
+++ b/app/assets/javascripts/admin/abuse_report/components/notes/abuse_report_note_actions.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
+import { __ } from '~/locale';
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+
+export default {
+ name: 'AbuseReportNoteActions',
+ i18n: {
+ editButtonText: __('Edit comment'),
+ },
+ components: {
+ GlButton,
+ ReplyButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ showReplyButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showEditButton: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="note-actions">
+ <reply-button
+ v-if="showReplyButton"
+ ref="replyButton"
+ @startReplying="$emit('startReplying')"
+ />
+ <gl-button
+ v-if="showEditButton"
+ v-gl-tooltip
+ category="tertiary"
+ icon="pencil"
+ :title="$options.i18n.editButtonText"
+ :aria-label="$options.i18n.editButtonText"
+ @click="$emit('startEditing')"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
index e005e183c9f..3e9cc36b8b2 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_actions.vue
@@ -64,7 +64,6 @@ export default {
},
computed: {
getDrawerHeaderHeight() {
- if (!this.showActionsDrawer || gon.use_new_navigation) return '0';
return getContentWrapperHeight();
},
isFormValid() {
diff --git a/app/assets/javascripts/admin/abuse_report/components/report_header.vue b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
index 90c1943cb27..78689c58ecc 100644
--- a/app/assets/javascripts/admin/abuse_report/components/report_header.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/report_header.vue
@@ -53,7 +53,7 @@ export default {
<template>
<header
- class="gl-py-4 gl-border-b gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
+ class="gl-py-4 gl-border-b gl-display-flex gl-justify-content-space-between gl-flex-direction-column gl-sm-flex-direction-row"
>
<div class="gl-display-flex gl-align-items-center gl-gap-3">
<gl-badge :variant="badgeVariant" :aria-label="badgeText">
@@ -67,7 +67,7 @@ export default {
<gl-link :href="user.path"> @{{ user.username }} </gl-link>
</div>
<nav
- class="gl-display-flex gl-sm-align-items-center gl-mt-4 gl-sm-mt-0 gl-xs-flex-direction-column"
+ class="gl-display-flex gl-sm-align-items-center gl-mt-4 gl-sm-mt-0 gl-flex-direction-column gl-sm-flex-direction-row"
>
<gl-button :href="user.adminPath">
{{ $options.i18n.adminProfile }}
diff --git a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
index 99c8b3ece10..07e91f8bf6a 100644
--- a/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/reported_content.vue
@@ -65,14 +65,14 @@ export default {
<template>
<div class="gl-pt-6">
<div
- class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column gl-align-items-center"
+ class="gl-pb-3 gl-display-flex gl-justify-content-space-between gl-flex-direction-column gl-sm-flex-direction-row gl-align-items-center"
>
<h2 class="gl-font-size-h1 gl-mt-2 gl-mb-2">
{{ $options.i18n.reportTypes[reportType] }}
</h2>
<div
- class="gl-display-flex gl-align-items-stretch gl-xs-flex-direction-column gl-mt-3 gl-sm-mt-0"
+ class="gl-display-flex gl-align-items-stretch gl-flex-direction-column gl-sm-flex-direction-row gl-mt-3 gl-sm-mt-0"
>
<template v-if="report.screenshot">
<gl-button data-testid="screenshot-button" @click="toggleScreenshotModal">
diff --git a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
index 01436436b93..31ca24e675f 100644
--- a/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
+++ b/app/assets/javascripts/admin/abuse_report/graphql/notes/abuse_report_note_permissions.fragment.graphql
@@ -1,3 +1,3 @@
fragment AbuseReportNotePermissions on NotePermissions {
- adminNote
+ resolveNote
}
diff --git a/app/assets/javascripts/admin/abuse_report/index.js b/app/assets/javascripts/admin/abuse_report/index.js
index c2117130d26..e4319b3edef 100644
--- a/app/assets/javascripts/admin/abuse_report/index.js
+++ b/app/assets/javascripts/admin/abuse_report/index.js
@@ -30,6 +30,7 @@ export const initAbuseReportApp = () => {
allowScopedLabels: false,
updatePath: abuseReport.report.updatePath,
listPath: abuseReportsListPath,
+ uploadNoteAttachmentPath: abuseReport.uploadNoteAttachmentPath,
labelsManagePath: '',
allowLabelCreate: true,
},
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 662451c5eb4..62924dcd0a8 100644
--- a/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
+++ b/app/assets/javascripts/analytics/shared/components/projects_dropdown_filter.vue
@@ -281,7 +281,7 @@ export default {
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
/>
<div>
- <div data-testid="project-name" data-qa-selector="project_name">{{ item.name }}</div>
+ <div data-testid="project-name">{{ item.name }}</div>
<div class="gl-text-gray-500" data-testid="project-full-path">
{{ item.fullPath }}
</div>
diff --git a/app/assets/javascripts/analytics/shared/constants.js b/app/assets/javascripts/analytics/shared/constants.js
index f0d9bf201e5..e48281a7453 100644
--- a/app/assets/javascripts/analytics/shared/constants.js
+++ b/app/assets/javascripts/analytics/shared/constants.js
@@ -6,6 +6,7 @@ import {
getDateInPast,
getCurrentUtcDate,
nWeeksBefore,
+ nYearsBefore,
} from '~/lib/utils/datetime_utility';
import { s__, __, sprintf, n__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
@@ -14,11 +15,11 @@ export const DATE_RANGE_LIMIT = 180;
export const DEFAULT_DATE_RANGE = 29; // 30 including current date
export const PROJECTS_PER_PAGE = 50;
-const { isoDate, mediumDate } = masks;
+const { isoDate } = masks;
export const dateFormats = {
isoDate,
- defaultDate: mediumDate,
- defaultDateTime: 'mmm d, yyyy h:MMtt',
+ defaultDate: 'mmm dd, yyyy',
+ defaultDateTime: 'mmm dd, yyyy h:MMtt',
month: 'mmmm',
};
@@ -251,3 +252,43 @@ export const METRICS_POPOVER_CONTENT = {
),
},
};
+
+export const USAGE_OVERVIEW_NO_DATA_ERROR = s__(
+ 'ValueStreamAnalytics|Failed to load usage overview data',
+);
+
+export const USAGE_OVERVIEW_DEFAULT_DATE_RANGE = {
+ endDate: TODAY,
+ startDate: nYearsBefore(TODAY, 1),
+};
+
+export const USAGE_OVERVIEW_IDENTIFIER_GROUPS = 'groups';
+export const USAGE_OVERVIEW_IDENTIFIER_PROJECTS = 'projects';
+export const USAGE_OVERVIEW_IDENTIFIER_ISSUES = 'issues';
+export const USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS = 'merge_requests';
+export const USAGE_OVERVIEW_IDENTIFIER_PIPELINES = 'pipelines';
+
+// Defines the constants used for querying the API as well as the order they appear
+export const USAGE_OVERVIEW_METADATA = {
+ [USAGE_OVERVIEW_IDENTIFIER_GROUPS]: { options: { title: __('Groups'), titleIcon: 'group' } },
+ [USAGE_OVERVIEW_IDENTIFIER_PROJECTS]: {
+ options: { title: __('Projects'), titleIcon: 'project' },
+ },
+ [USAGE_OVERVIEW_IDENTIFIER_ISSUES]: {
+ options: { title: __('Issues'), titleIcon: 'issues' },
+ },
+ [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS]: {
+ options: { title: __('Merge requests'), titleIcon: 'merge-request' },
+ },
+ [USAGE_OVERVIEW_IDENTIFIER_PIPELINES]: {
+ options: { title: __('Pipelines'), titleIcon: 'pipeline' },
+ },
+};
+
+export const USAGE_OVERVIEW_QUERY_INCLUDE_KEYS = {
+ [USAGE_OVERVIEW_IDENTIFIER_GROUPS]: 'includeGroups',
+ [USAGE_OVERVIEW_IDENTIFIER_PROJECTS]: 'includeProjects',
+ [USAGE_OVERVIEW_IDENTIFIER_ISSUES]: 'includeIssues',
+ [USAGE_OVERVIEW_IDENTIFIER_MERGE_REQUESTS]: 'includeMergeRequests',
+ [USAGE_OVERVIEW_IDENTIFIER_PIPELINES]: 'includePipelines',
+};
diff --git a/app/assets/javascripts/api/bulk_imports_api.js b/app/assets/javascripts/api/bulk_imports_api.js
index 248f5601705..a5c9f904c1f 100644
--- a/app/assets/javascripts/api/bulk_imports_api.js
+++ b/app/assets/javascripts/api/bulk_imports_api.js
@@ -1,12 +1,22 @@
import { buildApiUrl } from '~/api/api_utils';
import axios from '~/lib/utils/axios_utils';
-const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
+const BULK_IMPORT_ENTITIES_PATH = '/api/:version/bulk_imports/:id/entities';
+const BULK_IMPORTS_ENTITIES_PATH = '/api/:version/bulk_imports/entities';
const BULK_IMPORT_ENTITIES_FAILURES_PATH =
'/api/:version/bulk_imports/:id/entities/:entity_id/failures';
+export const getBulkImportHistory = (id, params = {}) => {
+ const bulkImportHistoryUrl = buildApiUrl(BULK_IMPORT_ENTITIES_PATH).replace(
+ ':id',
+ encodeURIComponent(id),
+ );
+
+ return axios.get(bulkImportHistoryUrl, { params });
+};
+
export const getBulkImportsHistory = (params) =>
- axios.get(buildApiUrl(BULK_IMPORT_ENTITIES_PATH), { params });
+ axios.get(buildApiUrl(BULK_IMPORTS_ENTITIES_PATH), { params });
export const getBulkImportFailures = (id, entityId, { page, perPage }) => {
const failuresPath = buildApiUrl(BULK_IMPORT_ENTITIES_FAILURES_PATH)
diff --git a/app/assets/javascripts/api/user_api.js b/app/assets/javascripts/api/user_api.js
index c056b42b5b6..302de976080 100644
--- a/app/assets/javascripts/api/user_api.js
+++ b/app/assets/javascripts/api/user_api.js
@@ -4,6 +4,7 @@ import { buildApiUrl } from './api_utils';
const USER_COUNTS_PATH = '/api/:version/user_counts';
const USERS_PATH = '/api/:version/users.json';
+const USERS_SAML_PATH = '/api/:version/groups/:id/users.json';
const USER_PATH = '/api/:version/users/:id';
const USER_STATUS_PATH = '/api/:version/users/:id/status';
const USER_PROJECTS_PATH = '/api/:version/users/:id/projects';
@@ -25,6 +26,25 @@ export function getUsers(query, options) {
});
}
+/**
+ * Returns a list of SAML users and service accounts that contains the query string.
+ * If the query string is less than 3 characters it returns an empty list.
+ *
+ * @param {string} query - query string to search
+ * @param {string} groupId -- top-level group id
+ * @param {object} options
+ */
+export function getGroupUsers(query, groupId, options) {
+ const url = buildApiUrl(USERS_SAML_PATH).replace(':id', groupId);
+ return axios.get(url, {
+ params: {
+ search: query,
+ per_page: DEFAULT_PER_PAGE,
+ ...options,
+ },
+ });
+}
+
export function getUser(id, options) {
const url = buildApiUrl(USER_PATH).replace(':id', encodeURIComponent(id));
return axios.get(url, {
diff --git a/app/assets/javascripts/authentication/password/components/password_input.vue b/app/assets/javascripts/authentication/password/components/password_input.vue
index 6e3af96cf33..7f2a2beaa47 100644
--- a/app/assets/javascripts/authentication/password/components/password_input.vue
+++ b/app/assets/javascripts/authentication/password/components/password_input.vue
@@ -27,11 +27,6 @@ export default {
required: false,
default: null,
},
- qaSelector: {
- type: String,
- required: false,
- default: null,
- },
testid: {
type: String,
required: false,
@@ -80,7 +75,6 @@ export default {
:autocomplete="autocomplete"
:name="name"
:minlength="minimumPasswordLength"
- :data-qa-selector="qaSelector"
:data-testid="testid"
:title="title"
:type="type"
diff --git a/app/assets/javascripts/authentication/password/index.js b/app/assets/javascripts/authentication/password/index.js
index a4f2d038cf7..903512a7b53 100644
--- a/app/assets/javascripts/authentication/password/index.js
+++ b/app/assets/javascripts/authentication/password/index.js
@@ -9,7 +9,7 @@ export const initPasswordInput = () => {
}
const { form } = el;
- const { title, id, minimumPasswordLength, qaSelector, testid, autocomplete, name } = el.dataset;
+ const { title, id, minimumPasswordLength, testid, autocomplete, name } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -21,7 +21,6 @@ export const initPasswordInput = () => {
title,
id,
minimumPasswordLength,
- qaSelector,
testid,
autocomplete,
name,
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
index 907b68e6ffc..e97846bae29 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/manage_two_factor_form.vue
@@ -119,7 +119,6 @@ export default {
type="password"
name="current_password"
:state="currentPasswordState"
- data-qa-selector="current_password_field"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
index d3b914ea8aa..240bf005532 100644
--- a/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
+++ b/app/assets/javascripts/authentication/two_factor_auth/components/recovery_codes.vue
@@ -115,14 +115,10 @@ export default {
</gl-sprintf>
</p>
- <gl-card
- class="codes-to-print gl-my-5"
- data-testid="recovery-codes"
- data-qa-selector="codes_content"
- >
+ <gl-card class="codes-to-print gl-my-5" data-testid="recovery-codes">
<ul class="gl-m-0 gl-pl-5">
<li v-for="(code, index) in codes" :key="index">
- <span class="gl-font-monospace" data-qa-selector="code_content">{{ code }}</span>
+ <span class="gl-font-monospace" data-testid="code-content">{{ code }}</span>
</li>
</ul>
</gl-card>
@@ -131,7 +127,7 @@ export default {
<clipboard-button
:title="$options.i18n.copyButton"
:text="codesAsString"
- data-qa-selector="copy_button"
+ data-testid="copy-button"
@click="handleButtonClick($options.copyButtonAction)"
>
{{ $options.i18n.copyButton }}
@@ -163,7 +159,7 @@ export default {
:disabled="proceedButtonDisabled"
:title="$options.i18n.proceedButton"
variant="confirm"
- data-qa-selector="proceed_button"
+ data-testid="proceed-button"
data-track-action="click_button"
:data-track-label="`${$options.trackingLabelPrefix}proceed_button`"
>{{ $options.i18n.proceedButton }}</gl-button
diff --git a/app/assets/javascripts/badges/components/badge_form.vue b/app/assets/javascripts/badges/components/badge_form.vue
index 19da1253a17..fbe773e6e2d 100644
--- a/app/assets/javascripts/badges/components/badge_form.vue
+++ b/app/assets/javascripts/badges/components/badge_form.vue
@@ -183,9 +183,7 @@ export default {
})
.catch((error) => {
createAlert({
- message: s__(
- 'Badges|Adding the badge failed, please check the entered URLs and try again.',
- ),
+ message: s__('Badges|Failed to add new badge. Check the URLs, then try again.'),
});
throw error;
});
@@ -215,7 +213,7 @@ export default {
@submit.prevent.stop="onSubmit"
>
<gl-form-group :label="s__('Badges|Name')" label-for="badge-name" class="gl-max-w-48">
- <gl-form-input id="badge-name" v-model="name" data-qa-selector="badge_name_field" />
+ <gl-form-input id="badge-name" v-model="name" data-testid="badge-name-field" />
</gl-form-group>
<div class="form-group">
@@ -224,7 +222,7 @@ export default {
<input
id="badge-link-url"
v-model="linkUrl"
- data-qa-selector="badge_link_url_field"
+ data-testid="badge-link-url-field"
type="URL"
class="form-control gl-form-input gl-max-w-80"
required
@@ -240,7 +238,7 @@ export default {
<input
id="badge-image-url"
v-model="imageUrl"
- data-qa-selector="badge_image_url_field"
+ data-testid="badge-image-url-field"
type="URL"
class="form-control gl-form-input gl-max-w-80"
required
@@ -272,7 +270,7 @@ export default {
type="submit"
variant="confirm"
category="primary"
- data-qa-selector="add_badge_button"
+ data-testid="add-badge-button"
class="gl-mr-3"
>
{{ saveText }}
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index 12c9662b30d..a4f88067fa9 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -29,10 +29,8 @@ export default {
GlModal: GlModalDirective,
},
i18n: {
- emptyGroupMessage: s__('Badges|This group has no badges, start by creating a new one above.'),
- emptyProjectMessage: s__(
- 'Badges|This project has no badges, start by creating a new one above.',
- ),
+ emptyGroupMessage: s__('Badges|This group has no badges. Add an existing badge or create one.'),
+ emptyProjectMessage: s__('Badges|This project has no badges. Start by adding a new badge.'),
},
data() {
return {
diff --git a/app/assets/javascripts/badges/components/badge_settings.vue b/app/assets/javascripts/badges/components/badge_settings.vue
index f0d354c6378..32c24564d21 100644
--- a/app/assets/javascripts/badges/components/badge_settings.vue
+++ b/app/assets/javascripts/badges/components/badge_settings.vue
@@ -25,7 +25,7 @@ export default {
addButton: s__('Badges|Add badge'),
addFormTitle: s__('Badges|Add new badge'),
deleteModalText: s__(
- 'Badges|You are going to delete this badge. Deleted badges %{strongStart}cannot%{strongEnd} be restored.',
+ 'Badges|If you delete this badge, you %{strongStart}cannot%{strongEnd} restore it.',
),
},
data() {
@@ -74,7 +74,7 @@ export default {
})
.catch((error) => {
createAlert({
- message: s__('Badges|Deleting the badge failed, please try again.'),
+ message: s__('Badges|Failed to delete the badge. Try again.'),
});
throw error;
});
diff --git a/app/assets/javascripts/batch_comments/mixins/resolved_status.js b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
index fddb843bb52..da7c7809bed 100644
--- a/app/assets/javascripts/batch_comments/mixins/resolved_status.js
+++ b/app/assets/javascripts/batch_comments/mixins/resolved_status.js
@@ -25,11 +25,10 @@ export default {
resolvedStatusMessage() {
let message;
const discussionResolved = this.isDiscussionResolved(
- this.draft ? this.draft.discussion_id : this.discussionId,
+ 'draft' in this ? this.draft.discussion_id : this.discussionId,
);
- const discussionToBeResolved = this.draft
- ? this.draft.resolve_discussion
- : this.resolveDiscussion;
+ const discussionToBeResolved =
+ 'draft' in this ? this.draft.resolve_discussion : this.resolveDiscussion;
if (discussionToBeResolved && discussionResolved && !this.$options.showStaysResolved) {
return undefined;
diff --git a/app/assets/javascripts/behaviors/index.js b/app/assets/javascripts/behaviors/index.js
index 84ff8fa7f33..fe3868fdd04 100644
--- a/app/assets/javascripts/behaviors/index.js
+++ b/app/assets/javascripts/behaviors/index.js
@@ -6,9 +6,9 @@ import installGlEmojiElement from './gl_emoji';
import initCopyAsGFM from './markdown/copy_as_gfm';
import './quick_submit';
import './requires_input';
-import initPageShortcuts from './shortcuts';
import { initToastMessages } from './toasts';
import { initGlobalAlerts } from './global_alerts';
+import './shortcuts';
import './toggler_behavior';
import './preview_markdown';
@@ -17,7 +17,6 @@ installGlEmojiElement();
initCopyAsGFM();
initCopyToClipboard();
-initPageShortcuts();
initCollapseSidebarOnWindowResize();
initToastMessages();
diff --git a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
index 36317444af9..72aae254584 100644
--- a/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/copy_as_gfm.js
@@ -152,7 +152,9 @@ export class CopyAsGFM {
if (lineElements.length > 0) {
for (let i = 0; i < lineElements.length; i += 1) {
const lineElement = lineElements[i];
- codeElement.appendChild(lineElement);
+ const line = document.createElement('span');
+ line.append(...lineElement.childNodes);
+ codeElement.appendChild(line);
codeElement.appendChild(document.createTextNode('\n'));
}
} else {
diff --git a/app/assets/javascripts/behaviors/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts.js
deleted file mode 100644
index 22a8be92e52..00000000000
--- a/app/assets/javascripts/behaviors/shortcuts.js
+++ /dev/null
@@ -1,36 +0,0 @@
-export default function initPageShortcuts() {
- const { page } = document.body.dataset;
- const pagesWithCustomShortcuts = [
- 'projects:activity',
- 'projects:artifacts:browse',
- 'projects:artifacts:file',
- 'projects:blame:show',
- 'projects:blob:show',
- 'projects:commit:show',
- 'projects:commits:show',
- 'projects:find_file:show',
- 'projects:issues:edit',
- 'projects:issues:index',
- 'projects:issues:new',
- 'projects:issues:show',
- 'projects:merge_requests:creations:diffs',
- 'projects:merge_requests:creations:new',
- 'projects:merge_requests:edit',
- 'projects:merge_requests:index',
- 'projects:merge_requests:show',
- 'projects:network:show',
- 'projects:show',
- 'projects:tree:show',
- 'groups:show',
- ];
-
- // the pages above have their own shortcuts sub-classes instantiated elsewhere
- // TODO: replace this whitelist with something more automated/maintainable
- // https://gitlab.com/gitlab-org/gitlab/-/issues/392845
- if (page && !pagesWithCustomShortcuts.includes(page)) {
- import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts/shortcuts')
- .then(({ default: Shortcuts }) => new Shortcuts())
- .catch(() => {});
- }
- return false;
-}
diff --git a/app/assets/javascripts/behaviors/shortcuts/index.js b/app/assets/javascripts/behaviors/shortcuts/index.js
new file mode 100644
index 00000000000..cc6d8a23f68
--- /dev/null
+++ b/app/assets/javascripts/behaviors/shortcuts/index.js
@@ -0,0 +1,16 @@
+const shortcutsPromise = import(/* webpackChunkName: 'shortcutsBundle' */ './shortcuts')
+ .then(({ default: Shortcuts }) => new Shortcuts())
+ .catch(() => {});
+
+export const addShortcutsExtension = (ShortcutExtension, ...args) =>
+ shortcutsPromise.then((shortcuts) => shortcuts.addExtension(ShortcutExtension, args));
+
+export const resetShortcutsForTests = async () => {
+ if (process.env.NODE_ENV === 'test') {
+ const { Mousetrap, clearStopCallbacksForTests } = await import('~/lib/mousetrap');
+ clearStopCallbacksForTests();
+ Mousetrap.reset();
+ const shortcuts = await shortcutsPromise;
+ shortcuts.extensions.clear();
+ }
+};
diff --git a/app/assets/javascripts/behaviors/shortcuts/keybindings.js b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
index 941662635ea..15229689306 100644
--- a/app/assets/javascripts/behaviors/shortcuts/keybindings.js
+++ b/app/assets/javascripts/behaviors/shortcuts/keybindings.js
@@ -538,13 +538,10 @@ const GLOBAL_SHORTCUTS_GROUP = {
GO_TO_YOUR_TODO_LIST,
TOGGLE_PERFORMANCE_BAR,
HIDE_APPEARING_CONTENT,
+ TOGGLE_SUPER_SIDEBAR,
],
};
-if (gon.use_new_navigation) {
- GLOBAL_SHORTCUTS_GROUP.keybindings.push(TOGGLE_SUPER_SIDEBAR);
-}
-
export const EDITING_SHORTCUTS_GROUP = {
id: 'editing',
name: __('Editing'),
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
index 9514ad853b0..e05694c0907 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts.js
@@ -63,11 +63,17 @@ function getToolbarBtnToShortcutsMap($textarea) {
export default class Shortcuts {
constructor() {
+ if (process.env.NODE_ENV !== 'production' && this.constructor !== Shortcuts) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Shortcuts cannot be subclassed.');
+ }
+
+ this.extensions = new Map();
this.onToggleHelp = this.onToggleHelp.bind(this);
this.helpModalElement = null;
this.helpModalVueInstance = null;
- this.bindCommands([
+ this.addAll([
[TOGGLE_KEYBOARD_SHORTCUTS_DIALOG, this.onToggleHelp],
[START_SEARCH, Shortcuts.focusSearch],
[FOCUS_FILTER_BAR, this.focusFilter.bind(this)],
@@ -94,16 +100,12 @@ export default class Shortcuts {
const findFileURL = document.body.dataset.findFile;
if (typeof findFileURL !== 'undefined' && findFileURL !== null) {
- this.bindCommand(GO_TO_PROJECT_FIND_FILE, () => {
+ this.add(GO_TO_PROJECT_FIND_FILE, () => {
visitUrl(findFileURL);
});
}
- const shortcutsModalTriggerEvent = 'click.shortcutsModalTrigger';
- // eslint-disable-next-line @gitlab/no-global-event-off
- $(document)
- .off(shortcutsModalTriggerEvent)
- .on(shortcutsModalTriggerEvent, '.js-shortcuts-modal-trigger', this.onToggleHelp);
+ $(document).on('click', '.js-shortcuts-modal-trigger', this.onToggleHelp);
if (shouldDisableShortcuts()) {
disableShortcuts();
@@ -111,6 +113,62 @@ export default class Shortcuts {
}
/**
+ * Instantiate a legacy shortcut extension class.
+ *
+ * NOTE: The preferred approach for adding shortcuts is described in
+ * https://docs.gitlab.com/ee/development/fe_guide/keyboard_shortcuts.html.
+ * This method is only for existing legacy shortcut classes.
+ *
+ * A shortcut extension class packages up several shortcuts and behaviors for
+ * a page or set of pages. They are considered legacy because they usually do
+ * not follow modern best practices. For instance, they may hook into the UI
+ * in brittle ways, e.g.. querySelectors.
+ *
+ * Extension classes can declare dependencies on other shortcut extension
+ * classes by listing them in a static `dependencies` property. This is
+ * essentially a reimplementation of the previous subclassing approach, but
+ * with idempotency: a shortcut extension class can now only be added at most
+ * one time.
+ *
+ * Extension classes are instantiated and given the Shortcuts singleton
+ * instance as their first argument. If the class constructor needs
+ * additional arguments, pass them via the second argument as an array.
+ *
+ * See https://gitlab.com/gitlab-org/gitlab/-/issues/392845 for more context.
+ *
+ * @param {Function} Extension The extension class to add/instantiate.
+ * @param {Array} [args] A list of additional args to pass to the extension
+ * class constructor.
+ * @param {Set} [extensionsCurrentlyLoading] For internal use only. Do not
+ * use.
+ * @returns The instantiated shortcut extension class.
+ */
+ addExtension(Extension, args = [], extensionsCurrentlyLoading = new Set()) {
+ extensionsCurrentlyLoading.add(Extension);
+
+ let instance = this.extensions.get(Extension);
+ if (!instance) {
+ for (const Dep of Extension.dependencies ?? []) {
+ if (extensionsCurrentlyLoading.has(Dep) || Dep === Shortcuts) {
+ // We've encountered a circular dependency, so stop recursing.
+ // eslint-disable-next-line no-continue
+ continue;
+ }
+
+ extensionsCurrentlyLoading.add(Dep);
+
+ this.addExtension(Dep, [], extensionsCurrentlyLoading);
+ }
+
+ instance = new Extension(this, ...args);
+ this.extensions.set(Extension, instance);
+ }
+
+ extensionsCurrentlyLoading.delete(Extension);
+ return instance;
+ }
+
+ /**
* Bind the keyboard shortcut(s) defined by the given command to the given
* callback.
*
@@ -120,7 +178,7 @@ export default class Shortcuts {
* @returns {void}
*/
// eslint-disable-next-line class-methods-use-this
- bindCommand(command, callback) {
+ add(command, callback) {
Mousetrap.bind(keysFor(command), callback);
}
@@ -132,8 +190,8 @@ export default class Shortcuts {
* command/callback pairs.
* @returns {void}
*/
- bindCommands(commandsAndCallbacks) {
- commandsAndCallbacks.forEach((commandAndCallback) => this.bindCommand(...commandAndCallback));
+ addAll(commandsAndCallbacks) {
+ commandsAndCallbacks.forEach((commandAndCallback) => this.add(...commandAndCallback));
}
onToggleHelp(e) {
@@ -198,11 +256,7 @@ export default class Shortcuts {
}
static focusSearch(e) {
- if (gon.use_new_navigation) {
- document.querySelector('#super-sidebar-search')?.click();
- } else {
- document.querySelector('#search')?.focus();
- }
+ document.querySelector('#super-sidebar-search')?.click();
if (e.preventDefault) {
e.preventDefault();
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
index 65ae67d156f..a0bfd337d10 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_blob.js
@@ -7,7 +7,6 @@ import {
getShaFromUrl,
} from '~/lib/utils/url_utility';
import { updateRefPortionOfTitle } from '~/repository/utils/title';
-import Shortcuts from './shortcuts';
const defaults = {
fileBlobPermalinkUrl: null,
@@ -19,15 +18,14 @@ function eventHasModifierKeys(event) {
return event.ctrlKey || event.metaKey || event.shiftKey;
}
-export default class ShortcutsBlob extends Shortcuts {
- constructor(opts) {
+export default class ShortcutsBlob {
+ constructor(shortcuts, opts) {
const options = { ...defaults, ...opts };
- super();
this.options = options;
this.shortcircuitPermalinkButton();
- this.bindCommand(PROJECT_FILES_GO_TO_PERMALINK, this.moveToFilePermalink.bind(this));
+ shortcuts.add(PROJECT_FILES_GO_TO_PERMALINK, this.moveToFilePermalink.bind(this));
}
moveToFilePermalink() {
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
index f26878cf161..393d0165a07 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_find_file.js
@@ -8,10 +8,8 @@ import {
import { addStopCallback } from '~/lib/mousetrap';
import ShortcutsNavigation from './shortcuts_navigation';
-export default class ShortcutsFindFile extends ShortcutsNavigation {
- constructor(projectFindFile) {
- super();
-
+export default class ShortcutsFindFile {
+ constructor(shortcuts, projectFindFile) {
addStopCallback((e, element, combo) => {
if (
element === projectFindFile.inputElement[0] &&
@@ -28,11 +26,13 @@ export default class ShortcutsFindFile extends ShortcutsNavigation {
return undefined;
});
- this.bindCommands([
+ shortcuts.addAll([
[PROJECT_FILES_MOVE_SELECTION_UP, projectFindFile.selectRowUp],
[PROJECT_FILES_MOVE_SELECTION_DOWN, projectFindFile.selectRowDown],
[PROJECT_FILES_GO_BACK, projectFindFile.goToTree],
[PROJECT_FILES_OPEN_SELECTION, projectFindFile.goToBlob],
]);
}
+
+ static dependencies = [ShortcutsNavigation];
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
index b0e515ac19d..cde6d59b210 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_issuable.js
@@ -16,12 +16,9 @@ import {
MR_COPY_SOURCE_BRANCH_NAME,
ISSUABLE_COPY_REF,
} from './keybindings';
-import Shortcuts from './shortcuts';
-
-export default class ShortcutsIssuable extends Shortcuts {
- constructor() {
- super();
+export default class ShortcutsIssuable {
+ constructor(shortcuts) {
this.branchInMemoryButton = document.createElement('button');
this.branchClipboardInstance = new ClipboardJS(this.branchInMemoryButton);
this.branchClipboardInstance.on('success', () => {
@@ -40,7 +37,7 @@ export default class ShortcutsIssuable extends Shortcuts {
toast(s__('GlobalShortcuts|Unable to copy the reference at this time.'));
});
- this.bindCommands([
+ shortcuts.addAll([
[ISSUE_MR_CHANGE_ASSIGNEE, () => ShortcutsIssuable.openSidebarDropdown('assignee')],
[ISSUE_MR_CHANGE_MILESTONE, () => ShortcutsIssuable.openSidebarDropdown('milestone')],
[ISSUABLE_CHANGE_LABEL, () => ShortcutsIssuable.openSidebarDropdown('labels')],
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
index 4691a4228e6..bae50c02599 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_navigation.js
@@ -21,13 +21,10 @@ import {
PROJECT_FILES_GO_TO_COMPARE,
NEW_ISSUE,
} from './keybindings';
-import Shortcuts from './shortcuts';
-export default class ShortcutsNavigation extends Shortcuts {
- constructor() {
- super();
-
- this.bindCommands([
+export default class ShortcutsNavigation {
+ constructor(shortcuts) {
+ shortcuts.addAll([
[GO_TO_PROJECT_OVERVIEW, () => findAndFollowLink('.shortcuts-project')],
[GO_TO_PROJECT_ACTIVITY_FEED, () => findAndFollowLink('.shortcuts-project-activity')],
[GO_TO_PROJECT_RELEASES, () => findAndFollowLink('.shortcuts-deployments-releases')],
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
index 02c6af53fc2..eee8c1acf1a 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_network.js
@@ -8,11 +8,9 @@ import {
} from './keybindings';
import ShortcutsNavigation from './shortcuts_navigation';
-export default class ShortcutsNetwork extends ShortcutsNavigation {
- constructor(graph) {
- super();
-
- this.bindCommands([
+export default class ShortcutsNetwork {
+ constructor(shortcuts, graph) {
+ shortcuts.addAll([
[REPO_GRAPH_SCROLL_LEFT, graph.scrollLeft],
[REPO_GRAPH_SCROLL_RIGHT, graph.scrollRight],
[REPO_GRAPH_SCROLL_UP, graph.scrollUp],
@@ -21,4 +19,6 @@ export default class ShortcutsNetwork extends ShortcutsNavigation {
[REPO_GRAPH_SCROLL_BOTTOM, graph.scrollBottom],
]);
}
+
+ static dependencies = [ShortcutsNavigation];
}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
index 62d612cfa6d..5f45331bf76 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_wiki.js
@@ -2,13 +2,13 @@ import findAndFollowLink from '~/lib/utils/navigation_utility';
import { EDIT_WIKI_PAGE } from './keybindings';
import ShortcutsNavigation from './shortcuts_navigation';
-export default class ShortcutsWiki extends ShortcutsNavigation {
- constructor() {
- super();
-
- this.bindCommand(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki);
+export default class ShortcutsWiki {
+ constructor(shortcuts) {
+ shortcuts.add(EDIT_WIKI_PAGE, ShortcutsWiki.editWiki);
}
+ static dependencies = [ShortcutsNavigation];
+
static editWiki() {
findAndFollowLink('.js-wiki-edit');
}
diff --git a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
index 379d5e38197..e9f54639fdd 100644
--- a/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
+++ b/app/assets/javascripts/blob/filepath_form/components/template_selector.vue
@@ -149,7 +149,6 @@ export default {
block
class="gl-font-regular"
data-testid="template-selector"
- data-qa-selector="template_selector"
:toggle-text="dropdownToggleText"
:search-placeholder="$options.i18n.searchPlaceholder"
:items="dropdownItems"
diff --git a/app/assets/javascripts/blob_edit/blob_bundle.js b/app/assets/javascripts/blob_edit/blob_bundle.js
index b3bd23e49f8..be96c83aea2 100644
--- a/app/assets/javascripts/blob_edit/blob_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_bundle.js
@@ -40,6 +40,7 @@ export default () => {
const filePath = `${editBlobForm.data('blobFilename')}`;
const currentAction = $('.js-file-title').data('currentAction');
const projectId = editBlobForm.data('project-id');
+ const projectPath = editBlobForm.data('project-path');
const isMarkdown = editBlobForm.data('is-markdown');
const previewMarkdownPath = editBlobForm.data('previewMarkdownPath');
const commitButton = $('.js-commit-button');
@@ -54,6 +55,7 @@ export default () => {
filePath,
currentAction,
projectId,
+ projectPath,
isMarkdown,
previewMarkdownPath,
});
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 007fbd29e82..78ccacd9f57 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -24,6 +24,10 @@ export default class EditBlob {
this.fetchMarkdownExtension();
}
+ if (this.options.filePath === '.gitlab/security-policies/policy.yml') {
+ this.fetchSecurityPolicyExtension(this.options.projectPath);
+ }
+
this.initModePanesAndLinks();
this.initFilepathForm();
this.initSoftWrap();
@@ -54,6 +58,20 @@ export default class EditBlob {
addEditorMarkdownListeners(this.editor);
}
+ async fetchSecurityPolicyExtension(projectPath) {
+ try {
+ const { SecurityPolicySchemaExtension } = await import(
+ '~/editor/extensions/source_editor_security_policy_schema_ext'
+ );
+ this.editor.use([{ definition: SecurityPolicySchemaExtension }]);
+ this.editor.registerSecurityPolicySchema(projectPath);
+ } catch (e) {
+ createAlert({
+ message: `${BLOB_EDITOR_ERROR}: ${e}`,
+ });
+ }
+ }
+
configureMonacoEditor() {
const editorEl = document.getElementById('editor');
const fileContentEl = document.getElementById('file-content');
@@ -64,6 +82,7 @@ export default class EditBlob {
this.editor = rootEditor.createInstance({
el: editorEl,
blobContent: editorEl.innerText,
+ blobPath: this.options.filePath,
});
this.editor.use([
{ definition: ToolbarExtension },
diff --git a/app/assets/javascripts/boards/components/board_add_new_column.vue b/app/assets/javascripts/boards/components/board_add_new_column.vue
index f103feecab2..477fc3d9b7b 100644
--- a/app/assets/javascripts/boards/components/board_add_new_column.vue
+++ b/app/assets/javascripts/boards/components/board_add_new_column.vue
@@ -7,8 +7,6 @@ import {
GlCollapsibleListbox,
GlIcon,
} from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapGetters, mapState } from 'vuex';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BoardAddNewColumnForm from '~/boards/components/board_add_new_column_form.vue';
import { __, s__ } from '~/locale';
@@ -31,7 +29,7 @@ export default {
directives: {
GlTooltip,
},
- inject: ['scopedLabelsAvailable', 'issuableType', 'fullPath', 'boardType', 'isApolloBoard'],
+ inject: ['scopedLabelsAvailable', 'issuableType', 'fullPath', 'boardType'],
props: {
listQueryVariables: {
type: Object,
@@ -51,12 +49,12 @@ export default {
selectedId: null,
selectedLabel: null,
selectedIdValid: true,
- labelsApollo: [],
+ labels: [],
searchTerm: '',
};
},
apollo: {
- labelsApollo: {
+ labels: {
query: boardLabelsQuery,
variables() {
return {
@@ -69,9 +67,6 @@ export default {
update(data) {
return data[this.boardType].labels.nodes;
},
- skip() {
- return !this.isApolloBoard;
- },
error(error) {
setError({
error,
@@ -81,36 +76,22 @@ export default {
},
},
computed: {
- ...mapState(['labels', 'labelsLoading']),
- ...mapGetters(['getListByLabelId']),
- labelsToUse() {
- return this.isApolloBoard ? this.labelsApollo : this.labels;
- },
isLabelsLoading() {
- return this.isApolloBoard ? this.$apollo.queries.labelsApollo.loading : this.labelsLoading;
+ return this.$apollo.queries.labels.loading;
},
columnForSelected() {
- if (this.isApolloBoard) {
- return getListByTypeId(this.lists, ListType.label, this.selectedId);
- }
- return this.getListByLabelId(this.selectedId);
+ return getListByTypeId(this.lists, ListType.label, this.selectedId);
},
items() {
- return (this.labelsToUse || []).map((i) => ({
+ return (this.labels || []).map((i) => ({
...i,
text: i.title,
value: i.id,
}));
},
},
- created() {
- if (!this.isApolloBoard) {
- this.filterItems();
- }
- },
methods: {
- ...mapActions(['createList', 'fetchLabels', 'highlightList']),
- async createListApollo({ labelId }) {
+ async createList({ labelId }) {
try {
await this.$apollo.mutate({
mutation: createListMutations[this.issuableType].mutation,
@@ -156,38 +137,23 @@ export default {
if (this.columnForSelected) {
const listId = this.columnForSelected.id;
- if (this.isApolloBoard) {
- this.$emit('highlight-list', listId);
- } else {
- this.highlightList(listId);
- }
+ this.$emit('highlight-list', listId);
return;
}
- if (this.isApolloBoard) {
- this.createListApollo({ labelId: this.selectedId });
- } else {
- this.createList({ labelId: this.selectedId });
- }
+ this.createList({ labelId: this.selectedId });
this.$emit('setAddColumnFormVisibility', false);
},
- filterItems(searchTerm) {
- this.fetchLabels(searchTerm);
- },
-
onSearch: debounce(function debouncedSearch(searchTerm) {
this.searchTerm = searchTerm;
- if (!this.isApolloBoard) {
- this.filterItems(searchTerm);
- }
}, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
setSelectedItem(selectedId) {
this.selectedId = selectedId;
- const label = this.labelsToUse.find(({ id }) => id === selectedId);
+ const label = this.labels.find(({ id }) => id === selectedId);
if (!selectedId || !label) {
this.selectedLabel = null;
} else {
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 4d915ff341a..2c8aa1cbe21 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -1,6 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters } from 'vuex';
import { omit } from 'lodash';
import { refreshCurrentPage, queryToObject } from '~/lib/utils/url_utility';
import { s__ } from '~/locale';
@@ -33,11 +31,10 @@ export default {
'isGroupBoard',
'issuableType',
'boardType',
- 'isApolloBoard',
],
data() {
return {
- boardListsApollo: {},
+ boardLists: {},
activeListId: '',
boardId: this.initialBoardId,
filterParams: { ...this.initialFilterParams },
@@ -59,20 +56,14 @@ export default {
this.setActiveId('');
}
},
- skip() {
- return !this.isApolloBoard;
- },
},
- boardListsApollo: {
+ boardLists: {
query() {
return listsQuery[this.issuableType].query;
},
variables() {
return this.listQueryVariables;
},
- skip() {
- return !this.isApolloBoard;
- },
update(data) {
const { lists } = data[this.boardType].board;
return formatBoardLists(lists);
@@ -91,7 +82,6 @@ export default {
},
computed: {
- ...mapGetters(['isSidebarOpen']),
listQueryVariables() {
return {
...(this.isIssueBoard && {
@@ -107,13 +97,10 @@ export default {
return (gon?.licensed_features?.swimlanes && this.isShowingEpicsSwimlanes) ?? false;
},
isAnySidebarOpen() {
- if (this.isApolloBoard) {
- return this.activeBoardItem?.id || this.activeListId;
- }
- return this.isSidebarOpen;
+ return this.activeBoardItem?.id || this.activeListId;
},
activeList() {
- return this.activeListId ? this.boardListsApollo[this.activeListId] : undefined;
+ return this.activeListId ? this.boardLists[this.activeListId] : undefined;
},
formattedFilterParams() {
return filterVariables({
@@ -134,7 +121,7 @@ export default {
},
methods: {
refetchLists() {
- this.$apollo.queries.boardListsApollo.refetch();
+ this.$apollo.queries.boardLists.refetch();
},
setActiveId(id) {
this.activeListId = id;
@@ -167,14 +154,14 @@ export default {
:add-column-form-visible="addColumnFormVisible"
:is-swimlanes-on="isSwimlanesOn"
:filter-params="formattedFilterParams"
- :board-lists-apollo="boardListsApollo"
+ :board-lists="boardLists"
:apollo-error="error"
:list-query-variables="listQueryVariables"
@setActiveList="setActiveId"
@setAddColumnFormVisibility="addColumnFormVisible = $event"
/>
<board-settings-sidebar
- v-if="!isApolloBoard || activeList"
+ v-if="activeList"
:list="activeList"
:list-id="activeListId"
:board-id="boardId"
diff --git a/app/assets/javascripts/boards/components/board_card.vue b/app/assets/javascripts/boards/components/board_card.vue
index fd45d2d31c3..6966a4e5d48 100644
--- a/app/assets/javascripts/boards/components/board_card.vue
+++ b/app/assets/javascripts/boards/components/board_card.vue
@@ -1,6 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import setSelectedBoardItemsMutation from '~/boards/graphql/client/set_selected_board_items.mutation.graphql';
import unsetSelectedBoardItemsMutation from '~/boards/graphql/client/unset_selected_board_items.mutation.graphql';
@@ -15,7 +13,7 @@ export default {
BoardCardInner,
},
mixins: [Tracking.mixin()],
- inject: ['disabled', 'isIssueBoard', 'isApolloBoard'],
+ inject: ['disabled', 'isIssueBoard'],
props: {
list: {
type: Object,
@@ -51,18 +49,14 @@ export default {
isIssue: this.isIssueBoard,
};
},
- skip() {
- return !this.isApolloBoard;
- },
},
selectedBoardItems: {
query: selectedBoardItemsQuery,
},
},
computed: {
- ...mapState(['activeId']),
activeItemId() {
- return this.isApolloBoard ? this.activeBoardItem?.id : this.activeId;
+ return this.activeBoardItem?.id;
},
isActive() {
return this.item.id === this.activeItemId;
@@ -86,17 +80,14 @@ export default {
return this.isColorful ? 'gl-pl-4 gl-border-l-solid gl-border-4' : '';
},
formattedItem() {
- return this.isApolloBoard
- ? {
- ...this.item,
- assignees: this.item.assignees?.nodes || [],
- labels: this.item.labels?.nodes || [],
- }
- : this.item;
+ return {
+ ...this.item,
+ assignees: this.item.assignees?.nodes || [],
+ labels: this.item.labels?.nodes || [],
+ };
},
},
methods: {
- ...mapActions(['toggleBoardItem']),
toggleIssue(e) {
// Don't do anything if this happened on a no trigger element
if (e.target.closest('.js-no-trigger')) return;
@@ -105,11 +96,7 @@ export default {
if (isMultiSelect && gon?.features?.boardMultiSelect) {
this.toggleBoardItemMultiSelection(this.item);
} else {
- if (this.isApolloBoard) {
- this.toggleItem();
- } else {
- this.toggleBoardItem({ boardItem: this.item });
- }
+ this.toggleItem();
this.track('click_card', { label: 'right_sidebar' });
}
},
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue
index a7f46dc9325..97dab2e1d34 100644
--- a/app/assets/javascripts/boards/components/board_card_inner.vue
+++ b/app/assets/javascripts/boards/components/board_card_inner.vue
@@ -8,14 +8,11 @@ import {
GlSprintf,
} from '@gitlab/ui';
import { sortBy } from 'lodash';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
import boardCardInner from 'ee_else_ce/boards/mixins/board_card_inner';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { updateHistory } from '~/lib/utils/url_utility';
import { sprintf, __, n__ } from '~/locale';
import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
-import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
@@ -32,7 +29,6 @@ export default {
GlLoadingIcon,
GlIcon,
UserAvatarLink,
- TooltipOnTruncate,
IssueDueDate,
IssueTimeEstimate,
IssueCardWeight: () => import('ee_component/boards/components/issue_card_weight.vue'),
@@ -53,7 +49,6 @@ export default {
'isEpicBoard',
'issuableType',
'isGroupBoard',
- 'isApolloBoard',
],
props: {
item: {
@@ -155,6 +150,9 @@ export default {
const { referencePath } = this.item;
return referencePath.split(this.itemPrefix)[0];
},
+ directNamespaceReference() {
+ return this.itemReferencePath.split('/').slice(-1)[0];
+ },
orderedLabels() {
return sortBy(this.item.labels.filter(this.isNonListLabel), 'title');
},
@@ -186,7 +184,6 @@ export default {
},
},
methods: {
- ...mapActions(['performSearch']),
setError,
isIndexLessThanlimit(index) {
return index < this.limitBeforeCounter;
@@ -224,9 +221,6 @@ export default {
updateHistory({
url: `${filterPath}${filter}`,
});
- if (!this.isApolloBoard) {
- this.performSearch();
- }
eventHub.$emit('updateTokens');
}
},
@@ -308,13 +302,15 @@ export default {
:work-item-type="item.type"
show-tooltip-on-hover
/>
- <tooltip-on-truncate
+ <span
v-if="showReferencePath"
+ v-gl-tooltip
:title="itemReferencePath"
- placement="bottom"
- class="board-item-path gl-text-truncate gl-font-weight-bold"
- >{{ itemReferencePath }}</tooltip-on-truncate
+ data-placement="bottom"
+ class="board-item-path gl-text-truncate gl-font-weight-bold gl-cursor-help"
>
+ {{ directNamespaceReference }}
+ </span>
{{ itemId }}
</span>
<span class="board-info-items gl-mt-3 gl-display-inline-block">
@@ -411,7 +407,7 @@ export default {
</span>
</span>
</div>
- <div class="board-card-assignee gl-display-flex gl-gap-3 gl-mb-n2">
+ <div class="board-card-assignee gl-display-flex gl-mb-n2">
<user-avatar-link
v-for="assignee in cappedAssignees"
:key="assignee.id"
@@ -432,7 +428,7 @@ export default {
v-if="shouldRenderCounter"
v-gl-tooltip
:title="assigneeCounterTooltip"
- class="avatar-counter gl-bg-gray-400 gl-cursor-help gl-font-weight-bold gl-ml-n4 gl-border-0 gl-line-height-24"
+ class="avatar-counter gl-bg-gray-100 gl-text-gray-900 gl-cursor-help gl-font-weight-bold gl-border-0 gl-line-height-24 gl-ml-n3"
data-placement="bottom"
>{{ assigneeCounterLabel }}</span
>
diff --git a/app/assets/javascripts/boards/components/board_card_move_to_position.vue b/app/assets/javascripts/boards/components/board_card_move_to_position.vue
index 8034819732a..9173503c888 100644
--- a/app/assets/javascripts/boards/components/board_card_move_to_position.vue
+++ b/app/assets/javascripts/boards/components/board_card_move_to_position.vue
@@ -1,7 +1,5 @@
<script>
import { GlDisclosureDropdown } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
import Tracking from '~/tracking';
import {
BOARD_CARD_MOVE_TO_POSITIONS_OPTIONS,
@@ -15,7 +13,6 @@ export default {
GlDisclosureDropdown,
},
mixins: [Tracking.mixin()],
- inject: ['isApolloBoard'],
props: {
item: {
type: Object,
@@ -37,7 +34,6 @@ export default {
},
},
computed: {
- ...mapState(['pageInfoByListId']),
tracking() {
return {
category: 'boards:list',
@@ -45,9 +41,6 @@ export default {
property: `type_card`,
};
},
- listHasNextPage() {
- return this.pageInfoByListId[this.list.id]?.hasNextPage;
- },
itemIdentifier() {
return `${this.item.id}-${this.item.iid}-${this.index}`;
},
@@ -59,7 +52,6 @@ export default {
},
},
methods: {
- ...mapActions(['moveItem']),
moveToStart() {
this.track('click_toggle_button', {
label: 'move_to_start',
@@ -85,20 +77,7 @@ export default {
});
},
moveToPosition({ positionInList }) {
- if (this.isApolloBoard) {
- this.$emit('moveToPosition', positionInList);
- } else {
- this.moveItem({
- itemId: this.item.id,
- itemIid: this.item.iid,
- itemPath: this.item.referencePath,
- fromListId: this.list.id,
- toListId: this.list.id,
- positionInList,
- atIndex: this.index,
- allItemsLoadedInList: !this.listHasNextPage,
- });
- }
+ this.$emit('moveToPosition', positionInList);
},
selectMoveAction({ text }) {
if (text === BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION) {
diff --git a/app/assets/javascripts/boards/components/board_column.vue b/app/assets/javascripts/boards/components/board_column.vue
index 67a4c5eba45..0ba8b958428 100644
--- a/app/assets/javascripts/boards/components/board_column.vue
+++ b/app/assets/javascripts/boards/components/board_column.vue
@@ -1,6 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters, mapActions, mapState } from 'vuex';
import BoardListHeader from 'ee_else_ce/boards/components/board_list_header.vue';
import { isListDraggable } from '../boards_util';
import BoardList from './board_list.vue';
@@ -10,7 +8,6 @@ export default {
BoardListHeader,
BoardList,
},
- inject: ['isApolloBoard'],
props: {
list: {
type: Object,
@@ -25,48 +22,21 @@ export default {
type: Object,
required: true,
},
- highlightedListsApollo: {
+ highlightedLists: {
type: Array,
required: false,
default: () => [],
},
},
computed: {
- ...mapState(['filterParams', 'highlightedLists']),
- ...mapGetters(['getBoardItemsByList']),
- highlightedListsToUse() {
- return this.isApolloBoard ? this.highlightedListsApollo : this.highlightedLists;
- },
highlighted() {
- return this.highlightedListsToUse.includes(this.list.id);
- },
- listItems() {
- return this.isApolloBoard ? [] : this.getBoardItemsByList(this.list.id);
+ return this.highlightedLists.includes(this.list.id);
},
isListDraggable() {
return isListDraggable(this.list);
},
- filtersToUse() {
- return this.isApolloBoard ? this.filters : this.filterParams;
- },
},
watch: {
- filterParams: {
- handler() {
- if (!this.isApolloBoard && this.list.id && !this.list.collapsed) {
- this.fetchItemsForList({ listId: this.list.id });
- }
- },
- deep: true,
- immediate: true,
- },
- 'list.id': {
- handler(id) {
- if (!this.isApolloBoard && id) {
- this.fetchItemsForList({ listId: this.list.id });
- }
- },
- },
highlighted: {
handler(highlighted) {
if (highlighted) {
@@ -78,9 +48,6 @@ export default {
immediate: true,
},
},
- methods: {
- ...mapActions(['fetchItemsForList']),
- },
};
</script>
@@ -101,17 +68,11 @@ export default {
>
<board-list-header
:list="list"
- :filter-params="filtersToUse"
+ :filter-params="filters"
:board-id="boardId"
@setActiveList="$emit('setActiveList', $event)"
/>
- <board-list
- ref="board-list"
- :board-id="boardId"
- :board-items="listItems"
- :list="list"
- :filter-params="filtersToUse"
- />
+ <board-list ref="board-list" :board-id="boardId" :list="list" :filter-params="filters" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index a6ff1653c17..2b9c5d52d5e 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -3,11 +3,9 @@ import { GlAlert } from '@gitlab/ui';
import { sortBy } from 'lodash';
import produce from 'immer';
import Draggable from 'vuedraggable';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapActions } from 'vuex';
import BoardAddNewColumn from 'ee_else_ce/boards/components/board_add_new_column.vue';
import { s__ } from '~/locale';
-import { defaultSortableOptions } from '~/sortable/constants';
+import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import {
DraggableItemTypes,
flashAnimationDuration,
@@ -29,15 +27,7 @@ export default {
EpicsSwimlanes: () => import('ee_component/boards/components/epics_swimlanes.vue'),
GlAlert,
},
- inject: [
- 'boardType',
- 'canAdminList',
- 'isIssueBoard',
- 'isEpicBoard',
- 'disabled',
- 'issuableType',
- 'isApolloBoard',
- ],
+ inject: ['boardType', 'canAdminList', 'isIssueBoard', 'isEpicBoard', 'disabled', 'issuableType'],
props: {
boardId: {
type: String,
@@ -51,7 +41,7 @@ export default {
type: Boolean,
required: true,
},
- boardListsApollo: {
+ boardLists: {
type: Object,
required: false,
default: () => {},
@@ -77,12 +67,11 @@ export default {
};
},
computed: {
- ...mapState(['boardLists', 'error']),
boardListsById() {
- return this.isApolloBoard ? this.boardListsApollo : this.boardLists;
+ return this.boardLists;
},
boardListsToUse() {
- const lists = this.isApolloBoard ? this.boardListsApollo : this.boardLists;
+ const lists = this.boardLists;
return sortBy([...Object.values(lists)], 'position');
},
canDragColumns() {
@@ -100,7 +89,7 @@ export default {
group: 'boards-list',
tag: 'div',
value: this.boardListsToUse,
- delay: 100,
+ delay: DRAG_DELAY,
delayOnTouchOnly: true,
filter: 'input',
preventOnFilter: false,
@@ -109,11 +98,10 @@ export default {
return this.canDragColumns ? options : {};
},
errorToDisplay() {
- return this.apolloError || this.error || null;
+ return this.apolloError || null;
},
},
methods: {
- ...mapActions(['moveList', 'unsetError']),
afterFormEnters() {
const el = this.canDragColumns ? this.$refs.list.$el : this.$refs.list;
el.scrollTo({ left: el.scrollWidth, behavior: 'smooth' });
@@ -126,11 +114,7 @@ export default {
}, flashAnimationDuration);
},
dismissError() {
- if (this.isApolloBoard) {
- setError({ message: null, captureError: false });
- } else {
- this.unsetError();
- }
+ setError({ message: null, captureError: false });
},
async updateListPosition({
item: {
@@ -139,17 +123,6 @@ export default {
newIndex,
to: { children },
}) {
- if (!this.isApolloBoard) {
- this.moveList({
- item: {
- dataset: { listId: movedListId, draggableItemType },
- },
- newIndex,
- to: { children },
- });
- return;
- }
-
if (draggableItemType !== DraggableItemTypes.list) {
return;
}
@@ -199,7 +172,7 @@ export default {
__typename: 'UpdateBoardListPayload',
errors: [],
list: {
- ...this.boardListsApollo[movedListId],
+ ...this.boardLists[movedListId],
position: targetPosition,
},
},
@@ -240,7 +213,7 @@ export default {
:board-id="boardId"
:list="list"
:filters="filterParams"
- :highlighted-lists-apollo="highlightedLists"
+ :highlighted-lists="highlightedLists"
:data-draggable-item-type="$options.draggableItemTypes.list"
:class="{ 'gl-display-none! gl-sm-display-inline-block!': addColumnFormVisible }"
@setActiveList="$emit('setActiveList', $event)"
diff --git a/app/assets/javascripts/boards/components/board_content_sidebar.vue b/app/assets/javascripts/boards/components/board_content_sidebar.vue
index bb740c0e7eb..7929c1ad488 100644
--- a/app/assets/javascripts/boards/components/board_content_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_content_sidebar.vue
@@ -1,8 +1,6 @@
<script>
import { GlDrawer } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapActions, mapGetters } from 'vuex';
import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdown_widget.vue';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
@@ -10,7 +8,6 @@ import { __, s__, sprintf } from '~/locale';
import SidebarTimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
import { INCIDENT } from '~/boards/constants';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import SidebarAssigneesWidget from '~/sidebar/components/assignees/sidebar_assignees_widget.vue';
import SidebarConfidentialityWidget from '~/sidebar/components/confidential/sidebar_confidentiality_widget.vue';
@@ -19,6 +16,7 @@ import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severit
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
+import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { setError } from '../graphql/cache_updates';
export default {
@@ -42,6 +40,7 @@ export default {
SidebarWeightWidget: () =>
import('ee_component/sidebar/components/weight/sidebar_weight_widget.vue'),
},
+ mixins: [glFeatureFlagMixin()],
inject: {
multipleAssigneesFeatureAvailable: {
default: false,
@@ -73,9 +72,6 @@ export default {
isGroupBoard: {
default: false,
},
- isApolloBoard: {
- default: false,
- },
timeTrackingLimitToHours: {
default: false,
},
@@ -96,9 +92,6 @@ export default {
assignees: data.activeBoardItem.assignees?.nodes || [],
};
},
- skip() {
- return !this.isApolloBoard;
- },
error(error) {
setError({
error,
@@ -108,10 +101,8 @@ export default {
},
},
computed: {
- ...mapGetters(['activeBoardItem']),
- ...mapState(['sidebarType']),
activeBoardIssuable() {
- return this.isApolloBoard ? this.activeBoardCard : this.activeBoardItem;
+ return this.activeBoardCard;
},
isSidebarOpen() {
return Boolean(this.activeBoardIssuable?.id);
@@ -154,44 +145,25 @@ export default {
const { referencePath = '' } = this.activeBoardIssuable;
return referencePath.slice(0, referencePath.indexOf('#'));
},
+ showWorkItemEpics() {
+ return this.glFeatures.displayWorkItemEpicIssueSidebar;
+ },
+ showEpicSidebarDropdownWidget() {
+ return this.epicFeatureAvailable && !this.isIncidentSidebar && this.activeBoardIssuable.id;
+ },
+ showIterationSidebarDropdownWidget() {
+ return (
+ this.iterationFeatureAvailable && !this.isIncidentSidebar && this.activeBoardIssuable.id
+ );
+ },
},
methods: {
- ...mapActions([
- 'toggleBoardItem',
- 'setAssignees',
- 'setActiveItemConfidential',
- 'setActiveBoardItemLabels',
- 'setActiveItemWeight',
- 'setActiveItemHealthStatus',
- ]),
handleClose() {
- if (this.isApolloBoard) {
- this.$apollo.mutate({
- mutation: setActiveBoardItemMutation,
- variables: {
- boardItem: null,
- },
- });
- } else {
- this.toggleBoardItem({
- boardItem: this.activeBoardIssuable,
- sidebarType: this.sidebarType,
- });
- }
- },
- handleUpdateSelectedLabels({ labels, id }) {
- this.setActiveBoardItemLabels({
- id,
- projectPath: this.projectPathForActiveIssue,
- labelIds: labels.map((label) => getIdFromGraphQLId(label.id)),
- labels,
- });
- },
- handleLabelRemove(removeLabelId) {
- this.setActiveBoardItemLabels({
- iid: this.activeBoardIssuable.iid,
- projectPath: this.projectPathForActiveIssue,
- removeLabelIds: [removeLabelId],
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: {
+ boardItem: null,
+ },
});
},
},
@@ -228,32 +200,36 @@ export default {
:initial-assignees="activeBoardIssuable.assignees"
:allow-multiple-assignees="multipleAssigneesFeatureAvailable"
:editable="canUpdate"
- @assignees-updated="!isApolloBoard && setAssignees($event)"
/>
<sidebar-dropdown-widget
- v-if="epicFeatureAvailable && !isIncidentSidebar"
+ v-if="showEpicSidebarDropdownWidget"
:key="`epic-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="epic"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
+ :issue-id="activeBoardIssuable.id"
+ :show-work-item-epics="showWorkItemEpics"
data-testid="sidebar-epic"
/>
<div>
<sidebar-dropdown-widget
+ v-if="activeBoardIssuable.id"
:key="`milestone-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
issuable-attribute="milestone"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="projectPathForActiveIssue"
:issuable-type="issuableType"
+ :issue-id="activeBoardIssuable.id"
data-testid="sidebar-milestones"
/>
<sidebar-iteration-widget
- v-if="iterationFeatureAvailable && !isIncidentSidebar"
+ v-if="showIterationSidebarDropdownWidget"
:key="`iteration-${activeBoardIssuable.iid}`"
:iid="activeBoardIssuable.iid"
+ :issue-id="activeBoardIssuable.id"
:workspace-path="projectPathForActiveIssue"
:attr-workspace-path="groupPathForActiveIssue"
:issuable-type="issuableType"
@@ -290,8 +266,6 @@ export default {
workspace-type="project"
:issuable-type="issuableType"
:label-create-type="labelType"
- @onLabelRemove="!isApolloBoard && handleLabelRemove($event)"
- @updateSelectedLabels="!isApolloBoard && handleUpdateSelectedLabels($event)"
>
{{ __('None') }}
</sidebar-labels-widget>
@@ -306,20 +280,17 @@ export default {
:iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @weightUpdated="!isApolloBoard && setActiveItemWeight($event)"
/>
<sidebar-health-status-widget
v-if="healthStatusFeatureAvailable"
:iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @statusUpdated="!isApolloBoard && setActiveItemHealthStatus($event)"
/>
<sidebar-confidentiality-widget
:iid="activeBoardIssuable.iid"
:full-path="projectPathForActiveIssue"
:issuable-type="issuableType"
- @confidentialityUpdated="!isApolloBoard && setActiveItemConfidential($event)"
/>
<sidebar-subscriptions-widget
:iid="activeBoardIssuable.iid"
diff --git a/app/assets/javascripts/boards/components/board_filtered_search.vue b/app/assets/javascripts/boards/components/board_filtered_search.vue
index 91dd5c81f77..faaef226c21 100644
--- a/app/assets/javascripts/boards/components/board_filtered_search.vue
+++ b/app/assets/javascripts/boards/components/board_filtered_search.vue
@@ -1,8 +1,6 @@
<script>
import { pickBy, isEmpty, mapValues } from 'lodash';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
-import { getIdFromGraphQLId, isGid, convertToGraphQLId } from '~/graphql_shared/utils';
+import { getIdFromGraphQLId, isGid } from '~/graphql_shared/utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -24,7 +22,6 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import { AssigneeFilterType, GroupByParamType } from 'ee_else_ce/boards/constants';
-import { TYPENAME_ITERATION } from '~/graphql_shared/constants';
import eventHub from '../eventhub';
export default {
@@ -32,7 +29,7 @@ export default {
search: __('Search'),
},
components: { FilteredSearch },
- inject: ['initialFilterParams', 'isApolloBoard'],
+ inject: ['initialFilterParams'],
props: {
isSwimlanesOn: {
type: Boolean,
@@ -342,18 +339,6 @@ export default {
},
);
},
- formattedFilterParams() {
- const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
- const filtersCopy = convertObjectPropsToCamelCase(rawFilterParams, {});
- if (this.filterParams?.iterationId) {
- filtersCopy.iterationId = convertToGraphQLId(
- TYPENAME_ITERATION,
- this.filterParams.iterationId,
- );
- }
-
- return filtersCopy;
- },
},
created() {
eventHub.$on('updateTokens', this.updateTokens);
@@ -366,11 +351,15 @@ export default {
eventHub.$off('updateTokens', this.updateTokens);
},
methods: {
- ...mapActions(['performSearch']),
- updateTokens() {
+ formattedFilterParams() {
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
- this.filterParams = convertObjectPropsToCamelCase(rawFilterParams, {});
- this.$emit('setFilters', this.formattedFilterParams);
+ const filtersCopy = convertObjectPropsToCamelCase(rawFilterParams, {});
+ this.filterParams = filtersCopy;
+
+ return filtersCopy;
+ },
+ updateTokens() {
+ this.$emit('setFilters', this.formattedFilterParams());
this.filteredSearchKey += 1;
},
handleFilter(filters) {
@@ -382,11 +371,7 @@ export default {
replace: true,
});
- if (this.isApolloBoard) {
- this.$emit('setFilters', this.formattedFilterParams);
- } else {
- this.performSearch();
- }
+ this.$emit('setFilters', this.formattedFilterParams());
},
getFilterParams(filters = []) {
const notFilters = filters.filter((item) => item.value.operator === '!=');
diff --git a/app/assets/javascripts/boards/components/board_form.vue b/app/assets/javascripts/boards/components/board_form.vue
index a3d55ac8306..5f4917ea487 100644
--- a/app/assets/javascripts/boards/components/board_form.vue
+++ b/app/assets/javascripts/boards/components/board_form.vue
@@ -1,9 +1,6 @@
<script>
import { GlModal, GlAlert } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { visitUrl, updateHistory, getParameterByName } from '~/lib/utils/url_utility';
+import { visitUrl } from '~/lib/utils/url_utility';
import { __, s__ } from '~/locale';
import eventHub from '~/boards/eventhub';
import { formType } from '../constants';
@@ -61,9 +58,6 @@ export default {
isProjectBoard: {
default: false,
},
- isApolloBoard: {
- default: false,
- },
},
props: {
canAdminBoard: {
@@ -184,7 +178,6 @@ export default {
}
},
methods: {
- ...mapActions(['setBoard']),
setError,
isFocusMode() {
return Boolean(document.querySelector('.content-wrapper > .js-focus-mode-board.is-focused'));
@@ -227,23 +220,12 @@ export default {
} else {
try {
const board = await this.createOrUpdateBoard();
- if (this.isApolloBoard) {
- if (this.board.id) {
- eventHub.$emit('updateBoard', board);
- } else {
- this.$emit('addBoard', board);
- }
+ if (this.board.id) {
+ eventHub.$emit('updateBoard', board);
} else {
- this.setBoard(board);
+ this.$emit('addBoard', board);
}
this.cancel();
-
- if (!this.isApolloBoard) {
- const param = getParameterByName('group_by')
- ? `?group_by=${getParameterByName('group_by')}`
- : '';
- updateHistory({ url: `${this.boardBaseUrl}/${getIdFromGraphQLId(board.id)}${param}` });
- }
} catch (error) {
setError({ error, message: this.$options.i18n.saveErrorMessage });
} finally {
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index ca10cbbad5e..8a5c6882e56 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,11 +1,9 @@
<script>
import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
import { STATUS_CLOSED } from '~/issues/constants';
import { sprintf, __, s__ } from '~/locale';
-import { defaultSortableOptions } from '~/sortable/constants';
+import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
import listQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
@@ -57,7 +55,6 @@ export default {
'fullPath',
'boardType',
'issuableType',
- 'isApolloBoard',
],
props: {
list: {
@@ -68,10 +65,6 @@ export default {
type: String,
required: true,
},
- boardItems: {
- type: Array,
- required: true,
- },
filterParams: {
type: Object,
required: true,
@@ -115,7 +108,7 @@ export default {
};
},
skip() {
- return !this.isApolloBoard || this.list.collapsed;
+ return this.list.collapsed;
},
update(data) {
return data[this.boardType].board.lists.nodes[0];
@@ -157,11 +150,8 @@ export default {
},
},
computed: {
- ...mapState(['pageInfoByListId', 'listsFlags', 'isUpdateIssueOrderInProgress']),
boardListItems() {
- return this.isApolloBoard
- ? this.currentList?.[`${this.issuableType}s`].nodes || []
- : this.boardItems;
+ return this.currentList?.[`${this.issuableType}s`].nodes || [];
},
listQueryVariables() {
return {
@@ -190,17 +180,10 @@ export default {
return this.list.maxIssueCount > 0 && this.listItemsCount > this.list.maxIssueCount;
},
hasNextPage() {
- return this.isApolloBoard
- ? this.currentList?.[`${this.issuableType}s`].pageInfo?.hasNextPage
- : this.pageInfoByListId[this.list.id]?.hasNextPage;
+ return this.currentList?.[`${this.issuableType}s`].pageInfo?.hasNextPage;
},
loading() {
- return this.isApolloBoard
- ? this.$apollo.queries.currentList.loading && !this.isLoadingMore
- : this.listsFlags[this.list.id]?.isLoading;
- },
- loadingMore() {
- return this.isApolloBoard ? this.isLoadingMore : this.listsFlags[this.list.id]?.isLoadingMore;
+ return this.$apollo.queries.currentList.loading && !this.isLoadingMore;
},
epicCreateFormVisible() {
return this.isEpicBoard && this.list.listType !== STATUS_CLOSED && this.showEpicForm;
@@ -224,10 +207,7 @@ export default {
return !this.disabled;
},
treeRootWrapper() {
- return this.canMoveIssue &&
- (!this.listsFlags[this.list.id]?.addItemToListInProgress || this.addItemToListInProgress)
- ? Draggable
- : 'ul';
+ return this.canMoveIssue && !this.addItemToListInProgress ? Draggable : 'ul';
},
treeRootOptions() {
const options = {
@@ -238,16 +218,14 @@ export default {
'ghost-class': 'board-card-drag-active',
'data-list-id': this.list.id,
value: this.boardListItems,
- delay: 100,
+ delay: DRAG_DELAY,
delayOnTouchOnly: true,
};
return this.canMoveIssue ? options : {};
},
disableScrollingWhenMutationInProgress() {
- return (
- this.hasNextPage && (this.isUpdateIssueOrderInProgress || this.updateIssueOrderInProgress)
- );
+ return this.hasNextPage && this.updateIssueOrderInProgress;
},
showMoveToPosition() {
return !this.disabled && this.list.listType !== ListType.closed;
@@ -280,7 +258,6 @@ export default {
eventHub.$off(`scroll-board-list-${this.list.id}`, this.scrollToTop);
},
methods: {
- ...mapActions(['fetchItemsForList', 'moveItem']),
listHeight() {
return this.listRef?.getBoundingClientRect()?.height || 0;
},
@@ -294,19 +271,15 @@ export default {
this.listRef.scrollTop = 0;
},
async loadNextPage() {
- if (this.isApolloBoard) {
- this.isLoadingMore = true;
- await this.$apollo.queries.currentList.fetchMore({
- variables: {
- ...this.listQueryVariables,
- id: this.list.id,
- after: this.currentList?.[`${this.issuableType}s`].pageInfo.endCursor,
- },
- });
- this.isLoadingMore = false;
- } else {
- this.fetchItemsForList({ listId: this.list.id, fetchNext: true });
- }
+ this.isLoadingMore = true;
+ await this.$apollo.queries.currentList.fetchMore({
+ variables: {
+ ...this.listQueryVariables,
+ id: this.list.id,
+ after: this.currentList?.[`${this.issuableType}s`].pageInfo.endCursor,
+ },
+ });
+ this.isLoadingMore = false;
},
toggleForm() {
if (this.isEpicBoard) {
@@ -320,7 +293,7 @@ export default {
return index !== 0 && index % 6 === 0;
},
onReachingListBottom() {
- if (!this.loadingMore && this.hasNextPage) {
+ if (!this.isLoadingMore && this.hasNextPage) {
this.showCount = true;
this.loadNextPage();
}
@@ -343,7 +316,7 @@ export default {
from,
to,
item: {
- dataset: { draggableItemType, itemId, itemIid, itemPath },
+ dataset: { draggableItemType, itemId, itemIid },
},
}) {
if (draggableItemType !== DraggableItemTypes.card) {
@@ -387,32 +360,20 @@ export default {
}
}
- if (this.isApolloBoard) {
- this.updateIssueOrderInProgress = true;
- await this.moveBoardItem(
- {
- itemId,
- iid: itemIid,
- fromListId: from.dataset.listId,
- toListId: to.dataset.listId,
- moveBeforeId,
- moveAfterId,
- },
- newIndex,
- ).finally(() => {
- this.updateIssueOrderInProgress = false;
- });
- } else {
- this.moveItem({
+ this.updateIssueOrderInProgress = true;
+ await this.moveBoardItem(
+ {
itemId,
- itemIid,
- itemPath,
+ iid: itemIid,
fromListId: from.dataset.listId,
toListId: to.dataset.listId,
moveBeforeId,
moveAfterId,
- });
- }
+ },
+ newIndex,
+ ).finally(() => {
+ this.updateIssueOrderInProgress = false;
+ });
},
isItemInTheList(itemIid) {
const items = this.toList?.[`${this.issuableType}s`]?.nodes || [];
@@ -718,7 +679,7 @@ export default {
data-issue-id="-1"
>
<gl-loading-icon
- v-if="loadingMore"
+ v-if="isLoadingMore"
size="sm"
:label="$options.i18n.loadingMoreBoardItems"
/>
diff --git a/app/assets/javascripts/boards/components/board_list_header.vue b/app/assets/javascripts/boards/components/board_list_header.vue
index bedb3a75a70..f50c510fcf6 100644
--- a/app/assets/javascripts/boards/components/board_list_header.vue
+++ b/app/assets/javascripts/boards/components/board_list_header.vue
@@ -8,14 +8,11 @@ import {
GlSprintf,
GlTooltipDirective,
} from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
import { isListDraggable } from '~/boards/boards_util';
import { isScopedLabel, parseBoolean } from '~/lib/utils/common_utils';
import { fetchPolicies } from '~/lib/graphql';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { n__, s__ } from '~/locale';
-import sidebarEventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import { TYPE_ISSUE } from '~/issues/constants';
import { formatDate } from '~/lib/utils/datetime_utility';
@@ -23,8 +20,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import setActiveBoardItemMutation from 'ee_else_ce/boards/graphql/client/set_active_board_item.mutation.graphql';
import AccessorUtilities from '~/lib/utils/accessor';
import {
- inactiveId,
- LIST,
ListType,
toggleFormEventPrefix,
updateListQueries,
@@ -81,9 +76,6 @@ export default {
issuableType: {
default: TYPE_ISSUE,
},
- isApolloBoard: {
- default: false,
- },
},
props: {
list: {
@@ -106,7 +98,6 @@ export default {
},
},
computed: {
- ...mapState(['activeId']),
isLoggedIn() {
return Boolean(this.currentUserId);
},
@@ -238,21 +229,12 @@ export default {
}
},
methods: {
- ...mapActions(['updateList', 'setActiveId', 'toggleListCollapsed']),
openSidebarSettings() {
- if (this.activeId === inactiveId) {
- sidebarEventHub.$emit('sidebar.closeAll');
- }
-
- if (this.isApolloBoard) {
- this.$apollo.mutate({
- mutation: setActiveBoardItemMutation,
- variables: { boardItem: null },
- });
- this.$emit('setActiveList', this.list.id);
- } else {
- this.setActiveId({ id: this.list.id, sidebarType: LIST });
- }
+ this.$apollo.mutate({
+ mutation: setActiveBoardItemMutation,
+ variables: { boardItem: null },
+ });
+ this.$emit('setActiveList', this.list.id);
this.track('click_button', { label: 'list_settings' });
},
@@ -297,33 +279,29 @@ export default {
}
},
async updateListFunction(collapsed) {
- if (this.isApolloBoard) {
- try {
- await this.$apollo.mutate({
- mutation: updateListQueries[this.issuableType].mutation,
- variables: {
- listId: this.list.id,
- collapsed,
- },
- optimisticResponse: {
- updateBoardList: {
- __typename: 'UpdateBoardListPayload',
- errors: [],
- list: {
- ...this.list,
- collapsed,
- },
+ try {
+ await this.$apollo.mutate({
+ mutation: updateListQueries[this.issuableType].mutation,
+ variables: {
+ listId: this.list.id,
+ collapsed,
+ },
+ optimisticResponse: {
+ updateBoardList: {
+ __typename: 'UpdateBoardListPayload',
+ errors: [],
+ list: {
+ ...this.list,
+ collapsed,
},
},
- });
- } catch (error) {
- setError({
- error,
- message: s__('Boards|An error occurred while updating the list. Please try again.'),
- });
- }
- } else {
- this.updateList({ listId: this.list.id, collapsed });
+ },
+ });
+ } catch (error) {
+ setError({
+ error,
+ message: s__('Boards|An error occurred while updating the list. Please try again.'),
+ });
}
},
/**
@@ -337,17 +315,13 @@ export default {
return `${start} - ${due}`;
},
updateLocalCollapsedStatus(collapsed) {
- if (this.isApolloBoard) {
- this.$apollo.mutate({
- mutation: toggleCollapsedMutations[this.issuableType].mutation,
- variables: {
- list: this.list,
- collapsed,
- },
- });
- } else {
- this.toggleListCollapsed({ listId: this.list.id, collapsed });
- }
+ this.$apollo.mutate({
+ mutation: toggleCollapsedMutations[this.issuableType].mutation,
+ variables: {
+ list: this.list,
+ collapsed,
+ },
+ });
},
},
};
diff --git a/app/assets/javascripts/boards/components/board_new_issue.vue b/app/assets/javascripts/boards/components/board_new_issue.vue
index d78b60e91a8..ea22bb08f2a 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.vue
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,6 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import { getMilestone, formatIssueInput, getBoardQuery } from 'ee_else_ce/boards/boards_util';
import BoardNewIssueMixin from 'ee_else_ce/boards/mixins/board_new_issue';
@@ -22,7 +20,7 @@ export default {
ProjectSelect,
},
mixins: [BoardNewIssueMixin],
- inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard', 'isApolloBoard'],
+ inject: ['boardType', 'groupId', 'fullPath', 'isGroupBoard', 'isEpicBoard'],
props: {
list: {
type: Object,
@@ -50,9 +48,6 @@ export default {
boardId: this.boardId,
};
},
- skip() {
- return !this.isApolloBoard;
- },
update(data) {
const { board } = data.workspace;
return {
@@ -69,7 +64,6 @@ export default {
},
},
computed: {
- ...mapGetters(['getBoardItemsByList']),
formEventPrefix() {
return toggleFormEventPrefix.issue;
},
@@ -81,37 +75,19 @@ export default {
},
},
methods: {
- ...mapActions(['addListNewIssue']),
submit({ title }) {
const labels = this.list.label ? [this.list.label] : [];
const assignees = this.list.assignee ? [this.list.assignee] : [];
const milestone = getMilestone(this.list);
- if (this.isApolloBoard) {
- return this.addNewIssueToList({
- issueInput: {
- title,
- labelIds: labels?.map((l) => l.id),
- assigneeIds: assignees?.map((a) => a?.id),
- milestoneId: milestone?.id,
- projectPath: this.projectPath,
- },
- });
- }
-
- const firstItemId = this.getBoardItemsByList(this.list.id)[0]?.id;
- return this.addListNewIssue({
- list: this.list,
+ return this.addNewIssueToList({
issueInput: {
title,
labelIds: labels?.map((l) => l.id),
assigneeIds: assignees?.map((a) => a?.id),
milestoneId: milestone?.id,
projectPath: this.projectPath,
- moveAfterId: firstItemId,
},
- }).then(() => {
- this.cancel();
});
},
addNewIssueToList({ issueInput }) {
diff --git a/app/assets/javascripts/boards/components/board_settings_sidebar.vue b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
index 89e13625210..7e8f0ffdc60 100644
--- a/app/assets/javascripts/boards/components/board_settings_sidebar.vue
+++ b/app/assets/javascripts/boards/components/board_settings_sidebar.vue
@@ -2,10 +2,7 @@
import produce from 'immer';
import { GlButton, GlDrawer, GlLabel, GlModal, GlModalDirective } from '@gitlab/ui';
import { MountingPortal } from 'portal-vue';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState, mapGetters } from 'vuex';
import {
- LIST,
ListType,
ListTypeTitles,
listsQuery,
@@ -13,7 +10,6 @@ import {
} from 'ee_else_ce/boards/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { __, s__ } from '~/locale';
-import eventHub from '~/sidebar/event_hub';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { setError } from '../graphql/cache_updates';
@@ -40,14 +36,7 @@ export default {
GlModal: GlModalDirective,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: [
- 'boardType',
- 'canAdminList',
- 'issuableType',
- 'scopedLabelsAvailable',
- 'isIssueBoard',
- 'isApolloBoard',
- ],
+ inject: ['boardType', 'canAdminList', 'issuableType', 'scopedLabelsAvailable', 'isIssueBoard'],
inheritAttrs: false,
props: {
listId: {
@@ -61,7 +50,7 @@ export default {
list: {
type: Object,
required: false,
- default: () => null,
+ default: () => {},
},
queryVariables: {
type: Object,
@@ -75,16 +64,14 @@ export default {
},
modalId: 'board-settings-sidebar-modal',
computed: {
- ...mapGetters(['isSidebarOpen']),
- ...mapState(['activeId', 'sidebarType', 'boardLists']),
isWipLimitsOn() {
return this.glFeatures.wipLimits && this.isIssueBoard;
},
activeListId() {
- return this.isApolloBoard ? this.listId : this.activeId;
+ return this.listId;
},
activeList() {
- return (this.isApolloBoard ? this.list : this.boardLists[this.activeId]) || {};
+ return this.list;
},
activeListLabel() {
return this.activeList.label;
@@ -96,20 +83,10 @@ export default {
return ListTypeTitles[ListType.label];
},
showSidebar() {
- if (this.isApolloBoard) {
- return Boolean(this.listId);
- }
- return this.sidebarType === LIST && this.isSidebarOpen;
+ return Boolean(this.listId);
},
},
- created() {
- eventHub.$on('sidebar.closeAll', this.unsetActiveListId);
- },
- beforeDestroy() {
- eventHub.$off('sidebar.closeAll', this.unsetActiveListId);
- },
methods: {
- ...mapActions(['unsetActiveId', 'removeList']),
handleModalPrimary() {
this.deleteBoardList();
},
@@ -118,19 +95,11 @@ export default {
},
deleteBoardList() {
this.track('click_button', { label: 'remove_list' });
- if (this.isApolloBoard) {
- this.deleteList(this.activeListId);
- } else {
- this.removeList(this.activeId);
- }
+ this.deleteList(this.activeListId);
this.unsetActiveListId();
},
unsetActiveListId() {
- if (this.isApolloBoard) {
- this.$emit('unsetActiveId');
- } else {
- this.unsetActiveId();
- }
+ this.$emit('unsetActiveId');
},
async deleteList(listId) {
try {
diff --git a/app/assets/javascripts/boards/components/board_top_bar.vue b/app/assets/javascripts/boards/components/board_top_bar.vue
index 31664c28831..d2be511343d 100644
--- a/app/assets/javascripts/boards/components/board_top_bar.vue
+++ b/app/assets/javascripts/boards/components/board_top_bar.vue
@@ -31,7 +31,6 @@ export default {
'fullPath',
'boardType',
'isEpicBoard',
- 'isApolloBoard',
],
props: {
boardId: {
@@ -63,9 +62,6 @@ export default {
boardId: this.boardId,
};
},
- skip() {
- return !this.isApolloBoard;
- },
update(data) {
const { board } = data.workspace;
return {
@@ -110,7 +106,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-flex-grow-1 gl-lg-mb-0 gl-mb-3 gl-w-full gl-min-w-0"
>
<boards-selector
- :board-apollo="board"
+ :board="board"
:is-current-board-loading="isLoading"
@switchBoard="$emit('switchBoard', $event)"
/>
diff --git a/app/assets/javascripts/boards/components/boards_selector.vue b/app/assets/javascripts/boards/components/boards_selector.vue
index cd2a4a02b2e..e9ff390c488 100644
--- a/app/assets/javascripts/boards/components/boards_selector.vue
+++ b/app/assets/javascripts/boards/components/boards_selector.vue
@@ -2,8 +2,6 @@
import { GlButton, GlCollapsibleListbox, GlModalDirective } from '@gitlab/ui';
import { produce } from 'immer';
import { differenceBy, debounce } from 'lodash';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
import BoardForm from 'ee_else_ce/boards/components/board_form.vue';
@@ -51,10 +49,9 @@ export default {
'weights',
'boardType',
'isGroupBoard',
- 'isApolloBoard',
],
props: {
- boardApollo: {
+ board: {
type: Object,
required: false,
default: () => ({}),
@@ -79,18 +76,11 @@ export default {
},
computed: {
- ...mapState(['board', 'isBoardLoading']),
- boardToUse() {
- return this.isApolloBoard ? this.boardApollo : this.board;
+ boardName() {
+ return this.board?.name || s__('IssueBoards|Select board');
},
- boardToUseName() {
- return this.boardToUse?.name || s__('IssueBoards|Select board');
- },
- boardToUseId() {
- return getIdFromGraphQLId(this.boardToUse.id) || '';
- },
- isBoardToUseLoading() {
- return this.isApolloBoard ? this.isCurrentBoardLoading : this.isBoardLoading;
+ boardId() {
+ return getIdFromGraphQLId(this.board.id) || '';
},
parentType() {
return this.boardType;
@@ -147,7 +137,7 @@ export default {
},
},
watch: {
- boardToUse(newBoard) {
+ board(newBoard) {
document.title = newBoard.name;
},
},
@@ -162,7 +152,6 @@ export default {
eventHub.$off('showBoardModal', this.showPage);
},
methods: {
- ...mapActions(['fetchBoard', 'unsetActiveId']),
fullBoardId(boardId) {
return fullBoardId(boardId);
},
@@ -251,13 +240,6 @@ export default {
this.$emit('switchBoard', board.id);
},
- fetchCurrentBoard(boardId) {
- this.fetchBoard({
- fullPath: this.fullPath,
- fullBoardId: fullBoardId(boardId),
- boardType: this.boardType,
- });
- },
setFilterTerm(value) {
this.filterTerm = value;
},
@@ -268,15 +250,9 @@ export default {
}
},
switchBoardGroup(value) {
- if (this.isApolloBoard) {
- // Epic board ID is supported in EE version of this file
- this.$emit('switchBoard', this.fullBoardId(value));
- updateHistory({ url: `${this.boardBaseUrl}/${value}` });
- } else {
- this.unsetActiveId();
- this.fetchCurrentBoard(value);
- updateHistory({ url: `${this.boardBaseUrl}/${value}` });
- }
+ // Epic board ID is supported in EE version of this file
+ this.$emit('switchBoard', this.fullBoardId(value));
+ updateHistory({ url: `${this.boardBaseUrl}/${value}` });
},
},
};
@@ -294,10 +270,10 @@ export default {
toggle-class="gl-min-w-20"
:header-text="$options.i18n.headerText"
:no-results-text="$options.i18n.noResultsText"
- :loading="isBoardToUseLoading"
+ :loading="isCurrentBoardLoading"
:items="listBoxItems"
- :toggle-text="boardToUseName"
- :selected="boardToUseId"
+ :toggle-text="boardName"
+ :selected="boardId"
@search="handleSearch"
@select="switchBoardGroup"
@shown="loadBoards"
@@ -350,7 +326,7 @@ export default {
:can-admin-board="canAdminBoard"
:scoped-issue-board-feature-enabled="scopedIssueBoardFeatureEnabled"
:weights="weights"
- :current-board="boardToUse"
+ :current-board="board"
:current-page="currentPage"
@addBoard="addBoard"
@cancel="cancel"
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 a2c4b42b6c5..f86bab40c93 100644
--- a/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
+++ b/app/assets/javascripts/boards/components/sidebar/board_sidebar_title.vue
@@ -1,7 +1,5 @@
<script>
import { GlAlert, GlButton, GlForm, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters, mapActions } from 'vuex';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
import { joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -22,7 +20,7 @@ export default {
directives: {
autofocusonshow,
},
- inject: ['fullPath', 'issuableType', 'isEpicBoard', 'isApolloBoard'],
+ inject: ['fullPath', 'issuableType', 'isEpicBoard'],
props: {
activeItem: {
type: Object,
@@ -37,15 +35,11 @@ export default {
};
},
computed: {
- ...mapGetters(['activeBoardItem']),
- item() {
- return this.isApolloBoard ? this.activeItem : this.activeBoardItem;
- },
pendingChangesStorageKey() {
- return this.getPendingChangesKey(this.item);
+ return this.getPendingChangesKey(this.activeItem);
},
projectPath() {
- const referencePath = this.item.referencePath || '';
+ const referencePath = this.activeItem.referencePath || '';
return referencePath.slice(0, referencePath.indexOf('#'));
},
validationState() {
@@ -53,7 +47,7 @@ export default {
},
},
watch: {
- item: {
+ activeItem: {
handler(updatedItem, formerItem) {
if (formerItem?.title !== this.title) {
localStorage.setItem(this.getPendingChangesKey(formerItem), this.title);
@@ -66,7 +60,6 @@ export default {
},
},
methods: {
- ...mapActions(['setActiveItemTitle']),
getPendingChangesKey(item) {
if (!item) {
return '';
@@ -92,16 +85,12 @@ export default {
}
},
cancel() {
- this.title = this.item.title;
+ this.title = this.activeItem.title;
this.$refs.sidebarItem.collapse();
this.showChangesAlert = false;
localStorage.removeItem(this.pendingChangesStorageKey);
},
async setActiveBoardItemTitle() {
- if (!this.isApolloBoard) {
- await this.setActiveItemTitle({ title: this.title, projectPath: this.projectPath });
- return;
- }
const { fullPath, issuableType, isEpicBoard, title } = this;
const workspacePath = isEpicBoard
? { groupPath: fullPath }
@@ -111,7 +100,7 @@ export default {
variables: {
input: {
...workspacePath,
- iid: String(this.item.iid),
+ iid: String(this.activeItem.iid),
title,
},
},
@@ -120,7 +109,7 @@ export default {
async setTitle() {
this.$refs.sidebarItem.collapse();
- if (!this.title || this.title === this.item.title) {
+ if (!this.title || this.title === this.activeItem.title) {
return;
}
@@ -130,14 +119,14 @@ export default {
localStorage.removeItem(this.pendingChangesStorageKey);
this.showChangesAlert = false;
} catch (e) {
- this.title = this.item.title;
+ this.title = this.activeItem.title;
setError({ error: e, message: this.$options.i18n.updateTitleError });
} finally {
this.loading = false;
}
},
handleOffClick() {
- if (this.title !== this.item.title) {
+ if (this.title !== this.activeItem.title) {
this.showChangesAlert = true;
localStorage.setItem(this.pendingChangesStorageKey, this.title);
} else {
@@ -166,13 +155,13 @@ export default {
>
<template #title>
<span data-testid="item-title">
- <gl-link class="gl-reset-color gl-hover-text-blue-800" :href="item.webUrl">
- {{ item.title }}
+ <gl-link class="gl-reset-color gl-hover-text-blue-800" :href="activeItem.webUrl">
+ {{ activeItem.title }}
</gl-link>
</span>
</template>
<template #collapsed>
- <span class="gl-text-gray-800">{{ item.referencePath }}</span>
+ <span class="gl-text-gray-800">{{ activeItem.referencePath }}</span>
</template>
<gl-alert v-if="showChangesAlert" variant="warning" class="gl-mb-5" :dismissible="false">
{{ $options.i18n.reviewYourChanges }}
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 9d7b7a38c6d..72b8aef31a4 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -24,7 +24,7 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
- const isApolloBoard = window.gon?.features?.apolloBoards;
+ const isApolloBoard = true;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index 113840dbc52..0dacd5af5cc 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -23,11 +23,11 @@ export default () => {
topLevelLinks.forEach((el) => addTooltipToEl(el));
$expanderBtn.on('click', () => {
- const detailItems = $('.breadcrumbs-detail-item');
+ const detailItems = $('.gl-breadcrumb-item');
const hiddenClass = 'gl-display-none!';
$.each(detailItems, (_key, item) => {
- $(item).toggleClass(hiddenClass);
+ $(item).removeClass(hiddenClass);
});
// remove the ellipsis
diff --git a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
index de37aa431e6..3a0fd376d3c 100644
--- a/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
+++ b/app/assets/javascripts/ci/artifacts/components/job_artifacts_table.vue
@@ -10,7 +10,7 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -40,7 +40,6 @@ import {
I18N_BULK_DELETE_ERROR,
I18N_BULK_DELETE_PARTIAL_ERROR,
I18N_BULK_DELETE_CONFIRMATION_TOAST,
- SELECTED_ARTIFACTS_MAX_COUNT,
I18N_BULK_DELETE_MAX_SELECTED,
} from '../constants';
import JobCheckbox from './job_checkbox.vue';
@@ -77,7 +76,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- inject: ['projectId', 'projectPath', 'canDestroyArtifacts'],
+ inject: ['projectId', 'projectPath', 'canDestroyArtifacts', 'jobArtifactsCountLimit'],
apollo: {
jobArtifacts: {
query: getJobArtifactsQuery,
@@ -151,7 +150,7 @@ export default {
return Boolean(this.selectedArtifacts.length);
},
isSelectedArtifactsLimitReached() {
- return this.selectedArtifacts.length >= SELECTED_ARTIFACTS_MAX_COUNT;
+ return this.selectedArtifacts.length >= this.jobArtifactsCountLimit;
},
canBulkDestroyArtifacts() {
return this.canDestroyArtifacts;
diff --git a/app/assets/javascripts/ci/artifacts/constants.js b/app/assets/javascripts/ci/artifacts/constants.js
index 28c371cda1e..166946035d1 100644
--- a/app/assets/javascripts/ci/artifacts/constants.js
+++ b/app/assets/javascripts/ci/artifacts/constants.js
@@ -47,7 +47,6 @@ export const I18N_MODAL_BODY = s__(
export const I18N_MODAL_PRIMARY = s__('Artifacts|Delete artifact');
export const I18N_MODAL_CANCEL = __('Cancel');
-export const SELECTED_ARTIFACTS_MAX_COUNT = 50;
export const I18N_BULK_DELETE_MAX_SELECTED = s__(
'Artifacts|Maximum selected artifacts limit reached',
);
diff --git a/app/assets/javascripts/ci/artifacts/index.js b/app/assets/javascripts/ci/artifacts/index.js
index c6021eb056f..0a84b94f5fa 100644
--- a/app/assets/javascripts/ci/artifacts/index.js
+++ b/app/assets/javascripts/ci/artifacts/index.js
@@ -19,7 +19,7 @@ export const initArtifactsTable = () => {
return false;
}
- const { projectPath, projectId, canDestroyArtifacts } = el.dataset;
+ const { projectPath, projectId, canDestroyArtifacts, jobArtifactsCountLimit } = el.dataset;
return new Vue({
el,
@@ -28,6 +28,7 @@ export const initArtifactsTable = () => {
projectPath,
projectId,
canDestroyArtifacts: parseBoolean(canDestroyArtifacts),
+ jobArtifactsCountLimit: parseInt(jobArtifactsCountLimit, 10),
},
render: (createElement) => createElement(App),
});
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
index 572a8183730..349ce761d25 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_about.vue
@@ -101,7 +101,7 @@ export default {
<span
v-for="item in projectInfoItems"
:key="`${item.icon}`"
- class="gl-display-flex gl-align-items-center gl-xs-mb-3"
+ class="gl-display-flex gl-align-items-center gl-mb-3 gl-sm-mb-0"
>
<gl-icon class="gl-text-primary gl-mr-2" :name="item.icon" />
<div
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
index fbc7ddf5c91..6d062d8b7f1 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue
@@ -12,7 +12,7 @@ export default {
GlTableLite,
},
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -27,11 +27,11 @@ export default {
query: getCiCatalogResourceComponents,
variables() {
return {
- id: this.resourceId,
+ fullPath: this.resourcePath,
};
},
update(data) {
- return data?.ciCatalogResource?.components?.nodes || [];
+ return data?.ciCatalogResource?.latestVersion?.components?.nodes || [];
},
error() {
createAlert({ message: this.$options.i18n.fetchError });
@@ -64,7 +64,7 @@ export default {
thClass: 'gl-w-40p',
},
{
- key: 'defaultValue',
+ key: 'default',
label: s__('CiCatalogComponent|Default Value'),
thClass: 'gl-w-40p',
},
@@ -103,7 +103,6 @@ export default {
data-testid="component-section"
>
<h3 class="gl-font-size-h2" data-testid="component-name">{{ component.name }}</h3>
- <p class="gl-mt-5">{{ component.description }}</p>
<div class="gl-display-flex">
<pre
class="gl-w-85p gl-py-4 gl-display-flex gl-justify-content-space-between gl-m-0 gl-border-r-none"
@@ -124,7 +123,7 @@ export default {
</div>
<div class="gl-mt-5">
<b class="gl-display-block gl-mb-4"> {{ $options.i18n.inputTitle }}</b>
- <gl-table-lite :items="component.inputs.nodes" :fields="$options.fields">
+ <gl-table-lite :items="component.inputs" :fields="$options.fields">
<template #cell(required)="{ item }">
{{ humanizeBoolean(item.required) }}
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
index 026a30988fd..b1170b13ef6 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_details.vue
@@ -14,7 +14,7 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -31,10 +31,10 @@ export default {
<template>
<gl-tabs>
<gl-tab :title="$options.i18n.tabs.readme" lazy>
- <ci-resource-readme :resource-id="resourceId" />
+ <ci-resource-readme :resource-path="resourcePath" />
</gl-tab>
<gl-tab v-if="glFeatures.ciCatalogComponentsTab" :title="$options.i18n.tabs.components" lazy>
- <ci-resource-components :resource-id="resourceId"
+ <ci-resource-components :resource-path="resourcePath"
/></gl-tab>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
index 29009c14e1b..b9d6173a777 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue
@@ -1,8 +1,8 @@
<script>
import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { isNumeric } from '~/lib/utils/number_utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import CiResourceAbout from './ci_resource_about.vue';
import CiResourceHeaderSkeletonLoader from './ci_resource_header_skeleton_loader.vue';
@@ -48,9 +48,6 @@ export default {
entityId() {
return getIdFromGraphQLId(this.resource.id);
},
- fullPath() {
- return `${this.rootNamespace.fullPath}/${this.rootNamespace.name}`;
- },
hasLatestVersion() {
return this.latestVersion?.tagName;
},
@@ -60,13 +57,11 @@ export default {
latestVersion() {
return this.resource.latestVersion;
},
- rootNamespace() {
- return this.resource.rootNamespace;
- },
versionBadgeText() {
- return isNumeric(this.latestVersion.tagName)
- ? `v${this.latestVersion.tagName}`
- : this.latestVersion.tagName;
+ return this.latestVersion.tagName;
+ },
+ webPath() {
+ return cleanLeadingSeparator(this.resource?.webPath);
},
},
};
@@ -89,7 +84,7 @@ export default {
class="gl-display-flex gl-flex-direction-column gl-align-items-flex-start gl-justify-content-center"
>
<div class="gl-font-sm gl-text-secondary">
- {{ fullPath }}
+ {{ webPath }}
</div>
<span class="gl-display-flex">
<div class="gl-font-lg gl-font-weight-bold">{{ resource.name }}</div>
diff --git a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
index d473833869d..343b555c4d8 100644
--- a/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
+++ b/app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue
@@ -11,7 +11,7 @@ export default {
},
directives: { SafeHtml },
props: {
- resourceId: {
+ resourcePath: {
type: String,
required: true,
},
@@ -26,7 +26,7 @@ export default {
query: getCiCatalogResourceReadme,
variables() {
return {
- id: this.resourceId,
+ fullPath: this.resourcePath,
};
},
update(data) {
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
index db84eaa82c2..3a9ec341789 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -2,15 +2,17 @@
import { GlBanner, GlLink } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import { CATALOG_FEEDBACK_DISMISSED_KEY } from '../../constants';
const defaultTitle = __('CI/CD Catalog');
const defaultDescription = s__(
- 'CiCatalog|Discover CI configuration resources for a seamless CI/CD experience.',
+ 'CiCatalog|Discover CI/CD components that can improve your pipeline with additional functionality.',
);
export default {
components: {
+ BetaBadge,
GlBanner,
GlLink,
},
@@ -45,7 +47,7 @@ export default {
};
</script>
<template>
- <div class="gl-border-b-1 gl-border-gray-100 gl-border-b-solid">
+ <div class="page-title-holder">
<gl-banner
v-if="!isFeedbackBannerDismissed"
class="gl-mt-5"
@@ -58,9 +60,12 @@ export default {
{{ $options.i18n.banner.description }}
</p>
</gl-banner>
- <h1 class="gl-font-size-h-display">{{ pageTitle }}</h1>
+ <div class="gl-my-4 gl-display-flex gl-align-items-center">
+ <h1 class="gl-m-0 gl-font-size-h-display">{{ pageTitle }}</h1>
+ <beta-badge class="gl-ml-3" />
+ </div>
<p>
- <span data-testid="description">{{ pageDescription }}</span>
+ <span data-testid="page-description">{{ pageDescription }}</span>
<gl-link :href="$options.learnMorePath" target="_blank">{{
$options.i18n.learnMore
}}</gl-link>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
index 3722b8e6c59..5de71fa1fc5 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_list_skeleton_loader.vue
@@ -37,21 +37,19 @@ export default {
>
<!-- Catalog project avatar -->
<rect x="0" y="0" width="48" height="48" rx="4" ry="4" />
- <!-- namespace path -->
- <rect x="60" y="4" width="400" height="16" rx="2" ry="2" />
+ <!-- resource path -->
+ <rect x="60" y="0" width="200" height="10" rx="2" ry="2" />
+ <!-- resource name -->
+ <rect x="60" y="14" width="400" height="16" rx="2" ry="2" />
<!-- Project description -->
- <rect x="60" y="30" width="500" height="12" rx="2" ry="2" />
+ <rect x="60" y="34" width="500" height="12" rx="2" ry="2" />
<!-- Release date line -->
<rect :x="coordinates.releaseDateX" y="30" width="200" height="12" rx="2" ry="2" />
<!-- Favorites -->
- <rect :x="coordinates.statsX" y="4" width="16" height="16" rx="2" ry="2" />
- <rect :x="coordinates.statsX + 18" y="7" width="18" height="10" rx="2" ry="2" />
-
- <!-- Forks -->
<rect :x="coordinates.statsX + 50" y="4" width="16" height="16" rx="2" ry="2" />
- <rect :x="coordinates.statsX + 68" y="7" width="18" height="10" rx="2" ry="2" />
+ <rect :x="coordinates.statsX + 70" y="7" width="18" height="10" rx="2" ry="2" />
</gl-skeleton-loader>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
new file mode 100644
index 00000000000..e074cfda6f7
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlSearchBoxByClick, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { __ } from '~/locale';
+import { SORT_ASC, SORT_DESC, SORT_OPTION_CREATED } from '../../constants';
+
+export default {
+ components: {
+ GlSearchBoxByClick,
+ GlSorting,
+ GlSortingItem,
+ },
+ data() {
+ return {
+ currentSortOption: SORT_OPTION_CREATED,
+ isAscending: false,
+ searchTerm: '',
+ };
+ },
+ computed: {
+ currentSortDirection() {
+ return this.isAscending ? SORT_ASC : SORT_DESC;
+ },
+ currentSorting() {
+ return `${this.currentSortOption}_${this.currentSortDirection}`;
+ },
+ currentSortText() {
+ const currentSort = this.$options.sortOptions.find(
+ (sort) => sort.key === this.currentSortOption,
+ );
+ return currentSort.text;
+ },
+ },
+ watch: {
+ currentSorting(newSorting) {
+ this.$emit('update-sorting', newSorting);
+ },
+ },
+ methods: {
+ isActiveSort(sortItem) {
+ return sortItem === this.currentSortOption;
+ },
+ onClear() {
+ this.$emit('update-search-term', '');
+ },
+ onSortDirectionChange() {
+ this.isAscending = !this.isAscending;
+ },
+ onSubmitSearch() {
+ this.$emit('update-search-term', this.searchTerm);
+ },
+ setSelectedSortOption(sortingItem) {
+ this.currentSortOption = sortingItem.key;
+ },
+ },
+ sortOptions: [{ key: SORT_OPTION_CREATED, text: __('Created at') }],
+};
+</script>
+<template>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-search-box-by-click
+ v-model="searchTerm"
+ data-testid="catalog-search-bar"
+ @submit="onSubmitSearch"
+ @clear="onClear"
+ />
+ <gl-sorting
+ :is-ascending="isAscending"
+ :text="currentSortText"
+ @sortDirectionChange="onSortDirectionChange"
+ >
+ <gl-sorting-item
+ v-for="sortingItem in $options.sortOptions"
+ :key="sortingItem.key"
+ :active="isActiveSort(sortingItem.key)"
+ @click="setSelectedSortOption(sortingItem)"
+ >
+ {{ sortingItem.text }}
+ </gl-sorting-item>
+ </gl-sorting>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
index 080955b4322..57d19af614f 100644
--- a/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue
@@ -1,16 +1,9 @@
<script>
-import {
- GlAvatar,
- GlBadge,
- GlButton,
- GlIcon,
- GlLink,
- GlSprintf,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { s__ } from '~/locale';
+import { GlAvatar, GlBadge, GlIcon, GlLink, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
+import { s__, n__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import { CI_RESOURCE_DETAILS_PAGE_NAME } from '../../router/constants';
export default {
@@ -21,7 +14,6 @@ export default {
components: {
GlAvatar,
GlBadge,
- GlButton,
GlIcon,
GlLink,
GlSprintf,
@@ -42,12 +34,27 @@ export default {
authorProfileUrl() {
return this.latestVersion.author.webUrl;
},
+ resourceId() {
+ return cleanLeadingSeparator(this.resource.webPath);
+ },
+ detailsPageResolved() {
+ return this.$router.resolve({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: this.resourceId },
+ });
+ },
+ detailsPageHref() {
+ return decodeURIComponent(this.detailsPageResolved.href);
+ },
entityId() {
return getIdFromGraphQLId(this.resource.id);
},
starCount() {
return this.resource?.starCount || 0;
},
+ starCountText() {
+ return n__('Star', 'Stars', this.starCount);
+ },
hasReleasedVersion() {
return Boolean(this.latestVersion?.releasedAt);
},
@@ -60,26 +67,33 @@ export default {
releasedAt() {
return getTimeago().format(this.latestVersion?.releasedAt);
},
- resourcePath() {
- return `${this.resource.rootNamespace?.name} / ${this.resource.rootNamespace?.fullPath} / `;
- },
tagName() {
return this.latestVersion?.tagName || this.$options.i18n.unreleased;
},
+ webPath() {
+ return cleanLeadingSeparator(this.resource?.webPath);
+ },
},
methods: {
- navigateToDetailsPage() {
- this.$router.push({
- name: CI_RESOURCE_DETAILS_PAGE_NAME,
- params: { id: this.entityId },
- });
+ navigateToDetailsPage(e) {
+ // Open link in a new tab if any of these modifier key is held down.
+ if (e?.ctrlKey || e?.metaKey) {
+ return;
+ }
+
+ // Override the <a> tag if no modifier key is held down to use Vue router and not
+ // open a new tab.
+ e.preventDefault();
+
+ // Push to the decoded URL to avoid all the / being encoded
+ this.$router.push({ path: decodeURIComponent(this.resourceId) });
},
},
};
</script>
<template>
<li
- class="gl-display-flex gl-display-flex-wrap gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
+ class="gl-display-flex gl-display-flex-wrap gl-align-items-center gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-text-gray-500 gl-py-3"
data-testid="catalog-resource-item"
>
<gl-avatar
@@ -92,36 +106,40 @@ export default {
@click="navigateToDetailsPage"
/>
<div class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
- <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-2">
- <gl-button
- variant="link"
+ <span class="gl-font-sm gl-mb-1">{{ webPath }}</span>
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2 gl-mb-1">
+ <gl-link
class="gl-text-gray-900! gl-mr-1"
+ :href="detailsPageHref"
data-testid="ci-resource-link"
@click="navigateToDetailsPage"
>
- {{ resourcePath }} <b> {{ resource.name }}</b>
- </gl-button>
+ <b> {{ resource.name }}</b>
+ </gl-link>
<div class="gl-display-flex gl-flex-grow-1 gl-md-justify-content-space-between">
- <gl-badge size="sm">{{ tagName }}</gl-badge>
+ <gl-badge size="sm" class="gl-h-5 gl-align-self-center">{{ tagName }}</gl-badge>
<span class="gl-display-flex gl-align-items-center gl-ml-5">
- <span class="gl--flex-center" data-testid="stats-favorites">
- <gl-icon name="star" :size="14" class="gl-mr-1" />
+ <span
+ v-gl-tooltip.top
+ :title="starCountText"
+ class="gl--flex-center"
+ data-testid="stats-favorites"
+ >
+ <gl-icon name="star-o" :size="14" class="gl-mr-2" />
<span class="gl-mr-3">{{ starCount }}</span>
</span>
</span>
</div>
</div>
<div
- class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between"
+ class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-justify-content-space-between gl-font-sm"
>
- <span class="gl-display-flex gl-flex-basis-two-thirds gl-font-sm">{{
- resource.description
- }}</span>
+ <span class="gl-display-flex gl-flex-basis-two-thirds">{{ resource.description }}</span>
<div class="gl-display-flex gl-justify-content-end">
<span v-if="hasReleasedVersion">
<gl-sprintf :message="$options.i18n.releasedMessage">
<template #timeAgo>
- <span v-gl-tooltip.bottom :title="formattedDate">
+ <span v-gl-tooltip.top :title="formattedDate">
{{ releasedAt }}
</span>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
index a53ddefaa50..e53a10d8935 100644
--- a/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/empty_state.vue
@@ -1,22 +1,70 @@
<script>
-import { GlEmptyState } from '@gitlab/ui';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
import { s__ } from '~/locale';
+import { COMPONENTS_DOCS_URL } from '~/ci/catalog/constants';
export default {
- i18n: {
- title: s__('CiCatalog|Get started with the CI/CD Catalog'),
- description: s__(
- 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
- ),
- },
name: 'CiCatalogEmptyState',
+ COMPONENTS_DOCS_URL,
components: {
GlEmptyState,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ searchTerm: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ searchTitle() {
+ return this.isQueryTooSmall
+ ? this.$options.i18n.searchTooSmall.title
+ : this.$options.i18n.search.title;
+ },
+ isSearching() {
+ return this.searchTerm?.length > 0;
+ },
+ isQueryTooSmall() {
+ return this.isSearching && this.searchTerm?.length < 3;
+ },
+ },
+ i18n: {
+ default: {
+ title: s__('CiCatalog|Get started with the CI/CD Catalog'),
+ description: s__(
+ 'CiCatalog|Create a pipeline component repository and make reusing pipeline configurations faster and easier.',
+ ),
+ },
+ search: {
+ title: s__('CiCatalog|No result found'),
+ description: s__(
+ 'CiCatalog|Edit your search and try again. Or %{linkStart}learn to create a component repository%{linkEnd}.',
+ ),
+ },
+ searchTooSmall: {
+ title: s__('CiCatalog|Search must be at least 3 characters'),
+ },
},
};
</script>
<template>
<div>
- <gl-empty-state :title="$options.i18n.title" :description="$options.i18n.description" />
+ <gl-empty-state v-if="isSearching" :title="searchTitle">
+ <template #description>
+ <gl-sprintf :message="$options.i18n.search.description">
+ <template #link="{ content }">
+ <gl-link :href="$options.COMPONENTS_DOCS_URL" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </template>
+ </gl-empty-state>
+ <gl-empty-state
+ v-else
+ :title="$options.i18n.default.title"
+ :description="$options.i18n.default.description"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
index da2c73be900..b7e117f9c26 100644
--- a/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resource_details_page.vue
@@ -2,8 +2,7 @@
import { GlEmptyState } from '@gitlab/ui';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
-import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { CI_CATALOG_RESOURCE_TYPE } from '../../graphql/settings';
+import { cleanLeadingSeparator } from '~/lib/utils/url_utility';
import getCatalogCiResourceDetails from '../../graphql/queries/get_ci_catalog_resource_details.query.graphql';
import getCatalogCiResourceSharedData from '../../graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
import CiResourceDetails from '../details/ci_resource_details.vue';
@@ -28,7 +27,7 @@ export default {
query: getCatalogCiResourceSharedData,
variables() {
return {
- id: this.graphQLId,
+ fullPath: this.cleanFullPath,
};
},
update(data) {
@@ -43,7 +42,7 @@ export default {
query: getCatalogCiResourceDetails,
variables() {
return {
- id: this.graphQLId,
+ fullPath: this.cleanFullPath,
};
},
update(data) {
@@ -56,8 +55,8 @@ export default {
},
},
computed: {
- graphQLId() {
- return convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, this.$route.params.id);
+ cleanFullPath() {
+ return cleanLeadingSeparator(this.$route.params.id);
},
isLoadingDetails() {
return this.$apollo.queries.resourceAdditionalDetails.loading;
@@ -103,7 +102,7 @@ export default {
:pipeline-status="pipelineStatus"
:resource="resourceSharedData"
/>
- <ci-resource-details :resource-id="graphQLId" />
+ <ci-resource-details :resource-path="cleanFullPath" />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
index 5e8727a3ed0..e1c86f38d7e 100644
--- a/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
+++ b/app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue
@@ -1,17 +1,21 @@
<script>
import { createAlert } from '~/alert';
import { s__ } from '~/locale';
-import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
-import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
-import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
-import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import CatalogSearch from '../list/catalog_search.vue';
+import CiResourcesList from '../list/ci_resources_list.vue';
+import CatalogListSkeletonLoader from '../list/catalog_list_skeleton_loader.vue';
+import CatalogHeader from '../list/catalog_header.vue';
+import EmptyState from '../list/empty_state.vue';
import getCatalogResources from '../../graphql/queries/get_ci_catalog_resources.query.graphql';
+import getCurrentPage from '../../graphql/queries/client/get_current_page.query.graphql';
+import updateCurrentPageMutation from '../../graphql/mutations/client/update_current_page.mutation.graphql';
export default {
components: {
CatalogHeader,
CatalogListSkeletonLoader,
+ CatalogSearch,
CiResourcesList,
EmptyState,
},
@@ -19,8 +23,9 @@ export default {
return {
catalogResources: [],
currentPage: 1,
- totalCount: 0,
pageInfo: {},
+ searchTerm: '',
+ totalCount: 0,
};
},
apollo: {
@@ -43,6 +48,12 @@ export default {
createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
},
},
+ currentPage: {
+ query: getCurrentPage,
+ update(data) {
+ return data?.page?.current || 1;
+ },
+ },
},
computed: {
hasResources() {
@@ -51,6 +62,12 @@ export default {
isLoading() {
return this.$apollo.queries.catalogResources.loading;
},
+ isSearching() {
+ return this.searchTerm?.length > 0;
+ },
+ showEmptyState() {
+ return !this.hasResources && !this.isSearching;
+ },
},
methods: {
async handlePrevPage() {
@@ -63,7 +80,7 @@ export default {
},
});
- this.currentPage -= 1;
+ this.decrementPage();
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
@@ -78,7 +95,7 @@ export default {
},
});
- this.currentPage += 1;
+ this.incrementPage();
} catch (e) {
// Ensure that the current query is properly stoped if an error occurs.
this.$apollo.queries.catalogResources.stop();
@@ -86,6 +103,36 @@ export default {
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
}
},
+ updatePageCount(pageNumber) {
+ this.$apollo.mutate({
+ mutation: updateCurrentPageMutation,
+ variables: {
+ pageNumber,
+ },
+ });
+ },
+ decrementPage() {
+ this.updatePageCount(this.currentPage - 1);
+ },
+ incrementPage() {
+ this.updatePageCount(this.currentPage + 1);
+ },
+ onUpdateSearchTerm(searchTerm) {
+ this.searchTerm = !searchTerm.length ? null : searchTerm;
+ this.resetPageCount();
+ this.$apollo.queries.catalogResources.refetch({
+ searchTerm: this.searchTerm,
+ });
+ },
+ onUpdateSorting(sortValue) {
+ this.resetPageCount();
+ this.$apollo.queries.catalogResources.refetch({
+ sortValue,
+ });
+ },
+ resetPageCount() {
+ this.updatePageCount(1);
+ },
},
i18n: {
fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
@@ -95,18 +142,24 @@ export default {
<template>
<div>
<catalog-header />
- <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
- <empty-state v-else-if="!hasResources" />
- <ci-resources-list
- v-else
- :current-page="currentPage"
- :page-info="pageInfo"
- :prev-text="__('Prev')"
- :next-text="__('Next')"
- :resources="catalogResources"
- :total-count="totalCount"
- @onPrevPage="handlePrevPage"
- @onNextPage="handleNextPage"
+ <catalog-search
+ class="gl-py-4 gl-border-b-1 gl-border-gray-100 gl-border-b-solid gl-border-t-1 gl-border-t-solid"
+ @update-search-term="onUpdateSearchTerm"
+ @update-sorting="onUpdateSorting"
/>
+ <catalog-list-skeleton-loader v-if="isLoading" class="gl-w-full gl-mt-3" />
+ <empty-state v-else-if="!hasResources" :search-term="searchTerm" />
+ <template v-else>
+ <ci-resources-list
+ :current-page="currentPage"
+ :page-info="pageInfo"
+ :prev-text="__('Prev')"
+ :next-text="__('Next')"
+ :resources="catalogResources"
+ :total-count="totalCount"
+ @onPrevPage="handlePrevPage"
+ @onNextPage="handleNextPage"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
index ab067f991cd..34c0ac797c1 100644
--- a/app/assets/javascripts/ci/catalog/constants.js
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -1,35 +1,9 @@
-// We disable this for the entire file until the mock data is cleanup
-/* eslint-disable @gitlab/require-i18n-strings */
+import { helpPagePath } from '~/helpers/help_page_helper';
+
export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
-export const componentsMockData = {
- __typename: 'CiComponentConnection',
- nodes: [
- {
- id: 'gid://gitlab/Ci::Component/1',
- name: 'Ruby gal',
- description: 'This is a pretty amazing component that does EVERYTHING ruby.',
- path: 'gitlab.com/gitlab-org/ruby-gal@~latest',
- inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
- },
- {
- id: 'gid://gitlab/Ci::Component/2',
- name: 'Javascript madness',
- description: 'Adds some spice to your life.',
- path: 'gitlab.com/gitlab-org/javascript-madness@~latest',
- inputs: {
- nodes: [
- { name: 'isFun', defaultValue: 'true', required: true },
- { name: 'RandomNumber', defaultValue: '10', required: false },
- ],
- },
- },
- {
- id: 'gid://gitlab/Ci::Component/3',
- name: 'Go go go',
- description: 'When you write Go, you gotta go go go.',
- path: 'gitlab.com/gitlab-org/go-go-go@~latest',
- inputs: { nodes: [{ name: 'version', defaultValue: '1.0.0', required: true }] },
- },
- ],
-};
+export const SORT_OPTION_CREATED = 'CREATED';
+export const SORT_ASC = 'ASC';
+export const SORT_DESC = 'DESC';
+
+export const COMPONENTS_DOCS_URL = helpPagePath('ci/components/index');
diff --git a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
index a86db4c1b03..b3a750e9604 100644
--- a/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql
@@ -1,5 +1,6 @@
fragment CatalogResourceFields on CiCatalogResource {
id
+ webPath
icon
name
description
@@ -15,10 +16,4 @@ fragment CatalogResourceFields on CiCatalogResource {
webUrl
}
}
- rootNamespace {
- id
- fullPath
- name
- }
- webPath
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql b/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql
new file mode 100644
index 00000000000..7ffd8f6ea61
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/mutations/client/update_current_page.mutation.graphql
@@ -0,0 +1,7 @@
+mutation updateCurrentPage($pageNumber: Int!) {
+ updateCurrentPage(pageNumber: $pageNumber) @client {
+ page {
+ current
+ }
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql
new file mode 100644
index 00000000000..b49895a64aa
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/client/get_current_page.query.graphql
@@ -0,0 +1,5 @@
+query getCurrentPage {
+ page @client {
+ current
+ }
+}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
index 6aef5dcc4e7..41ac72aa9de 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql
@@ -1,17 +1,18 @@
-query getCiCatalogResourceComponents($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceComponents($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
- components @client {
- nodes {
- id
- name
- description
- path
- inputs {
- nodes {
+ webPath
+ latestVersion {
+ id
+ components {
+ nodes {
+ id
+ name
+ path
+ inputs {
name
- defaultValue
required
+ default
}
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
index 382d3866795..a77e8f12d03 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceDetails($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceDetails($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
+ webPath
openIssuesCount
openMergeRequestsCount
versions(first: 1) {
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
index 6b3d0cdcfc7..c1fde8dcb43 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql
@@ -1,6 +1,7 @@
-query getCiCatalogResourceReadme($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceReadme($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
id
+ webPath
readmeHtml
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
index 4ac4cb0e394..3d5d139a334 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql
@@ -1,7 +1,7 @@
#import "../fragments/catalog_resource.fragment.graphql"
-query getCiCatalogResourceSharedData($id: CiCatalogResourceID!) {
- ciCatalogResource(id: $id) {
+query getCiCatalogResourceSharedData($fullPath: ID!) {
+ ciCatalogResource(fullPath: $fullPath) {
...CatalogResourceFields
}
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
index aae29edef5e..1cf213dec63 100644
--- a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql
@@ -1,7 +1,21 @@
#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
-query getCatalogResources($after: String, $before: String, $first: Int = 20, $last: Int) {
- ciCatalogResources(after: $after, before: $before, first: $first, last: $last) {
+query getCatalogResources(
+ $searchTerm: String
+ $sortValue: CiCatalogResourceSort
+ $after: String
+ $before: String
+ $first: Int = 20
+ $last: Int
+) {
+ ciCatalogResources(
+ search: $searchTerm
+ sort: $sortValue
+ after: $after
+ before: $before
+ first: $first
+ last: $last
+ ) {
pageInfo {
startCursor
endCursor
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
index a87b26ca4fc..4038188a7ce 100644
--- a/app/assets/javascripts/ci/catalog/graphql/settings.js
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -1,32 +1,42 @@
-import { componentsMockData } from '../constants';
+import getCurrentPage from './queries/client/get_current_page.query.graphql';
export const ciCatalogResourcesItemsCount = 20;
export const CI_CATALOG_RESOURCE_TYPE = 'Ci::Catalog::Resource';
export const cacheConfig = {
- cacheConfig: {
- typePolicies: {
- Query: {
- fields: {
- ciCatalogResource(_, { args, toReference }) {
- return toReference({
- __typename: 'CiCatalogResource',
- id: args.id,
- });
- },
- ciCatalogResources: {
- keyArgs: false,
- },
+ typePolicies: {
+ Query: {
+ fields: {
+ ciCatalogResource(_, { args, toReference }) {
+ return toReference({
+ __typename: 'CiCatalogResource',
+ // Webpath is the fullpath with a leading slash
+ webPath: `/${args.fullPath}`,
+ });
+ },
+ ciCatalogResources: {
+ keyArgs: false,
},
},
},
+ CiCatalogResource: {
+ keyFields: ['webPath'],
+ },
},
};
export const resolvers = {
- CiCatalogResource: {
- components() {
- return componentsMockData;
+ Mutation: {
+ updateCurrentPage: (_, { pageNumber }, { cache }) => {
+ cache.writeQuery({
+ query: getCurrentPage,
+ data: {
+ page: {
+ __typename: 'CatalogPage',
+ current: pageNumber,
+ },
+ },
+ });
},
},
};
diff --git a/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql b/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql
new file mode 100644
index 00000000000..8604fae0655
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/typedefs.graphql
@@ -0,0 +1,11 @@
+type CatalogPage {
+ current: Int
+}
+
+extend type Query {
+ page: CatalogPage
+}
+
+extend type Mutation {
+ updateCurrentPage(pageNumber: Int!): CatalogPage
+}
diff --git a/app/assets/javascripts/ci/catalog/index.js b/app/assets/javascripts/ci/catalog/index.js
index 5815245506c..34866bfb821 100644
--- a/app/assets/javascripts/ci/catalog/index.js
+++ b/app/assets/javascripts/ci/catalog/index.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { cacheConfig, resolvers } from '~/ci/catalog/graphql/settings';
+import typeDefs from '~/ci/catalog/graphql/typedefs.graphql';
import GlobalCatalog from './global_catalog.vue';
import CiResourcesPage from './components/pages/ci_resources_page.vue';
@@ -19,7 +20,7 @@ export const initCatalog = (selector = '#js-ci-cd-catalog') => {
Vue.use(VueApollo);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers, cacheConfig),
+ defaultClient: createDefaultClient(resolvers, { cacheConfig, typeDefs }),
});
return new Vue({
diff --git a/app/assets/javascripts/ci/catalog/router/routes.js b/app/assets/javascripts/ci/catalog/router/routes.js
index ccfb0673c83..ce859e266d7 100644
--- a/app/assets/javascripts/ci/catalog/router/routes.js
+++ b/app/assets/javascripts/ci/catalog/router/routes.js
@@ -4,6 +4,6 @@ import { CI_RESOURCES_PAGE_NAME, CI_RESOURCE_DETAILS_PAGE_NAME } from './constan
export const createRoutes = (listComponent) => {
return [
{ name: CI_RESOURCES_PAGE_NAME, path: '', component: listComponent },
- { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id', component: CiResourceDetailsPage },
+ { name: CI_RESOURCE_DETAILS_PAGE_NAME, path: '/:id+', component: CiResourceDetailsPage },
];
};
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
index ccfe773b01f..2ad6c7c6578 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue
@@ -29,13 +29,16 @@ import {
EVENT_ACTION,
EXPANDED_VARIABLES_NOTE,
FLAG_LINK_TITLE,
+ MASKED_VALUE_MIN_LENGTH,
VARIABLE_ACTIONS,
variableOptions,
+ WHITESPACE_REG_EX,
} from '../constants';
import CiEnvironmentsDropdown from './ci_environments_dropdown.vue';
import { awsTokenList } from './ci_variable_autocomplete_tokens';
const trackingMixin = Tracking.mixin({ label: DRAWER_EVENT_LABEL });
+const KEY_REGEX = /^\w+$/;
export const i18n = {
addVariable: s__('CiVariables|Add variable'),
@@ -50,25 +53,38 @@ export const i18n = {
flags: __('Flags'),
flagsLinkTitle: FLAG_LINK_TITLE,
key: __('Key'),
+ keyFeedback: s__("CiVariables|A variable key can only contain letters, numbers, and '_'."),
+ keyHelpText: s__(
+ 'CiVariables|You can use CI/CD variables with the same name in different places, but the variables might overwrite each other. %{linkStart}What is the order of precedence for variables?%{linkEnd}',
+ ),
maskedField: s__('CiVariables|Mask variable'),
maskedDescription: s__(
'CiVariables|Variable will be masked in job logs. Requires values to meet regular expression requirements.',
),
+ maskedValueMinLengthValidationText: s__(
+ 'CiVariables|The value must have at least %{charsAmount} characters.',
+ ),
modalDeleteMessage: s__('CiVariables|Do you want to delete the variable %{key}?'),
protectedField: s__('CiVariables|Protect variable'),
protectedDescription: s__(
'CiVariables|Export variable to pipelines running on protected branches and tags only.',
),
+ unsupportedCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: %{unsupportedChars}.',
+ ),
+ unsupportedAndWhitespaceCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: %{unsupportedChars} and whitespace characters.',
+ ),
valueFeedback: {
rawHelpText: s__('CiVariables|Variable value will be evaluated as raw string.'),
- maskedReqsNotMet: s__(
- 'CiVariables|This variable value does not meet the masking requirements.',
- ),
},
variableReferenceTitle: s__('CiVariables|Value might contain a variable reference'),
variableReferenceDescription: s__(
'CiVariables|Unselect "Expand variable reference" if you want to use the variable value as a raw string.',
),
+ whitespaceCharsValidationText: s__(
+ 'CiVariables|This value cannot be masked because it contains the following characters: whitespace characters.',
+ ),
type: __('Type'),
value: __('Value'),
};
@@ -146,7 +162,7 @@ export default {
return regex.test(this.variable.value);
},
canSubmit() {
- return this.variable.key.length > 0 && this.isValueValid;
+ return this.variable.key.length > 0 && this.isKeyValid && this.isValueValid;
},
getDrawerHeaderHeight() {
return getContentWrapperHeight();
@@ -157,6 +173,9 @@ export default {
isExpanded() {
return !this.variable.raw;
},
+ isKeyValid() {
+ return KEY_REGEX.test(this.variable.key);
+ },
isMaskedReqsMet() {
return !this.variable.masked || this.isValueMasked;
},
@@ -169,11 +188,76 @@ export default {
isEditing() {
return this.mode === EDIT_VARIABLE_ACTION;
},
+ isMaskedValueContainsWhitespaceChars() {
+ return this.isValueMaskable && WHITESPACE_REG_EX.test(this.variable.value);
+ },
maskedRegexToUse() {
return this.variable.raw ? this.maskableRawRegex : this.maskableRegex;
},
- maskedReqsNotMetText() {
- return !this.isMaskedReqsMet ? this.$options.i18n.valueFeedback.maskedReqsNotMet : '';
+ maskedSupportedCharsRegEx() {
+ const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
+ return new RegExp(supportedChars, 'g');
+ },
+ maskedValueMinLengthValidationText() {
+ return sprintf(this.$options.i18n.maskedValueMinLengthValidationText, {
+ charsAmount: MASKED_VALUE_MIN_LENGTH,
+ });
+ },
+ unsupportedCharsList() {
+ if (this.isMaskedReqsMet) {
+ return [];
+ }
+
+ return [
+ ...new Set(
+ this.variable.value
+ .replace(WHITESPACE_REG_EX, '')
+ .replace(this.maskedSupportedCharsRegEx, '')
+ .split(''),
+ ),
+ ];
+ },
+ unsupportedChars() {
+ return this.unsupportedCharsList.join(', ');
+ },
+ unsupportedCharsValidationText() {
+ return sprintf(
+ this.$options.i18n.unsupportedCharsValidationText,
+ {
+ unsupportedChars: this.unsupportedChars,
+ },
+ false,
+ );
+ },
+ unsupportedAndWhitespaceCharsValidationText() {
+ return sprintf(
+ this.$options.i18n.unsupportedAndWhitespaceCharsValidationText,
+ {
+ unsupportedChars: this.unsupportedChars,
+ },
+ false,
+ );
+ },
+ maskedValidationIssuesText() {
+ if (this.isMaskedReqsMet) {
+ return '';
+ }
+
+ let validationIssuesText = '';
+
+ if (this.unsupportedCharsList.length && !this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.unsupportedCharsValidationText;
+ } else if (this.unsupportedCharsList.length && this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.unsupportedAndWhitespaceCharsValidationText;
+ } else if (!this.unsupportedCharsList.length && this.isMaskedValueContainsWhitespaceChars) {
+ validationIssuesText = this.$options.i18n.whitespaceCharsValidationText;
+ }
+
+ if (this.variable.value.length < MASKED_VALUE_MIN_LENGTH) {
+ validationIssuesText += ` ${this.maskedValueMinLengthValidationText}`;
+ }
+
+ return validationIssuesText.trim();
},
modalActionText() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
@@ -218,9 +302,7 @@ export default {
let property;
if (this.isValueMaskable) {
- const supportedChars = this.maskedRegexToUse.replace('^', '').replace(/{(\d,)}\$/, '');
- const regex = new RegExp(supportedChars, 'g');
- property = this.variable.value.replace(regex, '');
+ property = this.variable.value.replace(this.maskedSupportedCharsRegEx, '');
} else if (this.hasVariableReference) {
property = '$';
}
@@ -246,6 +328,9 @@ export default {
flagLink: helpPagePath('ci/variables/index', {
anchor: 'define-a-cicd-variable-in-the-ui',
}),
+ variablesPrecedenceLink: helpPagePath('ci/variables/index', {
+ anchor: 'cicd-variable-precedence',
+ }),
i18n,
variableOptions,
deleteModal: {
@@ -339,6 +424,7 @@ export default {
class="gl-display-flex"
:title="$options.i18n.flagsLinkTitle"
:href="$options.flagLink"
+ data-testid="ci-variable-flags-docs-link"
target="_blank"
>
<gl-icon name="question-o" :size="14" />
@@ -377,22 +463,39 @@ export default {
class="gl-border-none gl-pb-0! gl-mb-n5"
data-testid="ci-variable-key"
/>
+ <p
+ v-if="variable.key.length > 0 && !isKeyValid"
+ class="gl-pt-3! gl-pb-0! gl-mb-0 gl-text-red-500 gl-border-none"
+ >
+ {{ $options.i18n.keyFeedback }}
+ </p>
+ <p class="gl-pt-3! gl-pb-0! gl-mb-0 gl-text-secondary gl-border-none">
+ <gl-sprintf :message="$options.i18n.keyHelpText">
+ <template #link="{ content }"
+ ><gl-link
+ :href="$options.variablesPrecedenceLink"
+ data-testid="ci-variable-precedence-docs-link"
+ >{{ content }}</gl-link
+ >
+ </template>
+ </gl-sprintf>
+ </p>
<gl-form-group
:label="$options.i18n.value"
label-for="ci-variable-value"
class="gl-border-none gl-mb-n2"
data-testid="ci-variable-value-label"
- :invalid-feedback="maskedReqsNotMetText"
+ :invalid-feedback="maskedValidationIssuesText"
:state="isValueValid"
>
<gl-form-textarea
id="ci-variable-value"
v-model="variable.value"
+ :spellcheck="false"
class="gl-border-none gl-font-monospace!"
rows="3"
max-rows="10"
data-testid="ci-variable-value"
- spellcheck="false"
/>
<p
v-if="variable.raw"
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index d85827b8220..4ec7333f465 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -2,6 +2,10 @@ import { __, s__, sprintf } from '~/locale';
export const ENVIRONMENT_QUERY_LIMIT = 30;
+export const MASKED_VALUE_MIN_LENGTH = 8;
+
+export const WHITESPACE_REG_EX = /\s/;
+
export const SORT_DIRECTIONS = {
ASC: 'KEY_ASC',
DESC: 'KEY_DESC',
diff --git a/app/assets/javascripts/ci/common/private/job_name_component.vue b/app/assets/javascripts/ci/common/private/job_name_component.vue
index b4e831d69d4..a0e611acc9d 100644
--- a/app/assets/javascripts/ci/common/private/job_name_component.vue
+++ b/app/assets/javascripts/ci/common/private/job_name_component.vue
@@ -1,5 +1,5 @@
<script>
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
/**
* Component that renders both the CI icon status and the job name.
@@ -30,7 +30,13 @@ export default {
</script>
<template>
<span class="mw-100 gl-display-flex gl-align-items-center gl-flex-grow-1">
- <ci-icon :size="iconSize" :status="status" :show-tooltip="false" class="gl-line-height-0" />
+ <ci-icon
+ :size="iconSize"
+ :status="status"
+ :show-tooltip="false"
+ :use-link="false"
+ class="gl-line-height-0"
+ />
<span class="gl-text-truncate mw-70p gl-pl-3 gl-display-inline-block">
{{ name }}
</span>
diff --git a/app/assets/javascripts/ci/job_details/components/environments_block.vue b/app/assets/javascripts/ci/job_details/components/environments_block.vue
index 4046e1ade82..16d553fd071 100644
--- a/app/assets/javascripts/ci/job_details/components/environments_block.vue
+++ b/app/assets/javascripts/ci/job_details/components/environments_block.vue
@@ -1,7 +1,7 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { __ } from '~/locale';
export default {
diff --git a/app/assets/javascripts/ci/job_details/components/job_header.vue b/app/assets/javascripts/ci/job_details/components/job_header.vue
index 1aa83a94bc5..031abc7a36c 100644
--- a/app/assets/javascripts/ci/job_details/components/job_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_header.vue
@@ -4,7 +4,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import { isGid, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { glEmojiTag } from '~/emoji';
import { __, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
export default {
diff --git a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
index 4a30878bec5..837efa154e2 100644
--- a/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
+++ b/app/assets/javascripts/ci/job_details/components/job_log_controllers.vue
@@ -20,6 +20,9 @@ export default {
'Job|Search for substrings in your job log output. Currently search is only supported for the visible job log output, not for any log output that is truncated due to size.',
),
logLineNumberNotFound: s__('Job|We could not find this element'),
+ enterFullscreen: s__('Job|Show full screen'),
+ exitFullScreen: s__('Job|Exit full screen'),
+ fullScreenNotAvailable: s__('Job|Full screen mode is not available'),
},
components: {
GlLink,
@@ -65,6 +68,16 @@ export default {
type: Array,
required: true,
},
+ fullScreenModeAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ fullScreenEnabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -80,34 +93,32 @@ export default {
size: numberToHumanSize(this.size),
});
},
- showJumpToFailures() {
- return this.glFeatures.jobLogJumpToFailures;
- },
hasFailures() {
return this.failureCount > 0;
},
shouldDisableJumpToFailures() {
return !this.hasFailures;
},
+ fullScreenTooltipContent() {
+ return this.fullScreenModeAvailable ? '' : this.$options.i18n.fullScreenNotAvailable;
+ },
},
mounted() {
this.checkFailureCount();
},
methods: {
checkFailureCount() {
- if (this.glFeatures.jobLogJumpToFailures) {
- backOff((next, stop) => {
- this.failureCount = document.querySelectorAll('.term-fg-l-red').length;
+ backOff((next, stop) => {
+ this.failureCount = document.querySelectorAll('.term-fg-l-red').length;
- if (this.hasFailures || (this.isComplete && !this.hasFailures)) {
- stop();
- } else {
- next();
- }
- }).catch(() => {
- this.failureCount = null;
- });
- }
+ if (this.hasFailures || (this.isComplete && !this.hasFailures)) {
+ stop();
+ } else {
+ next();
+ }
+ }).catch(() => {
+ this.failureCount = null;
+ });
},
handleScrollToNextFailure() {
const failures = document.querySelectorAll('.term-fg-l-red');
@@ -126,6 +137,12 @@ export default {
this.$emit('scrollJobLogBottom');
this.failureIndex = 0;
},
+ handleFullscreenMode() {
+ this.$emit('enterFullscreen');
+ },
+ handleExitFullscreenMode() {
+ this.$emit('exitFullscreen');
+ },
searchJobLog() {
this.searchResults = [];
@@ -221,7 +238,6 @@ export default {
<!-- scroll buttons -->
<gl-button
- v-if="showJumpToFailures"
v-gl-tooltip
:title="$options.i18n.scrollToNextFailureButtonLabel"
:aria-label="$options.i18n.scrollToNextFailureButtonLabel"
@@ -255,6 +271,29 @@ export default {
/>
</div>
<!-- eo scroll buttons -->
+
+ <div v-gl-tooltip="fullScreenTooltipContent">
+ <gl-button
+ v-if="!fullScreenEnabled"
+ :disabled="!fullScreenModeAvailable"
+ :title="$options.i18n.enterFullscreen"
+ :aria-label="$options.i18n.enterFullscreen"
+ class="btn-scroll gl-ml-3"
+ data-testid="job-controller-enter-fullscreen"
+ icon="maximize"
+ @click="handleFullscreenMode"
+ />
+ </div>
+
+ <gl-button
+ v-if="fullScreenEnabled"
+ :title="$options.i18n.exitFullScreen"
+ :aria-label="$options.i18n.exitFullScreen"
+ class="btn-scroll gl-ml-3"
+ data-testid="job-controller-exit-fullscreen"
+ icon="minimize"
+ @click="handleExitFullscreenMode"
+ />
</div>
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue b/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
deleted file mode 100644
index 39c612bc600..00000000000
--- a/app/assets/javascripts/ci/job_details/components/log/collapsible_section.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
-import LogLine from './line.vue';
-import LogLineHeader from './line_header.vue';
-
-export default {
- name: 'CollapsibleLogSection',
- components: {
- LogLine,
- LogLineHeader,
- },
- props: {
- section: {
- type: Object,
- required: true,
- },
- jobLogEndpoint: {
- type: String,
- required: true,
- },
- searchResults: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- badgeDuration() {
- return this.section.line && this.section.line.section_duration;
- },
- highlightedLines() {
- return this.searchResults.map((result) => result.lineNumber);
- },
- headerIsHighlighted() {
- const {
- line: { lineNumber },
- } = this.section;
-
- return this.highlightedLines.includes(lineNumber);
- },
- },
- methods: {
- handleOnClickCollapsibleLine(section) {
- this.$emit('onClickCollapsibleLine', section);
- },
- lineIsHighlighted({ lineNumber }) {
- return this.highlightedLines.includes(lineNumber);
- },
- },
-};
-</script>
-<template>
- <div>
- <log-line-header
- :line="section.line"
- :duration="badgeDuration"
- :path="jobLogEndpoint"
- :is-closed="section.isClosed"
- :is-highlighted="headerIsHighlighted"
- @toggleLine="handleOnClickCollapsibleLine(section)"
- />
- <template v-if="!section.isClosed">
- <log-line
- v-for="line in section.lines"
- :key="line.offset"
- :line="line"
- :path="jobLogEndpoint"
- :is-highlighted="lineIsHighlighted(line)"
- />
- </template>
- </div>
-</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line.vue b/app/assets/javascripts/ci/job_details/components/log/line.vue
index 416f75372f9..6ff2bb766c7 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line.vue
@@ -68,7 +68,7 @@ export default {
{
class: [
'js-log-line',
- 'log-line',
+ 'job-log-line',
{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight },
],
},
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_header.vue b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
index d36701323da..4716f1e5162 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_header.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_header.vue
@@ -24,6 +24,11 @@ export default {
type: String,
required: true,
},
+ hideDuration: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
duration: {
type: String,
required: false,
@@ -63,7 +68,7 @@ export default {
<template>
<div
- class="js-log-line log-line collapsible-line d-flex justify-content-between ws-normal gl-align-items-flex-start gl-relative"
+ class="js-log-line job-log-line-header job-log-line"
:class="{ 'gl-bg-gray-700': isHighlighted || applyHashHighlight }"
role="button"
@click="handleOnClick"
@@ -73,10 +78,10 @@ export default {
<span
v-for="(content, i) in line.content"
:key="i"
- class="line-text w-100 gl-white-space-pre-wrap"
+ class="gl-flex-grow-1 gl-white-space-pre-wrap"
:class="content.style"
>{{ content.text }}</span
>
- <duration-badge v-if="duration" :duration="duration" />
+ <duration-badge v-if="duration && !hideDuration" :duration="duration" />
</div>
</template>
diff --git a/app/assets/javascripts/ci/job_details/components/log/line_number.vue b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
index 30b4c80f3fa..ea39c00c8a3 100644
--- a/app/assets/javascripts/ci/job_details/components/log/line_number.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/line_number.vue
@@ -20,7 +20,7 @@ export default {
return h(
'a',
{
- class: 'gl-link d-inline-block text-right line-number flex-shrink-0',
+ class: 'job-log-line-number',
attrs: {
id: lineId,
href: lineHref,
diff --git a/app/assets/javascripts/ci/job_details/components/log/log.vue b/app/assets/javascripts/ci/job_details/components/log/log.vue
index fb6a6a58074..8ca9515996c 100644
--- a/app/assets/javascripts/ci/job_details/components/log/log.vue
+++ b/app/assets/javascripts/ci/job_details/components/log/log.vue
@@ -4,14 +4,15 @@
import { mapState, mapActions } from 'vuex';
import { scrollToElement } from '~/lib/utils/common_utils';
import { getLocationHash } from '~/lib/utils/url_utility';
-import CollapsibleLogSection from './collapsible_section.vue';
import LogLine from './line.vue';
+import LogLineHeader from './line_header.vue';
export default {
components: {
- CollapsibleLogSection,
+ LogLineHeader,
LogLine,
},
+ inject: ['pagePath'],
props: {
searchResults: {
type: Array,
@@ -20,23 +21,11 @@ export default {
},
},
computed: {
- ...mapState([
- 'jobLogEndpoint',
- 'jobLog',
- 'isJobLogComplete',
- 'isScrolledToBottomBeforeReceivingJobLog',
- ]),
+ ...mapState(['jobLog', 'jobLogSections', 'isJobLogComplete']),
highlightedLines() {
return this.searchResults.map((result) => result.lineNumber);
},
},
- updated() {
- this.$nextTick(() => {
- if (!window.location.hash) {
- this.handleScrollDown();
- }
- });
- },
mounted() {
if (window.location.hash) {
const lineNumber = getLocationHash();
@@ -51,25 +40,27 @@ export default {
}
});
}
+
+ this.setupFullScreenListeners();
},
methods: {
- ...mapActions(['toggleCollapsibleLine', 'scrollBottom']),
+ ...mapActions(['toggleCollapsibleLine', 'scrollBottom', 'setupFullScreenListeners']),
handleOnClickCollapsibleLine(section) {
this.toggleCollapsibleLine(section);
},
- /**
- * The job log is sent in HTML, which means we need to use `v-html` to render it
- * Using the updated hook with $nextTick is not enough to wait for the DOM to be updated
- * in this case because it runs before `v-html` has finished running, since there's no
- * Vue binding.
- * In order to scroll the page down after `v-html` has finished, we need to use setTimeout
- */
- handleScrollDown() {
- if (this.isScrolledToBottomBeforeReceivingJobLog) {
- setTimeout(() => {
- this.scrollBottom();
- }, 0);
+ isLineVisible(line) {
+ const { lineNumber, section } = line;
+
+ if (!section) {
+ // lines outside of sections can't be collapsed
+ return true;
}
+
+ return !Object.values(this.jobLogSections).find(
+ ({ isClosed, startLineNumber, endLineNumber }) => {
+ return isClosed && lineNumber > startLineNumber && lineNumber <= endLineNumber;
+ },
+ );
},
isHighlighted({ lineNumber }) {
return this.highlightedLines.includes(lineNumber);
@@ -78,23 +69,28 @@ export default {
};
</script>
<template>
- <code class="job-log d-block" data-testid="job-log-content">
- <template v-for="(section, index) in jobLog">
- <collapsible-log-section
- v-if="section.isHeader"
- :key="`collapsible-${index}`"
- :section="section"
- :job-log-endpoint="jobLogEndpoint"
- :search-results="searchResults"
- @onClickCollapsibleLine="handleOnClickCollapsibleLine"
- />
- <log-line
- v-else
- :key="section.offset"
- :line="section"
- :path="jobLogEndpoint"
- :is-highlighted="isHighlighted(section)"
- />
+ <code class="job-log gl-display-block" data-testid="job-log-content">
+ <template v-for="line in jobLog">
+ <template v-if="isLineVisible(line)">
+ <log-line-header
+ v-if="line.isHeader"
+ :key="line.offset"
+ :line="line"
+ :path="pagePath"
+ :is-closed="jobLogSections[line.section].isClosed"
+ :duration="jobLogSections[line.section].duration"
+ :hide-duration="jobLogSections[line.section].hideDuration"
+ :is-highlighted="isHighlighted(line)"
+ @toggleLine="handleOnClickCollapsibleLine(line.section)"
+ />
+ <log-line
+ v-else
+ :key="line.offset"
+ :line="line"
+ :path="pagePath"
+ :is-highlighted="isHighlighted(line)"
+ />
+ </template>
</template>
<div v-if="!isJobLogComplete" class="js-log-animation loader-animation pt-3 pl-3">
diff --git a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
index 7f419a249cf..836426f0bde 100644
--- a/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
+++ b/app/assets/javascripts/ci/job_details/components/manual_variables_form.vue
@@ -50,6 +50,11 @@ export default {
id: convertToGraphQLId(TYPENAME_COMMIT_STATUS, this.jobId),
};
},
+ skip() {
+ // variables list always contains one empty variable
+ // skip refetch if form already has non-empty variables
+ return this.variables.length > 1;
+ },
fetchPolicy: fetchPolicies.CACHE_AND_NETWORK,
update(data) {
const jobVariables = cloneDeep(data?.project?.job?.manualVariables?.nodes);
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
index 4ec9044a21c..19027265a12 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/job_container_item.vue
@@ -2,7 +2,7 @@
import { GlLink, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
index 231f45d7ae6..08eaa7c8ecd 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar.vue
@@ -85,7 +85,7 @@ export default {
};
</script>
<template>
- <aside class="right-sidebar build-sidebar" data-offset-top="101" data-spy="affix">
+ <aside class="right-sidebar build-sidebar">
<div class="sidebar-container">
<div class="blocks-container gl-p-4 gl-pt-0">
<sidebar-header
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
index f04987a87b5..a8b29e7c581 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/sidebar_job_details_container.vue
@@ -4,6 +4,7 @@ import { mapState } from 'vuex';
import { GlBadge } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { timeIntervalInWords } from '~/lib/utils/datetime_utility';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
import { __, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import DetailRow from './sidebar_detail_row.vue';
@@ -15,8 +16,9 @@ export default {
GlBadge,
},
mixins: [timeagoMixin],
+ inject: ['pipelineTestReportUrl'],
computed: {
- ...mapState(['job']),
+ ...mapState(['job', 'testSummary']),
coverage() {
return `${this.job.coverage}%`;
},
@@ -74,6 +76,32 @@ export default {
runnerAdminPath() {
return this.job?.runner?.admin_path || '';
},
+ hasTestSummaryDetails() {
+ return Object.keys(this.testSummary).length > 0;
+ },
+ testSummaryDescription() {
+ let message;
+
+ if (this.testSummary?.total?.failed > 0) {
+ message = sprintf(__('%{failures} of %{total} failed'), {
+ failures: this.testSummary?.total?.failed,
+ total: this.testSummary?.total.count,
+ });
+ } else {
+ message = sprintf(__('%{total}'), {
+ total: this.testSummary?.total.count,
+ });
+ }
+
+ return message;
+ },
+ testReportUrlWithJobName() {
+ const urlParams = {
+ job_name: this.job.name,
+ };
+
+ return mergeUrlParams(urlParams, this.pipelineTestReportUrl);
+ },
},
i18n: {
COVERAGE: __('Coverage'),
@@ -82,6 +110,7 @@ export default {
QUEUED: __('Queued'),
RUNNER: __('Runner'),
TAGS: __('Tags'),
+ TEST_SUMMARY: __('Test summary'),
TIMEOUT: __('Timeout'),
},
TIMEOUT_HELP_URL: helpPagePath('/ci/pipelines/settings.md', {
@@ -115,6 +144,13 @@ export default {
:path="runnerAdminPath"
/>
<detail-row v-if="job.coverage" :value="coverage" :title="$options.i18n.COVERAGE" />
+ <detail-row
+ v-if="hasTestSummaryDetails"
+ :value="testSummaryDescription"
+ :title="$options.i18n.TEST_SUMMARY"
+ :path="testReportUrlWithJobName"
+ data-testid="test-summary"
+ />
<p v-if="hasTags" class="build-detail-row" data-testid="job-tags">
<span class="font-weight-bold">{{ $options.i18n.TAGS }}:</span>
diff --git a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
index e229abcbe12..413eba4fb52 100644
--- a/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
+++ b/app/assets/javascripts/ci/job_details/components/sidebar/stages_dropdown.vue
@@ -1,7 +1,7 @@
<script>
import { GlLink, GlDisclosureDropdown, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { Mousetrap } from '~/lib/mousetrap';
import { s__ } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
diff --git a/app/assets/javascripts/ci/job_details/index.js b/app/assets/javascripts/ci/job_details/index.js
index 20235015ce6..9aa01c4686e 100644
--- a/app/assets/javascripts/ci/job_details/index.js
+++ b/app/assets/javascripts/ci/job_details/index.js
@@ -20,30 +20,41 @@ export const initJobDetails = () => {
}
const {
+ jobEndpoint,
+ logEndpoint,
+ pagePath,
+ projectPath,
artifactHelpUrl,
deploymentHelpUrl,
runnerSettingsUrl,
subscriptionsMoreMinutesUrl,
- endpoint,
- pagePath,
- buildStatus,
- projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable,
+ testReportSummaryUrl,
+ pipelineTestReportUrl,
} = el.dataset;
+ const fullScreenAPIAvailable = document.fullscreenEnabled;
+
// init store to start fetching log
const store = createStore();
- store.dispatch('init', { endpoint, pagePath });
+ store.dispatch('init', {
+ jobEndpoint,
+ logEndpoint,
+ testReportSummaryUrl,
+ fullScreenAPIAvailable,
+ });
return new Vue({
el,
apolloProvider,
store,
provide: {
+ pagePath,
projectPath,
retryOutdatedJobDocsUrl,
aiRootCauseAnalysisAvailable: parseBoolean(aiRootCauseAnalysisAvailable),
+ pipelineTestReportUrl,
},
render(h) {
return h(JobApp, {
@@ -52,10 +63,6 @@ export const initJobDetails = () => {
deploymentHelpUrl,
runnerSettingsUrl,
subscriptionsMoreMinutesUrl,
- endpoint,
- pagePath,
- buildStatus,
- projectPath,
},
});
},
diff --git a/app/assets/javascripts/ci/job_details/job_app.vue b/app/assets/javascripts/ci/job_details/job_app.vue
index e0708289b43..c2394aa4fac 100644
--- a/app/assets/javascripts/ci/job_details/job_app.vue
+++ b/app/assets/javascripts/ci/job_details/job_app.vue
@@ -56,15 +56,6 @@ export default {
required: false,
default: null,
},
- terminalPath: {
- type: String,
- required: false,
- default: null,
- },
- projectPath: {
- type: String,
- required: true,
- },
subscriptionsMoreMinutesUrl: {
type: String,
required: false,
@@ -88,9 +79,9 @@ export default {
'isJobLogSizeVisible',
'isScrollBottomDisabled',
'isScrollTopDisabled',
- 'isScrolledToBottomBeforeReceivingJobLog',
'hasError',
'selectedStage',
+ 'fullScreenEnabled',
]),
...mapGetters([
'headerTime',
@@ -104,6 +95,7 @@ export default {
'isScrollingDown',
'emptyStateAction',
'hasOfflineRunnersForProject',
+ 'fullScreenAPIAndContainerAvailable',
]),
shouldRenderContent() {
@@ -182,6 +174,8 @@ export default {
'stopPolling',
'toggleScrollButtons',
'toggleScrollAnimation',
+ 'enterFullscreen',
+ 'exitFullscreen',
]),
onHideManualVariablesForm() {
this.showUpdateVariablesState = false;
@@ -262,7 +256,6 @@ export default {
v-if="shouldRenderSharedRunnerLimitWarning"
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
- :project-path="projectPath"
:subscriptions-more-minutes-url="subscriptionsMoreMinutesUrl"
/>
@@ -303,9 +296,13 @@ export default {
:is-scrolling-down="isScrollingDown"
:is-complete="isJobLogComplete"
:job-log="jobLog"
+ :full-screen-mode-available="fullScreenAPIAndContainerAvailable"
+ :full-screen-enabled="fullScreenEnabled"
@scrollJobLogTop="scrollTop"
@scrollJobLogBottom="scrollBottom"
@searchResults="setSearchResults"
+ @enterFullscreen="enterFullscreen"
+ @exitFullscreen="exitFullscreen"
/>
<log :search-results="searchResults" />
</div>
diff --git a/app/assets/javascripts/ci/job_details/store/actions.js b/app/assets/javascripts/ci/job_details/store/actions.js
index 6f538e3b3d4..e1225ecd2c9 100644
--- a/app/assets/javascripts/ci/job_details/store/actions.js
+++ b/app/assets/javascripts/ci/job_details/store/actions.js
@@ -15,20 +15,85 @@ import { __ } from '~/locale';
import { reportToSentry } from '~/ci/utils';
import * as types from './mutation_types';
-export const init = ({ dispatch }, { endpoint, pagePath }) => {
- dispatch('setJobLogOptions', {
- endpoint,
- pagePath,
+export const init = (
+ { commit, dispatch },
+ { jobEndpoint, logEndpoint, testReportSummaryUrl, fullScreenAPIAvailable = false },
+) => {
+ commit(types.SET_JOB_LOG_OPTIONS, {
+ jobEndpoint,
+ logEndpoint,
+ testReportSummaryUrl,
+ fullScreenAPIAvailable,
});
return dispatch('fetchJob');
};
-export const setJobLogOptions = ({ commit }, options) => commit(types.SET_JOB_LOG_OPTIONS, options);
-
export const hideSidebar = ({ commit }) => commit(types.HIDE_SIDEBAR);
export const showSidebar = ({ commit }) => commit(types.SHOW_SIDEBAR);
+export const enterFullscreen = ({ dispatch }) => {
+ const el = document.querySelector('.build-log-container');
+
+ if (!document.fullscreenElement && el) {
+ el.requestFullscreen()
+ .then(() => {
+ dispatch('enterFullscreenSuccess');
+ })
+ .catch((err) => {
+ reportToSentry('job_enter_fullscreen_mode', err);
+ });
+ }
+};
+
+export const enterFullscreenSuccess = ({ commit }) => {
+ commit(types.ENTER_FULLSCREEN_SUCCESS);
+};
+
+export const exitFullscreen = ({ dispatch }) => {
+ if (document.fullscreenElement) {
+ document
+ .exitFullscreen()
+ .then(() => {
+ dispatch('exitFullscreenSuccess');
+ })
+ .catch((err) => {
+ reportToSentry('job_exit_fullscreen_mode', err);
+ });
+ }
+};
+
+export const exitFullscreenSuccess = ({ commit }) => {
+ commit(types.EXIT_FULLSCREEN_SUCCESS);
+};
+
+export const fullScreenContainerSetUpResult = ({ commit }, value) => {
+ commit(types.FULL_SCREEN_CONTAINER_SET_UP, value);
+};
+
+export const fullScreenModeAvailableSuccess = ({ commit }) => {
+ commit(types.FULL_SCREEN_MODE_AVAILABLE_SUCCESS);
+};
+
+export const setupFullScreenListeners = ({ dispatch, state, getters }) => {
+ if (!state.fullScreenContainerSetUp && getters.hasJobLog) {
+ const el = document.querySelector('.build-log-container');
+
+ if (el) {
+ dispatch('fullScreenModeAvailableSuccess');
+
+ el.addEventListener('fullscreenchange', () => {
+ if (!document.fullscreenElement) {
+ // Leaving fullscreen mode
+ dispatch('exitFullscreenSuccess');
+ }
+ });
+
+ dispatch('fullScreenContainerSetUpResult', true);
+ }
+ }
+};
+
export const toggleSidebar = ({ dispatch, state }) => {
if (state.isSidebarOpen) {
dispatch('hideSidebar');
@@ -149,39 +214,46 @@ export const enableScrollTop = ({ commit }) => commit(types.ENABLE_SCROLL_TOP);
export const toggleScrollAnimation = ({ commit }, toggle) =>
commit(types.TOGGLE_SCROLL_ANIMATION, toggle);
-/**
- * Responsible to handle automatic scroll
- */
-export const toggleScrollisInBottom = ({ commit }, toggle) => {
- commit(types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG, toggle);
-};
-
export const requestJobLog = ({ commit }) => commit(types.REQUEST_JOB_LOG);
-export const fetchJobLog = ({ dispatch, state }) =>
- // update trace endpoint once BE compeletes trace re-naming in #340626
- axios
- .get(`${state.jobLogEndpoint}/trace.json`, {
- params: { state: state.jobLogState },
- })
- .then(({ data }) => {
- dispatch('toggleScrollisInBottom', isScrolledToBottom());
- dispatch('receiveJobLogSuccess', data);
-
- if (data.complete) {
- dispatch('stopPollingJobLog');
- } else if (!state.jobLogTimeout) {
- dispatch('startPollingJobLog');
- }
- })
- .catch((e) => {
- if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
- dispatch('receiveJobLogUnauthorizedError');
- } else {
- reportToSentry('job_actions', e);
- dispatch('receiveJobLogError');
- }
- });
+export const fetchJobLog = ({ commit, dispatch, state }) => {
+ let isScrolledToBottomBeforeReceivingJobLog;
+
+ return (
+ axios
+ .get(state.logEndpoint, {
+ params: { state: state.jobLogState },
+ })
+ .then(({ data }) => {
+ isScrolledToBottomBeforeReceivingJobLog = isScrolledToBottom();
+
+ commit(types.RECEIVE_JOB_LOG_SUCCESS, data);
+
+ if (data.complete) {
+ dispatch('stopPollingJobLog');
+ dispatch('requestTestSummary');
+ } else if (!state.jobLogTimeout) {
+ dispatch('startPollingJobLog');
+ }
+ })
+ // place `scrollBottom` in a separate `then()` block
+ // to wait on related components to update
+ // after the RECEIVE_JOB_LOG_SUCCESS commit
+ .then(() => {
+ if (isScrolledToBottomBeforeReceivingJobLog) {
+ dispatch('scrollBottom');
+ }
+ })
+ .catch((e) => {
+ if (e.response?.status === HTTP_STATUS_FORBIDDEN) {
+ dispatch('receiveJobLogUnauthorizedError');
+ } else {
+ reportToSentry('job_actions', e);
+ dispatch('receiveJobLogError');
+ }
+ })
+ );
+};
export const startPollingJobLog = ({ dispatch, commit }) => {
const jobLogTimeout = setTimeout(() => {
@@ -198,8 +270,6 @@ export const stopPollingJobLog = ({ state, commit }) => {
commit(types.STOP_POLLING_JOB_LOG);
};
-export const receiveJobLogSuccess = ({ commit }, log) => commit(types.RECEIVE_JOB_LOG_SUCCESS, log);
-
export const receiveJobLogError = ({ dispatch }) => {
dispatch('stopPollingJobLog');
createAlert({
@@ -273,3 +343,23 @@ export const triggerManualJob = ({ state }, variables) => {
}),
);
};
+
+export const requestTestSummary = ({ state, commit, dispatch }) => {
+ if (!state.testSummaryComplete && state.testReportSummaryUrl?.length) {
+ axios
+ .get(state.testReportSummaryUrl)
+ .then(({ data }) => {
+ dispatch('receiveTestSummarySuccess', data);
+ })
+ .catch((e) => {
+ reportToSentry('job_test_summary_report', e);
+ })
+ .finally(() => {
+ commit(types.RECEIVE_TEST_SUMMARY_COMPLETE);
+ });
+ }
+};
+
+export const receiveTestSummarySuccess = ({ commit }, data) => {
+ commit(types.RECEIVE_TEST_SUMMARY_SUCCESS, data);
+};
diff --git a/app/assets/javascripts/ci/job_details/store/getters.js b/app/assets/javascripts/ci/job_details/store/getters.js
index a0f9db7409d..db967da87fb 100644
--- a/app/assets/javascripts/ci/job_details/store/getters.js
+++ b/app/assets/javascripts/ci/job_details/store/getters.js
@@ -48,3 +48,6 @@ export const isScrollingDown = (state) => isScrolledToBottom() && !state.isJobLo
export const hasOfflineRunnersForProject = (state) =>
state?.job?.runners?.available && !state?.job?.runners?.online;
+
+export const fullScreenAPIAndContainerAvailable = (state) =>
+ state.fullScreenAPIAvailable && state.fullScreenModeAvailable;
diff --git a/app/assets/javascripts/ci/job_details/store/mutation_types.js b/app/assets/javascripts/ci/job_details/store/mutation_types.js
index e125538317d..382bee9059f 100644
--- a/app/assets/javascripts/ci/job_details/store/mutation_types.js
+++ b/app/assets/javascripts/ci/job_details/store/mutation_types.js
@@ -11,8 +11,6 @@ export const ENABLE_SCROLL_BOTTOM = 'ENABLE_SCROLL_BOTTOM';
export const ENABLE_SCROLL_TOP = 'ENABLE_SCROLL_TOP';
export const TOGGLE_SCROLL_ANIMATION = 'TOGGLE_SCROLL_ANIMATION';
-export const TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG = 'TOGGLE_IS_SCROLL_IN_BOTTOM';
-
export const REQUEST_JOB = 'REQUEST_JOB';
export const RECEIVE_JOB_SUCCESS = 'RECEIVE_JOB_SUCCESS';
export const RECEIVE_JOB_ERROR = 'RECEIVE_JOB_ERROR';
@@ -28,3 +26,11 @@ export const SET_SELECTED_STAGE = 'SET_SELECTED_STAGE';
export const REQUEST_JOBS_FOR_STAGE = 'REQUEST_JOBS_FOR_STAGE';
export const RECEIVE_JOBS_FOR_STAGE_SUCCESS = 'RECEIVE_JOBS_FOR_STAGE_SUCCESS';
export const RECEIVE_JOBS_FOR_STAGE_ERROR = 'RECEIVE_JOBS_FOR_STAGE_ERROR';
+
+export const RECEIVE_TEST_SUMMARY_SUCCESS = 'RECEIVE_TEST_SUMMARY_SUCCESS';
+export const RECEIVE_TEST_SUMMARY_COMPLETE = 'RECEIVE_TEST_SUMMARY_COMPLETE';
+
+export const ENTER_FULLSCREEN_SUCCESS = 'ENTER_FULLSCREEN_SUCCESS';
+export const EXIT_FULLSCREEN_SUCCESS = 'EXIT_FULLSCREEN_SUCCESS';
+export const FULL_SCREEN_CONTAINER_SET_UP = 'FULL_SCREEN_CONTAINER_SET_UP';
+export const FULL_SCREEN_MODE_AVAILABLE_SUCCESS = 'FULL_SCREEN_MODE_AVAILABLE_SUCCESS';
diff --git a/app/assets/javascripts/ci/job_details/store/mutations.js b/app/assets/javascripts/ci/job_details/store/mutations.js
index fe6506bf8a5..866ce48ce9f 100644
--- a/app/assets/javascripts/ci/job_details/store/mutations.js
+++ b/app/assets/javascripts/ci/job_details/store/mutations.js
@@ -1,11 +1,12 @@
-import Vue from 'vue';
import * as types from './mutation_types';
-import { logLinesParser, updateIncrementalJobLog } from './utils';
+import { logLinesParser } from './utils';
export default {
[types.SET_JOB_LOG_OPTIONS](state, options = {}) {
- state.jobLogEndpoint = options.pagePath;
- state.jobEndpoint = options.endpoint;
+ state.jobEndpoint = options.jobEndpoint;
+ state.logEndpoint = options.logEndpoint;
+ state.testReportSummaryUrl = options.testReportSummaryUrl;
+ state.fullScreenAPIAvailable = options.fullScreenAPIAvailable;
},
[types.HIDE_SIDEBAR](state) {
@@ -21,15 +22,27 @@ export default {
}
if (log.append) {
- state.jobLog = log.lines ? updateIncrementalJobLog(log.lines, state.jobLog) : state.jobLog;
-
+ if (log.lines) {
+ const { sections, lines } = logLinesParser(log.lines, {
+ currentLines: state.jobLog,
+ currentSections: state.jobLogSections,
+ });
+
+ state.jobLog = lines;
+ state.jobLogSections = sections;
+ }
state.jobLogSize += log.size;
} else {
// When the job still does not have a log
// the job log response will not have a defined
// html or size. We keep the old value otherwise these
// will be set to `null`
- state.jobLog = log.lines ? logLinesParser(log.lines, [], window.location.hash) : state.jobLog;
+ if (log.lines) {
+ const { sections, lines } = logLinesParser(log.lines, {}, window.location.hash);
+
+ state.jobLog = lines;
+ state.jobLogSections = sections;
+ }
state.jobLogSize = log.size || state.jobLogSize;
}
@@ -63,7 +76,9 @@ export default {
* @param {Object} section
*/
[types.TOGGLE_COLLAPSIBLE_LINE](state, section) {
- Vue.set(section, 'isClosed', !section.isClosed);
+ if (state.jobLogSections[section]) {
+ state.jobLogSections[section].isClosed = !state.jobLogSections[section].isClosed;
+ }
},
[types.REQUEST_JOB](state) {
@@ -110,11 +125,6 @@ export default {
[types.TOGGLE_SCROLL_ANIMATION](state, toggle) {
state.isScrollingDown = toggle;
},
-
- [types.TOGGLE_IS_SCROLL_IN_BOTTOM_BEFORE_UPDATING_JOB_LOG](state, toggle) {
- state.isScrolledToBottomBeforeReceivingJobLog = toggle;
- },
-
[types.REQUEST_JOBS_FOR_STAGE](state, stage = {}) {
state.isLoadingJobs = true;
state.selectedStage = stage.name;
@@ -127,4 +137,22 @@ export default {
state.isLoadingJobs = false;
state.jobs = [];
},
+ [types.RECEIVE_TEST_SUMMARY_SUCCESS](state, testSummary) {
+ state.testSummary = testSummary;
+ },
+ [types.RECEIVE_TEST_SUMMARY_COMPLETE](state) {
+ state.testSummaryComplete = true;
+ },
+ [types.ENTER_FULLSCREEN_SUCCESS](state) {
+ state.fullScreenEnabled = true;
+ },
+ [types.EXIT_FULLSCREEN_SUCCESS](state) {
+ state.fullScreenEnabled = false;
+ },
+ [types.FULL_SCREEN_CONTAINER_SET_UP](state, value) {
+ state.fullScreenContainerSetUp = value;
+ },
+ [types.FULL_SCREEN_MODE_AVAILABLE_SUCCESS](state) {
+ state.fullScreenModeAvailable = true;
+ },
};
diff --git a/app/assets/javascripts/ci/job_details/store/state.js b/app/assets/javascripts/ci/job_details/store/state.js
index dfff65c364d..a3c1e7692c3 100644
--- a/app/assets/javascripts/ci/job_details/store/state.js
+++ b/app/assets/javascripts/ci/job_details/store/state.js
@@ -1,9 +1,12 @@
export default () => ({
jobEndpoint: null,
- jobLogEndpoint: null,
+ logEndpoint: null,
+ testReportSummaryUrl: null,
// sidebar
isSidebarOpen: true,
+ testSummary: {},
+ testSummaryComplete: false,
isLoading: false,
hasError: false,
@@ -13,10 +16,14 @@ export default () => ({
isScrollBottomDisabled: true,
isScrollTopDisabled: true,
- // Used to check if we should keep the automatic scroll
- isScrolledToBottomBeforeReceivingJobLog: true,
+ // fullscreen mode
+ fullScreenAPIAvailable: false,
+ fullScreenModeAvailable: false,
+ fullScreenEnabled: false,
+ fullScreenContainerSetUp: false,
jobLog: [],
+ jobLogSections: {},
isJobLogComplete: false,
jobLogSize: 0,
isJobLogSizeVisible: false,
diff --git a/app/assets/javascripts/ci/job_details/store/utils.js b/app/assets/javascripts/ci/job_details/store/utils.js
index c8b33638821..1536c1140d0 100644
--- a/app/assets/javascripts/ci/job_details/store/utils.js
+++ b/app/assets/javascripts/ci/job_details/store/utils.js
@@ -1,193 +1,105 @@
import { parseBoolean } from '~/lib/utils/common_utils';
/**
- * Adds the line number property
- * @param Object line
- * @param Number lineNumber
- */
-export const parseLine = (line = {}, lineNumber) => ({
- ...line,
- lineNumber,
-});
-
-/**
- * When a line has `section_header` set to true, we create a new
- * structure to allow to nest the lines that belong to the
- * collapsible section
+ * Filters out lines that have an offset lower than the offset provided.
*
- * @param Object line
- * @param Number lineNumber
- */
-export const parseHeaderLine = (line = {}, lineNumber, hash) => {
- let isClosed = parseBoolean(line.section_options?.collapsed);
-
- // if a hash is present in the URL then we ensure
- // all sections are visible so we can scroll to the hash
- // in the DOM
- if (hash) {
- isClosed = false;
- }
-
- return {
- isClosed,
- isHeader: true,
- line: parseLine(line, lineNumber),
- lines: [],
- };
-};
-
-/**
- * Finds the matching header section
- * for the section_duration object and adds it to it
+ * If no offset is provided, all the lines are returned back.
*
- * {
- * isHeader: true,
- * line: {
- * content: [],
- * lineNumber: 0,
- * section_duration: "",
- * },
- * lines: []
- * }
- *
- * @param Array data
- * @param Object durationLine
+ * @param {Array} newLines
+ * @param {Number} offset
+ * @returns Lines to be added to the log that have not been added.
*/
-export function addDurationToHeader(data, durationLine) {
- data.forEach((el) => {
- if (el.line && el.line.section === durationLine.section) {
- el.line.section_duration = durationLine.section_duration;
- }
- });
-}
-
-/**
- * Check is the current section belongs to a collapsible section
- *
- * @param Array acc
- * @param Object last
- * @param Object section
- *
- * @returns Boolean
- */
-export const isCollapsibleSection = (acc = [], last = {}, section = {}) =>
- acc.length > 0 &&
- last.isHeader === true &&
- !section.section_duration &&
- section.section === last.line.section;
-
-/**
- * Returns the next line number in the parsed log
- *
- * @param Array acc
- * @returns Number
- */
-export const getNextLineNumber = (acc) => {
- if (!acc?.length) {
- return 1;
- }
-
- const lastElement = acc[acc.length - 1];
- const nestedLines = lastElement.lines;
-
- if (lastElement.isHeader && !nestedLines.length && lastElement.line) {
- return lastElement.line.lineNumber + 1;
+const linesAfterOffset = (newLines = [], offset = -1) => {
+ if (offset === -1) {
+ return newLines;
}
-
- if (lastElement.isHeader && nestedLines.length) {
- return nestedLines[nestedLines.length - 1].lineNumber + 1;
- }
-
- return lastElement.lineNumber + 1;
+ return newLines.filter((newLine) => newLine.offset > offset);
};
/**
- * Parses the job log content into a structure usable by the template
+ * Parses a series of trace lines from a job and returns lines and
+ * sections of the log. Each line is annotated with a lineNumber.
*
- * For collaspible lines (section_header = true):
- * - creates a new array to hold the lines that are collapsible,
- * - adds a isClosed property to handle toggle
- * - adds a isHeader property to handle template logic
- * - adds the section_duration
- * For each line:
- * - adds the index as lineNumber
+ * Sections have a range: starting line and ending line, plus a
+ * "duration" string.
*
- * @param Array lines
- * @param Array accumulator
- * @returns Array parsed log lines
+ * @param {Array} newLines - Lines to add to the log
+ * @param {Object} currentState - Current log: lines and sections
+ * @returns Consolidated lines and sections to be displayed
*/
-export const logLinesParser = (lines = [], prevLogLines = [], hash = '') =>
- lines.reduce(
- (acc, line) => {
- const lineNumber = getNextLineNumber(acc);
+export const logLinesParser = (
+ newLines = [],
+ { currentLines = [], currentSections = {} } = {},
+ hash = '',
+) => {
+ const lastCurrentLine = currentLines[currentLines.length - 1];
+ const newLinesToAppend = linesAfterOffset(newLines, lastCurrentLine?.offset);
+
+ if (!newLinesToAppend.length) {
+ return { lines: currentLines, sections: currentSections };
+ }
- const last = acc[acc.length - 1];
+ let lineNumber = lastCurrentLine?.lineNumber || 0;
+ const lines = [...currentLines];
+ const sections = { ...currentSections };
+
+ newLinesToAppend.forEach((line) => {
+ const {
+ offset,
+ content,
+ section,
+ section_header: isHeader,
+ section_footer: isFooter,
+ section_duration: duration,
+ section_options: options,
+ } = line;
+
+ if (content.length) {
+ lineNumber += 1;
+ lines.push({
+ offset,
+ lineNumber,
+ content,
+ ...(section ? { section } : {}),
+ ...(isHeader ? { isHeader: true } : {}),
+ });
+ }
- // If the object is an header, we parse it into another structure
- if (line.section_header) {
- acc.push(parseHeaderLine(line, lineNumber, hash));
- } else if (isCollapsibleSection(acc, last, line)) {
- // if the object belongs to a nested section, we append it to the new `lines` array of the
- // previously formatted header
- last.lines.push(parseLine(line, lineNumber));
- } else if (line.section_duration) {
- // if the line has section_duration, we look for the correct header to add it
- addDurationToHeader(acc, line);
- } else {
- // otherwise it's a regular line
- acc.push(parseLine(line, lineNumber));
+ // root level lines have no section, skip creating one
+ if (section) {
+ sections[section] = sections[section] || {
+ startLineNumber: 0,
+ endLineNumber: Infinity, // by default, sections are unbounded / have no end
+ duration: null,
+ isClosed: false,
+ };
+
+ if (isHeader) {
+ sections[section].startLineNumber = lineNumber;
}
-
- return acc;
- },
- [...prevLogLines],
- );
-
-/**
- * Finds the repeated offset, removes the old one
- *
- * Returns a new array with the updated log without
- * the repeated offset
- *
- * @param Array newLog
- * @param Array oldParsed
- * @returns Array
- *
- */
-export const findOffsetAndRemove = (newLog = [], oldParsed = []) => {
- const cloneOldLog = [...oldParsed];
- const lastIndex = cloneOldLog.length - 1;
- const last = cloneOldLog[lastIndex];
-
- const firstNew = newLog[0];
-
- if (last && firstNew) {
- if (last.offset === firstNew.offset || (last.line && last.line.offset === firstNew.offset)) {
- cloneOldLog.splice(lastIndex);
- } else if (last.lines && last.lines.length) {
- const lastNestedIndex = last.lines.length - 1;
- const lastNested = last.lines[lastNestedIndex];
- if (lastNested.offset === firstNew.offset) {
- last.lines.splice(lastNestedIndex);
+ if (options) {
+ let isClosed = parseBoolean(options?.collapsed);
+ // if a hash is present in the URL then we ensure
+ // all sections are visible so we can scroll to the hash
+ // in the DOM
+ if (hash) {
+ isClosed = false;
+ }
+ sections[section].isClosed = isClosed;
+
+ const hideDuration = parseBoolean(options?.hide_duration);
+ if (hideDuration) {
+ sections[section].hideDuration = hideDuration;
+ }
+ }
+ if (duration) {
+ sections[section].duration = duration;
+ }
+ if (isFooter) {
+ sections[section].endLineNumber = lineNumber;
}
}
- }
-
- return cloneOldLog;
-};
-
-/**
- * When the job log is not complete, backend may send the last received line
- * in the new response.
- *
- * We need to check if that is the case by looking for the offset property
- * before parsing the incremental part
- *
- * @param array oldLog
- * @param array newLog
- */
-export const updateIncrementalJobLog = (newLog = [], oldParsed = []) => {
- const parsedLog = findOffsetAndRemove(newLog, oldParsed);
+ });
- return logLinesParser(newLog, parsedLog);
+ return { lines, sections };
};
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
index 3ad2582e36b..458281eb385 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/actions_cell.vue
@@ -84,6 +84,9 @@ export default {
artifactDownloadPath() {
return this.hasArtifacts.downloadPath;
},
+ canCancelJob() {
+ return this.job.userPermissions?.cancelBuild;
+ },
canReadJob() {
return this.job.userPermissions?.readBuild;
},
@@ -185,7 +188,7 @@ export default {
<gl-button-group>
<template v-if="canReadJob && canUpdateJob">
<gl-button
- v-if="isActive"
+ v-if="isActive && canCancelJob"
v-gl-tooltip
icon="cancel"
:title="$options.CANCEL"
diff --git a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
index efa74d86bd6..0ff535add6b 100644
--- a/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
+++ b/app/assets/javascripts/ci/jobs_page/components/job_cells/status_cell.vue
@@ -1,7 +1,7 @@
<script>
import { GlIcon } from '@gitlab/ui';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
diff --git a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
index 69719011079..b1ce3a8597a 100644
--- a/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
+++ b/app/assets/javascripts/ci/jobs_page/graphql/queries/get_jobs.query.graphql
@@ -71,6 +71,7 @@ query getJobs($fullPath: ID!, $after: String, $first: Int = 30, $statuses: [CiJo
readBuild
readJobArtifacts
updateBuild
+ cancelBuild
}
}
}
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
index c6340e6787a..afe66588fb9 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/job_item.vue
@@ -5,7 +5,7 @@ import delayedJobMixin from '~/ci/mixins/delayed_job_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import ActionComponent from '../../../common/private/job_action_component.vue';
import JobNameComponent from '../../../common/private/job_name_component.vue';
import { BRIDGE_KIND, RETRY_ACTION_TITLE, SINGLE_JOB, SKIP_RETRY_MODAL_KEY } from '../constants';
@@ -343,7 +343,7 @@ export default {
</div>
<gl-badge
v-if="isBridge"
- class="gl-mt-3"
+ class="gl-mt-3 gl-ml-7"
variant="info"
size="sm"
data-testid="job-bridge-badge"
diff --git a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
index 26521f87426..76ff662cd3f 100644
--- a/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_details/graph/components/linked_pipeline.vue
@@ -14,7 +14,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { __, sprintf } from '~/locale';
import CancelPipelineMutation from '~/ci/pipeline_details/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/ci/pipeline_details/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { reportToSentry } from '~/ci/utils';
import { ACTION_FAILURE, DOWNSTREAM, UPSTREAM } from '../constants';
@@ -287,7 +287,7 @@ export default {
/>
<div v-else :class="$options.styles.actionSizeClasses"></div>
</div>
- <div class="gl-pt-2">
+ <div class="gl-pt-2 gl-ml-7">
<gl-badge size="sm" variant="info" data-testid="downstream-pipeline-label">
{{ label }}
</gl-badge>
diff --git a/app/assets/javascripts/ci/pipeline_details/header/constants.js b/app/assets/javascripts/ci/pipeline_details/header/constants.js
new file mode 100644
index 00000000000..a4aed7b8f46
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_details/header/constants.js
@@ -0,0 +1,9 @@
+export const DELETE_MODAL_ID = 'pipeline-delete-modal';
+
+export const POLL_INTERVAL = 10000;
+
+export const SCHEDULE_SOURCE = 'schedule';
+export const AUTO_DEVOPS_SOURCE = 'AUTO_DEVOPS_SOURCE';
+export const DETACHED_EVENT_TYPE = 'DETACHED';
+export const MERGED_RESULT_EVENT_TYPE = 'MERGED_RESULT';
+export const MERGE_TRAIN_EVENT_TYPE = 'MERGE_TRAIN';
diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql
new file mode 100644
index 00000000000..80fc8b92a47
--- /dev/null
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql
@@ -0,0 +1,4 @@
+fragment PipelineHeaderData on Pipeline {
+ id
+ iid
+}
diff --git a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
index eb5643126a2..4ef79aaa03c 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
+++ b/app/assets/javascripts/ci/pipeline_details/header/graphql/queries/get_pipeline_header_data.query.graphql
@@ -1,15 +1,16 @@
+#import "ee_else_ce/ci/pipeline_details/header/graphql/fragments/pipeline_header.fragment.graphql"
+
query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
project(fullPath: $fullPath) {
id
pipeline(iid: $iid) {
- id
- iid
status
retryable
cancelable
userPermissions {
destroyPipeline
updatePipeline
+ cancelPipeline
}
detailedStatus {
id
@@ -41,6 +42,19 @@ query getPipelineHeaderData($fullPath: ID!, $iid: ID!) {
finishedAt
queuedDuration
duration
+ name
+ totalJobs
+ refText
+ triggeredByPath
+ stuck
+ child
+ complete
+ latest
+ mergeRequestEventType
+ configSource
+ failureReason
+ source
+ ...PipelineHeaderData
}
}
}
diff --git a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
index 651662d6395..1ecc4b2e1c1 100644
--- a/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
+++ b/app/assets/javascripts/ci/pipeline_details/header/pipeline_details_header.vue
@@ -17,7 +17,7 @@ import { setUrlFragment, redirectTo } from '~/lib/utils/url_utility'; // eslint-
import { __, s__, sprintf, formatNumber } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { LOAD_FAILURE, POST_FAILURE, DELETE_FAILURE, DEFAULT } from '../constants';
@@ -26,9 +26,15 @@ import deletePipelineMutation from '../graphql/mutations/delete_pipeline.mutatio
import retryPipelineMutation from '../graphql/mutations/retry_pipeline.mutation.graphql';
import { getQueryHeaders } from '../graph/utils';
import getPipelineQuery from './graphql/queries/get_pipeline_header_data.query.graphql';
-
-const DELETE_MODAL_ID = 'pipeline-delete-modal';
-const POLL_INTERVAL = 10000;
+import {
+ DELETE_MODAL_ID,
+ POLL_INTERVAL,
+ DETACHED_EVENT_TYPE,
+ AUTO_DEVOPS_SOURCE,
+ SCHEDULE_SOURCE,
+ MERGE_TRAIN_EVENT_TYPE,
+ MERGED_RESULT_EVENT_TYPE,
+} from './constants';
export default {
name: 'PipelineDetailsHeader',
@@ -129,40 +135,14 @@ export default {
},
},
props: {
- name: {
- type: String,
- required: false,
- default: '',
- },
- totalJobs: {
- type: String,
- required: false,
- default: '',
- },
- computeMinutes: {
- type: String,
- required: false,
- default: '',
- },
yamlErrors: {
type: String,
required: false,
default: '',
},
- failureReason: {
- type: String,
- required: false,
- default: '',
- },
- refText: {
- type: String,
- required: false,
- default: '',
- },
- badges: {
- type: Object,
- required: false,
- default: () => {},
+ trigger: {
+ type: Boolean,
+ required: true,
},
},
apollo: {
@@ -270,7 +250,7 @@ export default {
},
totalJobsText() {
return sprintf(__('%{jobs} Jobs'), {
- jobs: this.totalJobs,
+ jobs: this.pipeline?.totalJobs || 0,
});
},
triggeredText() {
@@ -312,10 +292,61 @@ export default {
canCancelPipeline() {
const { cancelable, userPermissions } = this.pipeline;
- return cancelable && userPermissions.updatePipeline;
+ return cancelable && userPermissions.cancelPipeline;
+ },
+ computeMinutes() {
+ return this.pipeline?.computeMinutes;
},
showComputeMinutes() {
- return this.isFinished && this.computeMinutes !== '0.0';
+ return this.isFinished && this.computeMinutes;
+ },
+ pipelineName() {
+ return this.pipeline?.name;
+ },
+ refText() {
+ return this.pipeline?.refText;
+ },
+ triggeredByPath() {
+ return this.pipeline?.triggeredByPath;
+ },
+ mergeRequestEventType() {
+ return this.pipeline.mergeRequestEventType;
+ },
+ isMergeTrainPipeline() {
+ return this.mergeRequestEventType === MERGE_TRAIN_EVENT_TYPE;
+ },
+ isMergedResultsPipeline() {
+ return this.mergeRequestEventType === MERGED_RESULT_EVENT_TYPE;
+ },
+ isDetachedPipeline() {
+ return this.mergeRequestEventType === DETACHED_EVENT_TYPE;
+ },
+ isAutoDevopsPipeline() {
+ return this.pipeline.configSource === AUTO_DEVOPS_SOURCE;
+ },
+ isScheduledPipeline() {
+ return this.pipeline.source === SCHEDULE_SOURCE;
+ },
+ isInvalidPipeline() {
+ return Boolean(this.yamlErrors);
+ },
+ failureReason() {
+ return this.pipeline.failureReason;
+ },
+ badges() {
+ return {
+ schedule: this.isScheduledPipeline,
+ trigger: this.trigger,
+ invalid: this.isInvalidPipeline,
+ child: this.pipeline.child,
+ latest: this.pipeline.latest,
+ mergeTrainPipeline: this.isMergeTrainPipeline,
+ mergedResultsPipeline: this.isMergedResultsPipeline,
+ detached: this.isDetachedPipeline,
+ failed: Boolean(this.failureReason),
+ autoDevops: this.isAutoDevopsPipeline,
+ stuck: this.pipeline.stuck,
+ };
},
},
methods: {
@@ -406,7 +437,9 @@ export default {
<gl-loading-icon v-if="loading" class="gl-text-left" size="lg" />
<div v-else class="gl-display-flex gl-justify-content-space-between gl-flex-wrap">
<div>
- <h3 v-if="name" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">{{ name }}</h3>
+ <h3 v-if="pipelineName" class="gl-mt-0 gl-mb-3" data-testid="pipeline-name">
+ {{ pipelineName }}
+ </h3>
<h3 v-else class="gl-mt-0 gl-mb-3" data-testid="pipeline-commit-title">
{{ commitTitle }}
</h3>
@@ -483,7 +516,7 @@ export default {
>
<gl-sprintf :message="$options.i18n.childBadgeText">
<template #link="{ content }">
- <gl-link :href="paths.triggeredByPath" target="_blank">
+ <gl-link :href="triggeredByPath" target="_blank">
{{ content }}
</gl-link>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
index 287f6e045c6..1823908c231 100644
--- a/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
+++ b/app/assets/javascripts/ci/pipeline_details/jobs/components/failed_jobs_table.vue
@@ -5,7 +5,7 @@ import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
import Tracking from '~/tracking';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { TRACKING_CATEGORIES } from '~/ci/constants';
import RetryFailedJobMutation from '../graphql/mutations/retry_failed_job.mutation.graphql';
import { DEFAULT_FIELDS } from '../../constants';
diff --git a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
index 4966b657887..0430bc83dd7 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipeline_details_header.js
@@ -12,29 +12,7 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
return;
}
- const {
- fullPath,
- pipelineIid,
- pipelinesPath,
- name,
- totalJobs,
- computeMinutes,
- yamlErrors,
- failureReason,
- triggeredByPath,
- schedule,
- trigger,
- child,
- latest,
- mergeTrainPipeline,
- mergedResultsPipeline,
- invalid,
- failed,
- autoDevops,
- detached,
- stuck,
- refText,
- } = el.dataset;
+ const { fullPath, pipelineIid, pipelinesPath, yamlErrors, trigger } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -46,32 +24,14 @@ export const createPipelineDetailsHeaderApp = (elSelector, apolloProvider, graph
fullProject: fullPath,
graphqlResourceEtag,
pipelinesPath,
- triggeredByPath,
},
pipelineIid,
},
render(createElement) {
return createElement(PipelineDetailsHeader, {
props: {
- name,
- totalJobs,
- computeMinutes,
yamlErrors,
- failureReason,
- refText,
- badges: {
- schedule: parseBoolean(schedule),
- trigger: parseBoolean(trigger),
- child: parseBoolean(child),
- latest: parseBoolean(latest),
- mergeTrainPipeline: parseBoolean(mergeTrainPipeline),
- mergedResultsPipeline: parseBoolean(mergedResultsPipeline),
- invalid: parseBoolean(invalid),
- failed: parseBoolean(failed),
- autoDevops: parseBoolean(autoDevops),
- detached: parseBoolean(detached),
- stuck: parseBoolean(stuck),
- },
+ trigger: parseBoolean(trigger),
},
});
},
diff --git a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
index ea2875713a9..b4528ab895d 100644
--- a/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
+++ b/app/assets/javascripts/ci/pipeline_details/pipelines_index.js
@@ -44,6 +44,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
params,
fullPath,
visibilityPipelineIdType,
+ showJenkinsCiPrompt,
} = el.dataset;
return new Vue({
@@ -57,6 +58,7 @@ export const initPipelinesIndex = (selector = '#pipelines-list-vue') => {
pipelineEditorPath,
pipelineSchedulesPath,
suggestedCiTemplates: JSON.parse(suggestedCiTemplates),
+ showJenkinsCiPrompt: parseBoolean(showJenkinsCiPrompt),
},
data() {
return {
diff --git a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
index 6b616601bc5..e3984685094 100644
--- a/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
+++ b/app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js
@@ -1,4 +1,5 @@
import { __, sprintf } from '~/locale';
+import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
import { TestStatus } from '../../constants';
/**
@@ -25,15 +26,27 @@ export function iconForTestStatus(status) {
return 'status_notfound';
}
}
-
export const formattedTime = (seconds = 0) => {
if (seconds < 1) {
- const milliseconds = seconds * 1000;
- return sprintf(__('%{milliseconds}ms'), { milliseconds: milliseconds.toFixed(2) });
+ return sprintf(__('%{milliseconds}ms'), {
+ milliseconds: (seconds * 1000).toFixed(2),
+ });
+ }
+ if (seconds < 60) {
+ return sprintf(__('%{seconds}s'), {
+ seconds: (seconds % 60).toFixed(2),
+ });
}
- return sprintf(__('%{seconds}s'), { seconds: seconds.toFixed(2) });
-};
+ const hoursAndMinutes = stringifyTime(parseSeconds(seconds));
+ const remainingSeconds =
+ seconds % 60 >= 1
+ ? sprintf(__('%{seconds}s'), {
+ seconds: Math.floor(seconds % 60),
+ })
+ : '';
+ return `${hoursAndMinutes} ${remainingSeconds}`.trim();
+};
export const addIconStatus = (testCase) => ({
...testCase,
icon: iconForTestStatus(testCase.status),
diff --git a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
index a7737d33285..6e9a705c046 100644
--- a/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
+++ b/app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue
@@ -2,6 +2,7 @@
import { GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
+import { getParameterValues } from '~/lib/utils/url_utility';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
@@ -19,7 +20,7 @@ export default {
inject: ['blobPath', 'summaryEndpoint', 'suiteEndpoint'],
computed: {
...mapState('testReports', ['isLoading', 'selectedSuiteIndex', 'testReports']),
- ...mapGetters('testReports', ['getSelectedSuite']),
+ ...mapGetters('testReports', ['getSelectedSuite', 'getTestSuites']),
showSuite() {
return this.selectedSuiteIndex !== null;
},
@@ -28,8 +29,16 @@ export default {
return testSuites.length > 0;
},
},
- created() {
- this.fetchSummary();
+ async created() {
+ await this.fetchSummary();
+ const jobName = getParameterValues('job_name')[0] || '';
+ if (jobName.length > 0) {
+ // get the index from the job name
+ const indexToSelect = this.getTestSuites.findIndex((test) => test.name === jobName);
+
+ this.setSelectedSuiteIndex(indexToSelect);
+ this.fetchTestSuite(indexToSelect);
+ }
},
methods: {
...mapActions('testReports', [
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
index 204eaf20664..956f02de09d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/editor/ci_editor_header.vue
@@ -4,7 +4,6 @@ import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- EDITOR_APP_DRAWER_AI_ASSISTANT,
EDITOR_APP_DRAWER_HELP,
EDITOR_APP_DRAWER_JOB_ASSISTANT,
EDITOR_APP_DRAWER_NONE,
@@ -14,17 +13,17 @@ import {
export default {
i18n: {
+ browseCatalog: __('Browse CI/CD Catalog'),
browseTemplates: __('Browse templates'),
help: __('Help'),
jobAssistant: s__('JobAssistant|Job assistant'),
- aiAssistant: s__('PipelinesAiAssistant|Ai assistant'),
},
TEMPLATE_REPOSITORY_URL,
components: {
GlButton,
},
mixins: [glFeatureFlagMixin(), Tracking.mixin()],
- inject: ['aiChatAvailable'],
+ inject: ['ciCatalogPath'],
props: {
showHelpDrawer: {
type: Boolean,
@@ -34,15 +33,6 @@ export default {
type: Boolean,
required: true,
},
- showAiAssistantDrawer: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- isAiConfigChatAvailable() {
- return this.glFeatures.aiCiConfigGenerator && this.aiChatAvailable;
- },
},
methods: {
toggleHelpDrawer() {
@@ -59,11 +49,10 @@ export default {
this.showJobAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_JOB_ASSISTANT,
);
},
- toggleAiAssistantDrawer() {
- this.$emit(
- 'switch-drawer',
- this.showAiAssistantDrawer ? EDITOR_APP_DRAWER_NONE : EDITOR_APP_DRAWER_AI_ASSISTANT,
- );
+ trackCatalogBrowsing() {
+ const { label, actions } = pipelineEditorTrackingOptions;
+
+ this.track(actions.browseCatalog, { label });
},
trackHelpDrawerClick() {
const { label, actions } = pipelineEditorTrackingOptions;
@@ -84,6 +73,16 @@ export default {
>
<slot></slot>
<gl-button
+ :href="ciCatalogPath"
+ size="small"
+ icon="external-link"
+ target="_blank"
+ data-testid="catalog-repo-link"
+ @click="trackCatalogBrowsing"
+ >
+ {{ $options.i18n.browseCatalog }}
+ </gl-button>
+ <gl-button
:href="$options.TEMPLATE_REPOSITORY_URL"
size="small"
icon="external-link"
@@ -109,14 +108,5 @@ export default {
>
{{ $options.i18n.jobAssistant }}
</gl-button>
- <gl-button
- v-if="isAiConfigChatAvailable"
- icon="bulb"
- size="small"
- data-testid="ai-assistant-drawer-toggle"
- @click="toggleAiAssistantDrawer"
- >
- {{ $options.i18n.aiAssistant }}
- </gl-button>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
index 21e21d54758..0064dc51d97 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/file_nav/branch_switcher.vue
@@ -105,9 +105,6 @@ export default {
branchesData() {
return this.availableBranches.map((branch) => ({
text: branch,
- extraAttrs: {
- 'data-qa-selector': 'branch_menu_item_button',
- },
value: branch,
}));
},
@@ -211,7 +208,6 @@ export default {
<gl-collapsible-listbox
v-model="currentBranch"
v-gl-tooltip.hover
- data-qa-selector="branch_selector_button"
searchable
:items="branchesData"
:title="$options.i18n.dropdownHeader"
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
index f00098105d3..f76243e81b9 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue
@@ -19,6 +19,11 @@ export default {
required: true,
},
},
+ data() {
+ return {
+ linkedPipelines: null,
+ };
+ },
apollo: {
linkedPipelines: {
query: getLinkedPipelinesQuery,
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
index 7c4a07e3f83..9c1bbff1cc4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/header/pipeline_status.vue
@@ -7,7 +7,7 @@ import getPipelineQuery from '~/ci/pipeline_editor/graphql/queries/pipeline.quer
import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import PipelineMiniGraph from '~/ci/pipeline_mini_graph/pipeline_mini_graph.vue';
import PipelineEditorMiniGraph from './pipeline_editor_mini_graph.vue';
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
index c7c15cdd76e..cd6150031d4 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/pipeline_editor_tabs.vue
@@ -1,6 +1,5 @@
<script>
import { GlAlert, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import CiEditorHeader from 'ee_else_ce/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import { s__, __ } from '~/locale';
import PipelineGraph from '~/ci/pipeline_editor/components/graph/pipeline_graph.vue';
import { getParameterValues, setUrlParams, updateHistory } from '~/lib/utils/url_utility';
@@ -20,6 +19,7 @@ import {
} from '../constants';
import getAppStatus from '../graphql/queries/client/app_status.query.graphql';
import CiConfigMergedPreview from './editor/ci_config_merged_preview.vue';
+import CiEditorHeader from './editor/ci_editor_header.vue';
import CiValidate from './validate/ci_validate.vue';
import TextEditor from './editor/text_editor.vue';
import EditorTab from './ui/editor_tab.vue';
@@ -95,10 +95,6 @@ export default {
type: Boolean,
required: true,
},
- showAiAssistantDrawer: {
- type: Boolean,
- required: true,
- },
isNewCiConfigFile: {
type: Boolean,
required: true,
@@ -198,7 +194,6 @@ export default {
<ci-editor-header
:show-help-drawer="showHelpDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
- :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
/>
<text-editor :commit-sha="commitSha" :value="ciFileContent" v-on="$listeners" />
diff --git a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
index 617088f303b..1d152a63407 100644
--- a/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue
@@ -9,6 +9,7 @@ import {
GlTooltip,
GlTooltipDirective,
GlSprintf,
+ GlEmptyState,
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
@@ -67,6 +68,7 @@ export default {
GlLink,
GlSprintf,
GlTooltip,
+ GlEmptyState,
ValidatePipelinePopover,
},
directives: {
@@ -226,38 +228,44 @@ export default {
</gl-button>
</div>
</div>
- <div v-if="isInitState" :class="$options.BASE_CLASSES">
- <img :src="validateTabIllustrationPath" />
- <h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.title }}</h1>
- <ul>
- <li class="gl-mb-3">{{ $options.i18n.contentNote }}</li>
- <li class="gl-mb-3">
+ <gl-empty-state
+ v-if="isInitState"
+ :svg-path="validateTabIllustrationPath"
+ :title="$options.i18n.title"
+ :primary-button-link="validateYaml"
+ :primary-button-text="$options.i18n.cta"
+ >
+ <template #description>
+ <p>{{ $options.i18n.contentNote }}</p>
+ <p>
<gl-sprintf :message="$options.i18n.simulationNote">
<template #code="{ content }">
<code>{{ content }}</code>
</template>
</gl-sprintf>
- </li>
- </ul>
- <div ref="simulatePipelineButton">
- <gl-button
- ref="simulatePipelineButton"
- variant="confirm"
- class="gl-mt-3"
- :disabled="isInitialCiContentLoading"
- data-testid="simulate-pipeline-button"
- @click="validateYaml"
- >
- {{ $options.i18n.cta }}
- </gl-button>
- </div>
- <gl-tooltip
- v-if="isInitialCiContentLoading"
- :target="() => $refs.simulatePipelineButton"
- :title="$options.i18n.ctaDisabledTooltip"
- data-testid="cta-tooltip"
- />
- </div>
+ </p>
+ </template>
+ <template #actions>
+ <div ref="simulatePipelineButton">
+ <gl-button
+ ref="simulatePipelineButton"
+ variant="confirm"
+ class="gl-mt-3"
+ :disabled="isInitialCiContentLoading"
+ data-testid="simulate-pipeline-button"
+ @click="validateYaml"
+ >
+ {{ $options.i18n.cta }}
+ </gl-button>
+ </div>
+ <gl-tooltip
+ v-if="isInitialCiContentLoading"
+ :target="() => $refs.simulatePipelineButton"
+ :title="$options.i18n.ctaDisabledTooltip"
+ data-testid="cta-tooltip"
+ />
+ </template>
+ </gl-empty-state>
<div v-else-if="isSimulationLoading" :class="$options.BASE_CLASSES">
<gl-loading-icon size="lg" class="gl-m-3" />
<h1 class="gl-font-size-h1 gl-mb-6">{{ $options.i18n.loading }}</h1>
diff --git a/app/assets/javascripts/ci/pipeline_editor/constants.js b/app/assets/javascripts/ci/pipeline_editor/constants.js
index e85138e361f..66725df15f0 100644
--- a/app/assets/javascripts/ci/pipeline_editor/constants.js
+++ b/app/assets/javascripts/ci/pipeline_editor/constants.js
@@ -1,4 +1,5 @@
import { s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
export const EDITOR_APP_DRAWER_HELP = 'HELP';
export const EDITOR_APP_DRAWER_JOB_ASSISTANT = 'JOB_ASSISTANT';
@@ -93,6 +94,9 @@ export const VALIDATE_TAB_FEEDBACK_URL = 'https://gitlab.com/gitlab-org/gitlab/-
export const COMMIT_SHA_POLL_INTERVAL = 1000;
+export const MIGRATION_PLAN_HELP_PATH = helpPagePath('ci/migration/plan_a_migration');
+export const MIGRATE_FROM_JENKINS_TRACKING_LABEL = 'migrate_from_jenkins_prompt';
+
export const I18N = {
title: s__('Pipelines|Get started with GitLab CI/CD'),
learnBasics: {
@@ -107,6 +111,13 @@ export const I18N = {
),
cta: s__('Pipelines|Try test template'),
},
+ migrateFromJenkins: {
+ title: s__('Pipelines|Migrate to GitLab CI/CD from Jenkins'),
+ description: s__(
+ 'Pipelines|Take advantage of simple, scalable pipelines and CI/CD-enabled features. You can view integration results, security scans, tests, code coverage and more directly in merge requests!',
+ ),
+ cta: s__('Pipelines|Start with a migration plan'),
+ },
},
templates: {
title: s__('Pipelines|Ready to set up CI/CD for your project?'),
diff --git a/app/assets/javascripts/ci/pipeline_editor/index.js b/app/assets/javascripts/ci/pipeline_editor/index.js
index bc20e478876..408e91a4d62 100644
--- a/app/assets/javascripts/ci/pipeline_editor/index.js
+++ b/app/assets/javascripts/ci/pipeline_editor/index.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { createAppOptions } from 'ee_else_ce/ci/pipeline_editor/options';
+import { createAppOptions } from '~/ci/pipeline_editor/options';
export const initPipelineEditor = (selector = '#js-pipeline-editor') => {
const el = document.querySelector(selector);
diff --git a/app/assets/javascripts/ci/pipeline_editor/options.js b/app/assets/javascripts/ci/pipeline_editor/options.js
index 340cb6ab979..9520295c94d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/options.js
+++ b/app/assets/javascripts/ci/pipeline_editor/options.js
@@ -19,6 +19,7 @@ export const createAppOptions = (el) => {
initialBranchName,
pipelineEtag,
// Add to provide/inject API for static values
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
@@ -40,7 +41,6 @@ export const createAppOptions = (el) => {
usesExternalConfig,
validateTabIllustrationPath,
ymlHelpPagePath,
- aiChatAvailable,
} = el.dataset;
const configurationPaths = Object.fromEntries(
@@ -109,7 +109,7 @@ export const createAppOptions = (el) => {
el,
apolloProvider,
provide: {
- aiChatAvailable: parseBoolean(aiChatAvailable),
+ ciCatalogPath,
ciConfigPath,
ciExamplesHelpPagePath,
ciHelpPagePath,
diff --git a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
index 09ba6292e13..ca2e1fbf37d 100644
--- a/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
+++ b/app/assets/javascripts/ci/pipeline_editor/pipeline_editor_home.vue
@@ -2,7 +2,6 @@
import { GlModal } from '@gitlab/ui';
import { __ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import JobAssistantDrawer from 'jh_else_ce/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import CommitSection from './components/commit/commit_section.vue';
import PipelineEditorDrawer from './components/drawer/pipeline_editor_drawer.vue';
@@ -19,9 +18,6 @@ import {
EDITOR_APP_DRAWER_NONE,
} from './constants';
-const AiAssistantDrawer = () =>
- import('ee_component/ci/pipeline_editor/components/ai_assistant_drawer.vue');
-
export default {
EDITOR_APP_DRAWER_HELP,
EDITOR_APP_DRAWER_JOB_ASSISTANT,
@@ -45,13 +41,11 @@ export default {
GlModal,
PipelineEditorDrawer,
JobAssistantDrawer,
- AiAssistantDrawer,
PipelineEditorFileNav,
PipelineEditorFileTree,
PipelineEditorHeader,
PipelineEditorTabs,
},
- mixins: [glFeatureFlagMixin()],
props: {
ciConfigData: {
type: Object,
@@ -105,9 +99,6 @@ export default {
showJobAssistantDrawer() {
return this.currentDrawer === EDITOR_APP_DRAWER_JOB_ASSISTANT;
},
- showAiAssistantDrawer() {
- return this.currentDrawer === EDITOR_APP_DRAWER_AI_ASSISTANT;
- },
},
mounted() {
this.showFileTree = JSON.parse(localStorage.getItem(FILE_TREE_DISPLAY_KEY)) || false;
@@ -189,7 +180,6 @@ export default {
:is-new-ci-config-file="isNewCiConfigFile"
:show-help-drawer="showHelpDrawer"
:show-job-assistant-drawer="showJobAssistantDrawer"
- :show-ai-assistant-drawer="showAiAssistantDrawer"
v-on="$listeners"
@switch-drawer="switchDrawer"
@set-current-tab="setCurrentTab"
@@ -222,11 +212,5 @@ export default {
v-on="$listeners"
@switch-drawer="switchDrawer"
/>
- <ai-assistant-drawer
- v-if="glFeatures.aiCiConfigGenerator"
- :is-visible="showAiAssistantDrawer"
- :z-index="drawerIndex[$options.EDITOR_APP_DRAWER_AI_ASSISTANT]"
- @switch-drawer="switchDrawer"
- />
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
index 4fded3aec60..4238f0e3872 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_job_item.vue
@@ -38,7 +38,7 @@ export default {
},
tooltipConfig: {
boundary: 'viewport',
- placement: 'bottom',
+ placement: 'top',
customClass: 'gl-pointer-events-none',
},
components: {
@@ -161,7 +161,6 @@ export default {
:tooltip-text="jobActionTooltipText"
:link="status.action.path"
:action-icon="status.action.icon"
- data-qa-selector="action_button"
/>
</div>
</template>
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
index ed78a335453..38a071a0319 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/legacy_pipeline_stage.vue
@@ -13,7 +13,7 @@
*/
import { GlDropdown, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import eventHub from '~/ci/event_hub';
import axios from '~/lib/utils/axios_utils';
diff --git a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
index f6a375ab94c..bbe17a3eb22 100644
--- a/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
+++ b/app/assets/javascripts/ci/pipeline_mini_graph/linked_pipelines_mini_list.vue
@@ -1,7 +1,7 @@
<script>
import { GlTooltipDirective } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { accessValue } from './accessors/linked_pipelines_accessors';
/**
* Renders the upstream/downstream portions of the pipeline mini graph.
@@ -92,7 +92,7 @@ export default {
'is-upstream': isUpstream,
'is-downstream': isDownstream,
}"
- class="linked-pipeline-mini-list gl-display-inline gl-vertical-align-middle"
+ class="linked-pipeline-mini-list gl-display-inline-flex gl-gap-2 gl-vertical-align-middle"
>
<ci-icon
v-for="pipeline in linkedPipelinesTrimmed"
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
index 5444e66cbdf..44a377144a5 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/pipeline_schedules_form.vue
@@ -391,7 +391,6 @@ export default {
:placeholder="s__('CiVariables|Input variable key')"
:class="$options.formElementClasses"
data-testid="pipeline-form-ci-variable-key"
- data-qa-selector="ci_variable_key_field"
@change="addEmptyVariable(variable)"
/>
@@ -411,7 +410,6 @@ export default {
class="gl-mb-3 gl-h-7!"
:no-resize="false"
data-testid="pipeline-form-ci-variable-value"
- data-qa-selector="ci_variable_value_field"
@change="resetVariable(index)"
/>
diff --git a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
index d979c0efaf2..245d4257bbb 100644
--- a/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
+++ b/app/assets/javascripts/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue
@@ -1,5 +1,5 @@
<script>
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
index a6297213402..c9631d8f36b 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue
@@ -1,7 +1,12 @@
<script>
import { GlButton, GlCard, GlSprintf } from '@gitlab/ui';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { STARTER_TEMPLATE_NAME, I18N } from '~/ci/pipeline_editor/constants';
+import {
+ STARTER_TEMPLATE_NAME,
+ I18N,
+ MIGRATION_PLAN_HELP_PATH,
+ MIGRATE_FROM_JENKINS_TRACKING_LABEL,
+} from '~/ci/pipeline_editor/constants';
import Tracking from '~/tracking';
import CiTemplates from './ci_templates.vue';
@@ -15,7 +20,7 @@ export default {
mixins: [Tracking.mixin()],
STARTER_TEMPLATE_NAME,
I18N,
- inject: ['pipelineEditorPath'],
+ inject: ['pipelineEditorPath', 'showJenkinsCiPrompt'],
data() {
return {
gettingStartedTemplateUrl: mergeUrlParams(
@@ -23,17 +28,23 @@ export default {
this.pipelineEditorPath,
),
tracker: null,
+ migrationPlanUrl: MIGRATION_PLAN_HELP_PATH,
+ migrationPromptTrackingLabel: MIGRATE_FROM_JENKINS_TRACKING_LABEL,
};
},
+ mounted() {
+ if (this.showJenkinsCiPrompt) {
+ this.trackEvent('render', this.migrationPromptTrackingLabel);
+ }
+ },
methods: {
- trackEvent(template) {
- this.track('template_clicked', {
- label: template,
- });
+ trackEvent(action, label) {
+ this.track(action, { label });
},
},
};
</script>
+
<template>
<div>
<h2 class="gl-font-size-h2 gl-text-gray-900">{{ $options.I18N.title }}</h2>
@@ -47,28 +58,62 @@ export default {
</gl-sprintf>
</p>
- <div class="gl-lg-w-25p gl-lg-pr-5 gl-mb-8">
- <gl-card>
- <div class="gl-flex-direction-row">
- <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
- <div class="gl-mb-3">
- <strong class="gl-text-gray-800 gl-mb-2">
- {{ $options.I18N.learnBasics.gettingStarted.title }}
- </strong>
+ <div class="gl-display-flex gl-flex-direction-row gl-flex-wrap">
+ <div
+ v-if="showJenkinsCiPrompt"
+ class="gl-lg-w-25p gl-md-w-half gl-w-full gl-md-pr-5 gl-pb-8"
+ data-testid="migrate-from-jenkins-prompt"
+ >
+ <gl-card class="gl-bg-blue-50">
+ <div class="gl-flex-direction-row">
+ <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="rocket" /></div>
+ <div class="gl-mb-3">
+ <strong class="gl-text-gray-800 gl-mb-2">{{
+ $options.I18N.learnBasics.migrateFromJenkins.title
+ }}</strong>
+ </div>
+ <p class="gl-font-sm gl-h-13">
+ {{ $options.I18N.learnBasics.migrateFromJenkins.description }}
+ </p>
+ </div>
+
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :href="migrationPlanUrl"
+ target="_blank"
+ @click="trackEvent('template_clicked', migrationPromptTrackingLabel)"
+ >
+ {{ $options.I18N.learnBasics.migrateFromJenkins.cta }}
+ </gl-button>
+ </gl-card>
+ </div>
+
+ <div class="gl-lg-w-25p gl-md-w-half gl-w-full gl-pb-8">
+ <gl-card>
+ <div class="gl-flex-direction-row">
+ <div class="gl-py-5"><gl-emoji class="gl-font-size-h2-xl" data-name="wave" /></div>
+ <div class="gl-mb-3">
+ <strong class="gl-text-gray-800 gl-mb-2">
+ {{ $options.I18N.learnBasics.gettingStarted.title }}
+ </strong>
+ </div>
+ <p class="gl-font-sm gl-h-13">
+ {{ $options.I18N.learnBasics.gettingStarted.description }}
+ </p>
</div>
- <p class="gl-font-sm">{{ $options.I18N.learnBasics.gettingStarted.description }}</p>
- </div>
- <gl-button
- category="primary"
- variant="confirm"
- :href="gettingStartedTemplateUrl"
- data-testid="test-template-link"
- @click="trackEvent($options.STARTER_TEMPLATE_NAME)"
- >
- {{ $options.I18N.learnBasics.gettingStarted.cta }}
- </gl-button>
- </gl-card>
+ <gl-button
+ category="primary"
+ variant="confirm"
+ :href="gettingStartedTemplateUrl"
+ data-testid="test-template-link"
+ @click="trackEvent('template_clicked', $options.STARTER_TEMPLATE_NAME)"
+ >
+ {{ $options.I18N.learnBasics.gettingStarted.cta }}
+ </gl-button>
+ </gl-card>
+ </div>
</div>
<h2 class="gl-font-lg gl-text-gray-900">{{ $options.I18N.templates.title }}</h2>
diff --git a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
index 82f1d57912a..7a49bf6a809 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/failure_widget/failed_job_details.vue
@@ -3,7 +3,7 @@ import { GlButton, GlIcon, GlLink, GlTooltip } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { __, s__, sprintf } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { BRIDGE_KIND } from '~/ci/pipeline_details/graph/constants';
import RetryMrFailedJobMutation from '~/ci/merge_requests/graphql/mutations/retry_mr_failed_job.mutation.graphql';
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
index 380f8ce172f..2d5fb8c9799 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_status_badge.vue
@@ -1,6 +1,6 @@
<script>
import { TRACKING_CATEGORIES } from '~/ci/constants';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import Tracking from '~/tracking';
import PipelinesTimeago from './time_ago.vue';
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
index d62a68f0dcc..9ccb7012897 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipeline_stop_modal.vue
@@ -2,7 +2,7 @@
import { GlLink, GlModal, GlSprintf } from '@gitlab/ui';
import { isEmpty } from 'lodash';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
/**
* Pipeline Stop Modal.
diff --git a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
index 3021b4a2ef8..a45387ca676 100644
--- a/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
+++ b/app/assets/javascripts/ci/pipelines_page/components/pipelines_artifacts.vue
@@ -70,3 +70,11 @@ export default {
:items="items"
/>
</template>
+
+<style scoped>
+/* TODO: Use max-height prop when gitlab-ui got updated.
+See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2374 */
+::v-deep .gl-new-dropdown-inner {
+ max-height: 310px !important;
+}
+</style>
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js b/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
deleted file mode 100644
index 04aca11b945..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/actions.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import pollUntilComplete from '~/lib/utils/poll_until_complete';
-import { STATUS_NOT_FOUND } from '../../constants';
-import * as types from './mutation_types';
-import { parseCodeclimateMetrics } from './utils/codequality_parser';
-
-export const setPaths = ({ commit }, paths) => commit(types.SET_PATHS, paths);
-
-export const fetchReports = ({ state, dispatch, commit }) => {
- commit(types.REQUEST_REPORTS);
-
- return pollUntilComplete(state.reportsPath)
- .then(({ data }) => {
- if (data.status === STATUS_NOT_FOUND) {
- return dispatch('receiveReportsError', data);
- }
- return dispatch('receiveReportsSuccess', {
- newIssues: parseCodeclimateMetrics(data.new_errors, state.headBlobPath),
- resolvedIssues: parseCodeclimateMetrics(data.resolved_errors, state.baseBlobPath),
- });
- })
- .catch((error) => dispatch('receiveReportsError', error));
-};
-
-export const receiveReportsSuccess = ({ commit }, data) => {
- commit(types.RECEIVE_REPORTS_SUCCESS, data);
-};
-
-export const receiveReportsError = ({ commit }, error) => {
- commit(types.RECEIVE_REPORTS_ERROR, error);
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/getters.js b/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
deleted file mode 100644
index 70d11e96a54..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/getters.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { spriteIcon } from '~/lib/utils/common_utils';
-import { sprintf, s__, n__ } from '~/locale';
-import { LOADING, ERROR, SUCCESS, STATUS_NOT_FOUND } from '../../constants';
-
-export const hasCodequalityIssues = (state) =>
- Boolean(state.newIssues?.length || state.resolvedIssues?.length);
-
-export const codequalityStatus = (state) => {
- if (state.isLoading) {
- return LOADING;
- }
- if (state.hasError) {
- return ERROR;
- }
-
- return SUCCESS;
-};
-
-export const codequalityText = (state) => {
- const { newIssues, resolvedIssues } = state;
- let text;
- if (!newIssues.length && !resolvedIssues.length) {
- text = s__('ciReport|No changes to code quality');
- } else if (newIssues.length && resolvedIssues.length) {
- text = sprintf(
- s__(`ciReport|Code quality scanning detected %{issueCount} changes in merged results`),
- {
- issueCount: newIssues.length + resolvedIssues.length,
- },
- );
- } else if (resolvedIssues.length) {
- text = n__(
- `ciReport|Code quality improved due to 1 resolved issue`,
- `ciReport|Code quality improved due to %d resolved issues`,
- resolvedIssues.length,
- );
- } else if (newIssues.length) {
- text = n__(
- `ciReport|Code quality degraded due to 1 new issue`,
- `ciReport|Code quality degraded due to %d new issues`,
- newIssues.length,
- );
- }
-
- return text;
-};
-
-export const codequalityPopover = (state) => {
- if (state.status === STATUS_NOT_FOUND) {
- return {
- title: s__('ciReport|Base pipeline codequality artifact not found'),
- content: sprintf(
- s__('ciReport|%{linkStartTag}Learn more about codequality reports %{linkEndTag}'),
- {
- linkStartTag: `<a href="${state.helpPath}" target="_blank" rel="noopener noreferrer">`,
- linkEndTag: `${spriteIcon('external-link', 's16')}</a>`,
- },
- false,
- ),
- };
- }
- return {};
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/index.js b/app/assets/javascripts/ci/reports/codequality_report/store/index.js
deleted file mode 100644
index c2f706e56e6..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/index.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = (initialState) => ({
- actions,
- getters,
- mutations,
- state: state(initialState),
-});
-
-export default (initialState) => new Vuex.Store(getStoreConfig(initialState));
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
deleted file mode 100644
index c362c973ae1..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/mutation_types.js
+++ /dev/null
@@ -1,5 +0,0 @@
-export const SET_PATHS = 'SET_PATHS';
-
-export const REQUEST_REPORTS = 'REQUEST_REPORTS';
-export const RECEIVE_REPORTS_SUCCESS = 'RECEIVE_REPORTS_SUCCESS';
-export const RECEIVE_REPORTS_ERROR = 'RECEIVE_REPORTS_ERROR';
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js b/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
deleted file mode 100644
index 249c2f35c0b..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/mutations.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_PATHS](state, paths) {
- state.baseBlobPath = paths.baseBlobPath;
- state.headBlobPath = paths.headBlobPath;
- state.reportsPath = paths.reportsPath;
- state.helpPath = paths.helpPath;
- },
- [types.REQUEST_REPORTS](state) {
- state.isLoading = true;
- },
- [types.RECEIVE_REPORTS_SUCCESS](state, data) {
- state.hasError = false;
- state.status = '';
- state.statusReason = '';
- state.isLoading = false;
- state.newIssues = data.newIssues;
- state.resolvedIssues = data.resolvedIssues;
- },
- [types.RECEIVE_REPORTS_ERROR](state, error) {
- state.isLoading = false;
- state.hasError = true;
- state.status = error?.status || '';
- state.statusReason = error?.response?.data?.status_reason;
- },
-};
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/state.js b/app/assets/javascripts/ci/reports/codequality_report/store/state.js
deleted file mode 100644
index f68dbc2a5fa..00000000000
--- a/app/assets/javascripts/ci/reports/codequality_report/store/state.js
+++ /dev/null
@@ -1,16 +0,0 @@
-export default () => ({
- reportsPath: null,
-
- baseBlobPath: null,
- headBlobPath: null,
-
- isLoading: false,
- hasError: false,
- status: '',
- statusReason: '',
-
- newIssues: [],
- resolvedIssues: [],
-
- helpPath: null,
-});
diff --git a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
index 417297df43c..417297df43c 100644
--- a/app/assets/javascripts/ci/reports/codequality_report/store/utils/codequality_parser.js
+++ b/app/assets/javascripts/ci/reports/codequality_report/utils/codequality_parser.js
diff --git a/app/assets/javascripts/ci/reports/components/report_section.vue b/app/assets/javascripts/ci/reports/components/report_section.vue
index fd6c6cca6b7..4a6a5e6e221 100644
--- a/app/assets/javascripts/ci/reports/components/report_section.vue
+++ b/app/assets/javascripts/ci/reports/components/report_section.vue
@@ -207,7 +207,6 @@ export default {
>
<gl-button
data-testid="report-section-expand-button"
- data-qa-selector="expand_report_button"
category="tertiary"
size="small"
:icon="isExpanded ? 'chevron-lg-up' : 'chevron-lg-down'"
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
index 8a920c85e06..a099d238c79 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_cell.vue
@@ -5,12 +5,11 @@ import { sprintf, __, formatNumber } from '~/locale';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerCreatedAt from '../runner_created_at.vue';
+import RunnerJobCount from '../runner_job_count.vue';
import RunnerName from '../runner_name.vue';
import RunnerTags from '../runner_tags.vue';
import RunnerTypeBadge from '../runner_type_badge.vue';
import RunnerManagersBadge from '../runner_managers_badge.vue';
-
-import { formatJobCount } from '../../utils';
import {
I18N_LOCKED_RUNNER_DESCRIPTION,
I18N_VERSION_LABEL,
@@ -25,6 +24,7 @@ export default {
TimeAgo,
RunnerSummaryField,
RunnerCreatedAt,
+ RunnerJobCount,
RunnerName,
RunnerTags,
RunnerTypeBadge,
@@ -52,9 +52,6 @@ export default {
additionalIpAddressCount() {
return this.managersCount - 1;
},
- jobCount() {
- return formatJobCount(this.runner.jobCount);
- },
createdBy() {
return this.runner?.createdBy;
},
@@ -135,7 +132,7 @@ export default {
</runner-summary-field>
<runner-summary-field icon="pipeline" data-testid="job-count" :tooltip="__('Jobs')">
- {{ jobCount }}
+ <runner-job-count :runner="runner" />
</runner-summary-field>
<runner-summary-field icon="calendar">
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
index b1b61e03eec..5ed987d28e7 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_summary_field.vue
@@ -26,8 +26,8 @@ export default {
<template>
<div v-gl-tooltip="tooltip" class="gl-display-inline-block gl-text-secondary gl-mb-3 gl-mr-4">
<gl-icon v-if="icon" :name="icon" :size="12" />
- <!-- display tooltip as a label for screen readers -->
- <span class="gl-sr-only">{{ tooltip }}</span>
+ <!-- display tooltip as a label for screen readers and make it unavailable for copying -->
+ <span class="gl-sr-only gl-user-select-none">{{ tooltip }}</span>
<slot></slot>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_count.vue b/app/assets/javascripts/ci/runner/components/runner_job_count.vue
new file mode 100644
index 00000000000..596e027efef
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/components/runner_job_count.vue
@@ -0,0 +1,36 @@
+<script>
+import runnerJobCountQuery from '../graphql/list/runner_job_count.query.graphql';
+import { formatJobCount } from '../utils';
+
+export default {
+ name: 'RunnerJobCount',
+ props: {
+ runner: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ jobCount: '-',
+ };
+ },
+ apollo: {
+ jobCount: {
+ query: runnerJobCountQuery,
+ variables() {
+ return { id: this.runner.id };
+ },
+ context: {
+ batchKey: 'RunnerJobCount',
+ },
+ update(data) {
+ return formatJobCount(data?.runner?.jobCount);
+ },
+ },
+ },
+};
+</script>
+<template>
+ <span>{{ jobCount }}</span>
+</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
index 653d9b05330..3cad0c52cd7 100644
--- a/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_jobs_table.vue
@@ -3,7 +3,7 @@ import { GlTableLite } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { formatTime } from '~/lib/utils/datetime_utility';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { tableField } from '../utils';
diff --git a/app/assets/javascripts/ci/runner/components/runner_list.vue b/app/assets/javascripts/ci/runner/components/runner_list.vue
index ec04701db2c..0282ac10fba 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list.vue
@@ -4,7 +4,7 @@ import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__ } from '~/locale';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import checkedRunnerIdsQuery from '../graphql/list/checked_runner_ids.query.graphql';
-import { formatJobCount, tableField } from '../utils';
+import { tableField } from '../utils';
import RunnerBulkDelete from './runner_bulk_delete.vue';
import RunnerBulkDeleteCheckbox from './runner_bulk_delete_checkbox.vue';
import RunnerSummaryCell from './cells/runner_summary_cell.vue';
@@ -95,9 +95,6 @@ export default {
onDeleted(event) {
this.$emit('deleted', event);
},
- formatJobCount(jobCount) {
- return formatJobCount(jobCount);
- },
runnerTrAttr(runner) {
if (runner) {
return {
diff --git a/app/assets/javascripts/ci/runner/components/runner_list_header.vue b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
index e4367db035e..6ed271d15ab 100644
--- a/app/assets/javascripts/ci/runner/components/runner_list_header.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_list_header.vue
@@ -7,7 +7,7 @@ export default {
<header
class="gl-my-5 gl-display-flex gl-align-items-flex-start gl-flex-wrap gl-justify-content-space-between"
>
- <h1 v-if="$scopedSlots.title" class="gl-my-0 gl-font-size-h1 header-title">
+ <h1 v-if="$scopedSlots.title" class="gl-mt-0 gl-mb-3 gl-font-size-h1 header-title">
<slot name="title"></slot>
</h1>
<div v-if="$scopedSlots.actions" class="gl-display-flex gl-gap-3">
diff --git a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
index 7ad9605d0a4..f6c96802004 100644
--- a/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
+++ b/app/assets/javascripts/ci/runner/graphql/list/list_item_shared.fragment.graphql
@@ -8,7 +8,6 @@ fragment ListItemShared on CiRunner {
version
paused
locked
- jobCount
tagList
createdAt
createdBy {
diff --git a/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql b/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql
new file mode 100644
index 00000000000..79ea19b048a
--- /dev/null
+++ b/app/assets/javascripts/ci/runner/graphql/list/runner_job_count.query.graphql
@@ -0,0 +1,6 @@
+query runnerJobCount($id: CiRunnerID!) {
+ runner(id: $id) {
+ id
+ jobCount
+ }
+}
diff --git a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
index b5042936b1e..cafac061c12 100644
--- a/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
+++ b/app/assets/javascripts/ci/runner/group_runners/group_runners_app.vue
@@ -159,7 +159,7 @@ export default {
search: {
deep: true,
handler() {
- // TODO Implement back button reponse using onpopstate
+ // TODO Implement back button response using onpopstate
// See https://gitlab.com/gitlab-org/gitlab/-/issues/333804
updateHistory({
url: fromSearchToUrl(this.search),
diff --git a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
index fb2e24e15f6..8ad599db6a7 100644
--- a/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
+++ b/app/assets/javascripts/ci_secure_files/components/secure_files_list.vue
@@ -223,7 +223,6 @@ export default {
ref="fileUpload"
type="file"
class="hidden"
- data-qa-selector="file_upload_field"
@change="uploadSecureFile"
/>
</div>
diff --git a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
index e1f6006fedf..d0675ba96fd 100644
--- a/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
+++ b/app/assets/javascripts/ci_settings_pipeline_triggers/components/triggers_list.vue
@@ -121,7 +121,6 @@ export default {
v-if="item.hasTokenExposed"
:text="item.token"
data-testid="clipboard-btn"
- data-qa-selector="clipboard_button"
:title="$options.i18n.copyTrigger"
css-class="gl-border-none gl-py-0 gl-px-2"
/>
diff --git a/app/assets/javascripts/clone_panel.js b/app/assets/javascripts/clone_panel.js
index 79280c13f0f..d2a5d5a9db7 100644
--- a/app/assets/javascripts/clone_panel.js
+++ b/app/assets/javascripts/clone_panel.js
@@ -14,7 +14,7 @@ export default function initClonePanel() {
$(`a:contains('${selectedCloneOption}')`, $cloneOptions).addClass('is-active');
}
- $('a', $cloneOptions).on('click', (e) => {
+ $('.js-clone-links a', $cloneOptions).on('click', (e) => {
const $this = $(e.currentTarget);
const url = $this.attr('href');
if (
diff --git a/app/assets/javascripts/clusters_list/components/agents.vue b/app/assets/javascripts/clusters_list/components/agents.vue
index 33d98c381fb..39b6e287288 100644
--- a/app/assets/javascripts/clusters_list/components/agents.vue
+++ b/app/assets/javascripts/clusters_list/components/agents.vue
@@ -1,6 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlAlert, GlLoadingIcon, GlBanner } from '@gitlab/ui';
+import feedbackBannerIllustration from '@gitlab/svgs/dist/illustrations/chat-sm.svg?url';
import { s__ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
@@ -112,6 +113,9 @@ export default {
feedbackBannerClasses() {
return this.isChildComponent ? 'gl-my-2' : 'gl-mb-4';
},
+ feedbackBannerIllustration() {
+ return feedbackBannerIllustration;
+ },
},
methods: {
updateTreeList(data) {
@@ -145,11 +149,11 @@ export default {
>
<gl-banner
v-if="!feedbackBannerDismissed"
- variant="introduction"
:class="feedbackBannerClasses"
:title="$options.i18n.feedbackBannerTitle"
:button-text="$options.i18n.feedbackBannerButton"
:button-link="$options.AGENT_FEEDBACK_ISSUE"
+ :svg-path="feedbackBannerIllustration"
@close="handleBannerClose"
>
<p>{{ $options.i18n.feedbackBannerText }}</p>
diff --git a/app/assets/javascripts/clusters_list/components/clusters.vue b/app/assets/javascripts/clusters_list/components/clusters.vue
index 0258d8e0da0..77c962e4056 100644
--- a/app/assets/javascripts/clusters_list/components/clusters.vue
+++ b/app/assets/javascripts/clusters_list/components/clusters.vue
@@ -252,12 +252,7 @@ export default {
class="gl-w-6 gl-h-6 gl-display-flex gl-align-items-center"
/>
- <gl-link
- data-qa-selector="cluster"
- :data-qa-cluster-name="item.name"
- :href="item.path"
- class="gl-px-3"
- >
+ <gl-link :href="item.path" class="gl-px-3">
{{ item.name }}
</gl-link>
diff --git a/app/assets/javascripts/commons/index.js b/app/assets/javascripts/commons/index.js
index d2a5ef83faf..6c9283a22cf 100644
--- a/app/assets/javascripts/commons/index.js
+++ b/app/assets/javascripts/commons/index.js
@@ -3,6 +3,3 @@ import './bootstrap';
import './vue';
import './gitlab_ui';
import '../lib/utils/axios_utils';
-import { openUserCountsBroadcast } from './nav/user_merge_requests';
-
-openUserCountsBroadcast();
diff --git a/app/assets/javascripts/commons/nav/user_merge_requests.js b/app/assets/javascripts/commons/nav/user_merge_requests.js
deleted file mode 100644
index 90dca0310f3..00000000000
--- a/app/assets/javascripts/commons/nav/user_merge_requests.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { getUserCounts } from '~/rest_api';
-
-let channel;
-
-function broadcastCount(newCount) {
- if (!channel) {
- return;
- }
-
- channel.postMessage(newCount);
-}
-
-function updateUserMergeRequestCounts(newCount) {
- const mergeRequestsCountEl = document.querySelector('.js-assigned-mr-count');
- mergeRequestsCountEl.textContent = newCount.toLocaleString();
-}
-
-function updateReviewerMergeRequestCounts(newCount) {
- const mergeRequestsCountEl = document.querySelector('.js-reviewer-mr-count');
- mergeRequestsCountEl.textContent = newCount.toLocaleString();
-}
-
-function updateMergeRequestCounts(newCount) {
- const mergeRequestsCountEl = document.querySelector('.js-merge-requests-count');
- mergeRequestsCountEl.textContent = newCount.toLocaleString();
- mergeRequestsCountEl.classList.toggle('gl-display-none', Number(newCount) === 0);
-}
-
-/**
- * Refresh user counts (and broadcast if open)
- */
-export function refreshUserMergeRequestCounts() {
- if (gon?.use_new_navigation) {
- // The new sidebar manages _all_ the counts in
- // ~/super_sidebar/user_counts_manager.js
- document.dispatchEvent(new CustomEvent('userCounts:fetch'));
- return Promise.resolve();
- }
- return getUserCounts()
- .then(({ data }) => {
- const assignedMergeRequests = data.assigned_merge_requests;
- const reviewerMergeRequests = data.review_requested_merge_requests;
- const fullCount = assignedMergeRequests + reviewerMergeRequests;
-
- updateUserMergeRequestCounts(assignedMergeRequests);
- updateReviewerMergeRequestCounts(reviewerMergeRequests);
- updateMergeRequestCounts(fullCount);
- broadcastCount(fullCount);
- })
- .catch((ex) => {
- console.error(ex); // eslint-disable-line no-console
- });
-}
-
-/**
- * Close the broadcast channel for user counts
- */
-export function closeUserCountsBroadcast() {
- if (!channel) {
- return;
- }
-
- channel.close();
- channel = null;
-}
-
-/**
- * Open the broadcast channel for user counts, adds user id so we only update
- *
- * **Please note:**
- * Not supported in all browsers, but not polyfilling for now
- * to keep bundle size small and
- * no special functionality lost except cross tab notifications
- */
-export function openUserCountsBroadcast() {
- if (gon?.use_new_navigation) {
- // The new sidebar broadcasts _all counts_ and updates
- // them accordingly. Therefore we do not need this manager
- // ~/super_sidebar/user_counts_manager.js
- return;
- }
- closeUserCountsBroadcast();
-
- if (window.BroadcastChannel) {
- const currentUserId = typeof gon !== 'undefined' && gon && gon.current_user_id;
- if (currentUserId) {
- channel = new BroadcastChannel(`mr_count_channel_${currentUserId}`);
- channel.onmessage = (ev) => {
- updateMergeRequestCounts(ev.data);
- };
- }
- }
-}
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
index caac61fe9a6..3dc3436347c 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
@@ -148,7 +148,7 @@ export default {
<gl-button-group>
<gl-dropdown
category="tertiary"
- contenteditable="false"
+ :contenteditable="false"
boundary="viewport"
:text="selectedLanguage.label"
@hide="clearCustomLanguageForm"
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
index 6ce6e731551..0818228e886 100644
--- a/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/media_bubble_menu.vue
@@ -238,7 +238,6 @@ export default {
name="content_editor_image"
:accept="$options.acceptedMimes[mediaType]"
class="gl-display-none"
- data-qa-selector="file_upload_field"
@change="onFileSelect"
/>
<gl-link
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 2e9388c1e20..a48245f732d 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -6,6 +6,7 @@ import { VARIANT_DANGER } from '~/alert';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { CONTENT_EDITOR_READY_EVENT } from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
+import SidebarMediator from '~/sidebar/sidebar_mediator';
import { createContentEditor } from '../services/create_content_editor';
import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants';
import ContentEditorAlert from './content_editor_alert.vue';
@@ -157,6 +158,7 @@ export default {
enableAutocomplete,
autocompleteDataSources,
codeSuggestionsConfig,
+ sidebarMediator: SidebarMediator.singleton,
tiptapOptions: {
autofocus,
editable,
diff --git a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
index dc27278d255..c1eae345f72 100644
--- a/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_toolbar.vue
@@ -3,6 +3,7 @@ import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_t
import { __, sprintf } from '~/locale';
import { getModifierKey } from '~/constants';
import trackUIControl from '../services/track_ui_control';
+import HeaderDivider from '../../vue_shared/components/markdown/header_divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarAttachmentButton from './toolbar_attachment_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
@@ -17,6 +18,7 @@ export default {
ToolbarAttachmentButton,
ToolbarMoreDropdown,
CommentTemplatesDropdown,
+ HeaderDivider,
},
inject: {
newCommentTemplatePath: { default: null },
@@ -73,14 +75,17 @@ export default {
</script>
<template>
<div
- class="gl-w-full gl-display-flex gl-align-items-center gl-flex-wrap gl-border-b gl-border-gray-100 gl-px-3 gl-rounded-top-base gl-justify-content-space-between"
+ class="gl-w-full gl-py-3 gl-row-gap-2 gl-display-flex gl-align-items-center gl-flex-wrap gl-border-b gl-border-gray-100 gl-px-3 gl-rounded-top-base"
data-testid="formatting-toolbar"
>
- <div class="gl-py-3 gl-display-flex gl-flex-wrap">
+ <div class="gl-display-flex">
<toolbar-text-style-dropdown
data-testid="text-styles"
@execute="trackToolbarControlExecution"
/>
+ <header-divider />
+ </div>
+ <div v-if="codeSuggestionsEnabled" class="gl-display-flex">
<toolbar-button
v-if="codeSuggestionsEnabled"
data-testid="code-suggestion"
@@ -91,22 +96,25 @@ export default {
:show-active-state="false"
@execute="trackToolbarControlExecution"
/>
- <toolbar-button
- data-testid="bold"
- content-type="bold"
- icon-name="bold"
- editor-command="toggleBold"
- :label="i18n.bold"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="italic"
- content-type="italic"
- icon-name="italic"
- editor-command="toggleItalic"
- :label="i18n.italic"
- @execute="trackToolbarControlExecution"
- />
+ <header-divider />
+ </div>
+ <toolbar-button
+ data-testid="bold"
+ content-type="bold"
+ icon-name="bold"
+ editor-command="toggleBold"
+ :label="i18n.bold"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="italic"
+ content-type="italic"
+ icon-name="italic"
+ editor-command="toggleItalic"
+ :label="i18n.italic"
+ @execute="trackToolbarControlExecution"
+ />
+ <div class="gl-display-flex">
<toolbar-button
data-testid="strike"
content-type="strike"
@@ -115,48 +123,51 @@ export default {
:label="i18n.strike"
@execute="trackToolbarControlExecution"
/>
- <toolbar-button
- data-testid="blockquote"
- content-type="blockquote"
- icon-name="quote"
- editor-command="toggleBlockquote"
- :label="i18n.quote"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="code"
- content-type="code"
- icon-name="code"
- editor-command="toggleCode"
- :label="i18n.code"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="link"
- content-type="link"
- icon-name="link"
- editor-command="editLink"
- :label="i18n.link"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="bullet-list"
- content-type="bulletList"
- icon-name="list-bulleted"
- class="gl-display-none gl-sm-display-inline"
- editor-command="toggleBulletList"
- :label="i18n.bulletList"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-button
- data-testid="ordered-list"
- content-type="orderedList"
- icon-name="list-numbered"
- class="gl-display-none gl-sm-display-inline"
- editor-command="toggleOrderedList"
- :label="i18n.numberedList"
- @execute="trackToolbarControlExecution"
- />
+ <header-divider />
+ </div>
+ <toolbar-button
+ data-testid="blockquote"
+ content-type="blockquote"
+ icon-name="quote"
+ editor-command="toggleBlockquote"
+ :label="i18n.quote"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="code"
+ content-type="code"
+ icon-name="code"
+ editor-command="toggleCode"
+ :label="i18n.code"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="link"
+ content-type="link"
+ icon-name="link"
+ editor-command="editLink"
+ :label="i18n.link"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="bullet-list"
+ content-type="bulletList"
+ icon-name="list-bulleted"
+ class="gl-display-none gl-sm-display-inline"
+ editor-command="toggleBulletList"
+ :label="i18n.bulletList"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="ordered-list"
+ content-type="orderedList"
+ icon-name="list-numbered"
+ class="gl-display-none gl-sm-display-inline"
+ editor-command="toggleOrderedList"
+ :label="i18n.numberedList"
+ @execute="trackToolbarControlExecution"
+ />
+ <div class="gl-display-flex">
<toolbar-button
data-testid="task-list"
content-type="taskList"
@@ -166,7 +177,12 @@ export default {
:label="i18n.taskList"
@execute="trackToolbarControlExecution"
/>
- <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
+ <div class="gl-display-none gl-sm-display-flex">
+ <header-divider />
+ </div>
+ </div>
+ <toolbar-table-button data-testid="table" @execute="trackToolbarControlExecution" />
+ <div class="gl-display-flex">
<toolbar-attachment-button
v-if="!hideAttachmentButton"
data-testid="attachment"
@@ -183,12 +199,13 @@ export default {
:label="__('Add a quick action')"
@execute="trackToolbarControlExecution"
/>
- <comment-templates-dropdown
- v-if="newCommentTemplatePath"
- :new-comment-template-path="newCommentTemplatePath"
- @select="insertSavedReply"
- />
- <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
+ <header-divider v-if="newCommentTemplatePath" />
</div>
+ <comment-templates-dropdown
+ v-if="newCommentTemplatePath"
+ :new-comment-template-path="newCommentTemplatePath"
+ @select="insertSavedReply"
+ />
+ <toolbar-more-dropdown data-testid="more" @execute="trackToolbarControlExecution" />
</div>
</template>
diff --git a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
index b34ebe85eb4..12d7114b036 100644
--- a/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/suggestions_dropdown.vue
@@ -1,12 +1,17 @@
<script>
-import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatar, GlLoadingIcon } from '@gitlab/ui';
+import SafeHtml from '~/vue_shared/directives/safe_html';
export default {
components: {
- GlAvatarLabeled,
+ GlAvatar,
GlLoadingIcon,
},
+ directives: {
+ SafeHtml,
+ },
+
props: {
char: {
type: String,
@@ -38,6 +43,12 @@ export default {
required: false,
default: false,
},
+
+ query: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
@@ -90,20 +101,32 @@ export default {
isEmoji() {
return this.nodeType === 'emoji';
},
+
+ shouldSelectFirstItem() {
+ return this.items.length && this.query;
+ },
},
watch: {
items() {
- this.selectedIndex = -1;
+ this.selectedIndex = this.shouldSelectFirstItem ? 0 : -1;
},
- selectedIndex() {
+ async selectedIndex() {
+ // wait for the DOM to update before scrolling
+ await this.$nextTick();
this.scrollIntoView();
},
},
+ mounted() {
+ if (this.shouldSelectFirstItem) {
+ this.selectedIndex = 0;
+ }
+ },
+
methods: {
getText(item) {
- if (this.isEmoji) return item.e;
+ if (this.isEmoji) return item.emoji.e;
switch (this.isReference && this.nodeProps.referenceType) {
case 'user':
@@ -133,10 +156,10 @@ export default {
if (this.isEmoji) {
Object.assign(props, {
- name: item.name,
- unicodeVersion: item.u,
- title: item.d,
- moji: item.e,
+ name: item.emoji.name,
+ unicodeVersion: item.emoji.u,
+ title: item.emoji.d,
+ moji: item.emoji.e,
});
}
@@ -173,7 +196,7 @@ export default {
return true;
}
- if (event.key === 'Enter') {
+ if (event.key === 'Enter' || event.key === 'Tab') {
this.enterHandler();
return true;
}
@@ -194,7 +217,7 @@ export default {
},
scrollIntoView() {
- this.$refs.dropdownItems[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
+ this.$refs.dropdownItems?.[this.selectedIndex]?.scrollIntoView({ block: 'nearest' });
},
selectItem(index) {
@@ -211,7 +234,17 @@ export default {
avatarSubLabel(item) {
return item.count ? `${item.name} (${item.count})` : item.name;
},
+
+ highlight(text) {
+ return this.query
+ ? String(text).replace(
+ new RegExp(this.query, 'i'),
+ (match) => `<strong class="gl-text-body!">${match}</strong>`,
+ )
+ : text;
+ },
},
+ safeHtmlConfig: { ALLOWED_TAGS: ['strong'] },
};
</script>
@@ -238,29 +271,45 @@ export default {
@click="selectItem(index)"
>
<div class="gl-new-dropdown-item-text-wrapper">
- <gl-avatar-labeled
- v-if="isUser"
- :label="item.username"
- :sub-label="avatarSubLabel(item)"
- :src="item.avatar_url"
- :entity-name="item.username"
- :shape="item.type === 'Group' ? 'rect' : 'circle'"
- :size="32"
- />
+ <span v-if="isUser" class="gl-flex">
+ <gl-avatar
+ :src="item.avatar_url"
+ :entity-name="item.username"
+ :size="24"
+ :shape="item.type === 'Group' ? 'rect' : 'circle'"
+ class="gl-vertical-align-middle gl-mx-2"
+ />
+ <span class="gl-vertical-align-middle">
+ <span v-safe-html:safeHtmlConfig="highlight(item.username)"></span>
+ <small
+ v-safe-html:safeHtmlConfig="highlight(avatarSubLabel(item))"
+ class="gl-text-gray-500"
+ ></small>
+ </span>
+ </span>
<span v-if="isIssue || isMergeRequest">
- <small>{{ item.iid }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.iid)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isVulnerability || isSnippet">
- <small>{{ item.id }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.id)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isEpic">
- <small>{{ item.reference }}</small>
- {{ item.title }}
+ <small
+ v-safe-html:safeHtmlConfig="highlight(item.reference)"
+ class="gl-text-gray-500"
+ ></small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isMilestone">
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<span v-if="isLabel" class="gl-display-flex">
<span
@@ -268,20 +317,31 @@ export default {
class="dropdown-label-box gl-flex-shrink-0 gl-top-0 gl-mr-3"
:style="{ backgroundColor: item.color }"
></span>
- {{ item.title }}
+ <span v-safe-html:safeHtmlConfig="highlight(item.title)"></span>
</span>
<div v-if="isCommand">
<div class="gl-mb-1">
- <span class="gl-font-weight-bold">/{{ item.name }}</span>
- <em class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</em>
+ /<span v-safe-html:safeHtmlConfig="highlight(item.name)"></span>
+ <span class="gl-text-gray-500 gl-font-sm">{{ item.params[0] }}</span>
</div>
- <small class="gl-text-gray-500"> {{ item.description }} </small>
+ <em
+ v-safe-html:safeHtmlConfig="highlight(item.description)"
+ class="gl-text-gray-500 gl-font-sm"
+ ></em>
</div>
<div v-if="isEmoji" class="gl-display-flex gl-align-items-center">
- <div class="gl-pr-4 gl-font-lg">{{ item.e }}</div>
+ <div class="gl-pr-4 gl-font-lg">
+ <gl-emoji
+ :key="item.emoji.e"
+ :data-name="item.emoji.name"
+ :title="item.emoji.d"
+ :data-unicode-version="item.emoji.u"
+ :data-fallback-src="item.emoji.src"
+ >{{ item.emoji.e }}</gl-emoji
+ >
+ </div>
<div class="gl-flex-grow-1">
- {{ item.name }}<br />
- <small>{{ item.d }}</small>
+ <span v-safe-html:safeHtmlConfig="highlight(item.fieldValue)"></span>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
index 78a01693f14..9093fc323cc 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_attachment_button.vue
@@ -47,7 +47,7 @@ export default {
category="tertiary"
icon="paperclip"
size="small"
- class="gl-mr-3"
+ class="gl-mr-2"
lazy
@click="openFileUpload"
/>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
index 60bfaab25a5..a4c4814fde9 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -87,7 +87,7 @@ export default {
:aria-label="label"
:title="label"
:icon="iconName"
- class="gl-mr-3"
+ class="gl-mr-2"
@click="execute"
/>
</editor-state-observer>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
index ab1546b9016..f09d583996c 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
@@ -93,7 +93,7 @@ export default {
:aria-label="__('Insert table')"
:toggle-text="__('Insert table')"
positioning-strategy="fixed"
- class="content-editor-table-dropdown gl-mr-3"
+ class="content-editor-table-dropdown gl-mr-2"
text-sr-only
:fluid-width="true"
@shown="setFocus(1, 1)"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
index efd0926d7ed..93de4cd9744 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/code_block.vue
@@ -170,7 +170,8 @@ export default {
>
<div
v-if="node.attrs.showPreview"
- contenteditable="false"
+ :contenteditable="false"
+ data-testid="sandbox-preview"
class="gl-mt-n3! gl-ml-n4! gl-mr-n4! gl-mb-3 gl-bg-white! gl-p-4 gl-border-b-1 gl-border-b-solid gl-border-b-gray-100"
>
<sandboxed-mermaid v-if="node.attrs.language === 'mermaid'" :source="diagramSource" />
@@ -178,14 +179,14 @@ export default {
</div>
<span
v-if="node.attrs.isFrontmatter"
- contenteditable="false"
+ :contenteditable="false"
data-testid="frontmatter-label"
class="gl-absolute gl-top-0 gl-right-3"
>{{ __('frontmatter') }}:{{ node.attrs.language }}</span
>
<div
v-if="isCodeSuggestion"
- contenteditable="false"
+ :contenteditable="false"
class="gl-relative gl-z-index-0"
data-testid="code-suggestion-box"
>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue b/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue
index b96b7400d85..06dc59c2ad8 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/footnote_definition.vue
@@ -18,8 +18,8 @@ export default {
<template>
<node-view-wrapper class="gl-display-flex gl-font-sm" as="div">
<span
+ :contenteditable="false"
data-testid="footnote-label"
- contenteditable="false"
class="gl-display-inline-flex gl-mr-2"
dir="auto"
>{{ node.attrs.label }}:</span
diff --git a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
index 44f5a2895fd..e7a1b058341 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue
@@ -149,7 +149,8 @@ export default {
>
<span
v-if="displayActionsDropdown"
- contenteditable="false"
+ :contenteditable="false"
+ data-testid="actions-dropdown"
class="gl-absolute gl-right-0 gl-top-0 gl-pr-1 gl-pt-1"
>
<gl-disclosure-dropdown
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index d29a407c5ca..23f2da7bc28 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -149,21 +149,26 @@ export default Extension.create({
const { clipboardData } = event;
const gfmContent = clipboardData.getData(GFM_FORMAT);
-
- if (gfmContent) {
- return this.editor.commands.pasteContent(gfmContent, true);
- }
-
const textContent = clipboardData.getData(TEXT_FORMAT);
const htmlContent = clipboardData.getData(HTML_FORMAT);
const { from, to } = view.state.selection;
+ const isCodeBlockActive = CODE_BLOCK_NODE_TYPES.some((type) =>
+ this.editor.isActive(type),
+ );
- if (pasteRaw) {
- this.editor.commands.insertContentAt(
- { from, to },
- textContent.replace(/^\s+|\s+$/gm, ''),
- );
+ if (pasteRaw || isCodeBlockActive) {
+ const isMarkdownCodeBlockActive = this.editor.isActive(CodeBlockHighlight.name, {
+ language: 'markdown',
+ });
+
+ const contentToInsert = isMarkdownCodeBlockActive
+ ? gfmContent || textContent
+ : textContent.replace(/^\s+|\s+$/gm, '');
+
+ if (!contentToInsert) return false;
+
+ this.editor.commands.insertContentAt({ from, to }, contentToInsert);
return true;
}
@@ -172,11 +177,6 @@ export default Extension.create({
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
const language = vsCodeMeta.mode;
- // if a code block is active, paste as plain text
- if (!textContent || CODE_BLOCK_NODE_TYPES.some((type) => this.editor.isActive(type))) {
- return false;
- }
-
if (hasVsCode) {
return this.editor.commands.pasteContent(
language === 'markdown' ? textContent : `\`\`\`${language}\n${textContent}\n\`\`\``,
@@ -184,6 +184,10 @@ export default Extension.create({
);
}
+ if (gfmContent) {
+ return this.editor.commands.pasteContent(gfmContent, true);
+ }
+
const preStartRegex = /^<pre[^>]*lang="markdown"[^>]*>/;
const preEndRegex = /<\/pre>$/;
const htmlContentWithoutMeta = htmlContent?.replace(/^<meta[^>]*>/, '');
diff --git a/app/assets/javascripts/content_editor/extensions/emoji.js b/app/assets/javascripts/content_editor/extensions/emoji.js
index be6ecb6cafd..96e03dfe598 100644
--- a/app/assets/javascripts/content_editor/extensions/emoji.js
+++ b/app/assets/javascripts/content_editor/extensions/emoji.js
@@ -46,7 +46,7 @@ export default Node.create({
title: node.attrs.title,
'data-unicode-version': node.attrs.unicodeVersion,
},
- node.attrs.moji,
+ node.attrs.moji || '',
];
},
diff --git a/app/assets/javascripts/content_editor/extensions/reference.js b/app/assets/javascripts/content_editor/extensions/reference.js
index fd248709b5a..0c385481ac5 100644
--- a/app/assets/javascripts/content_editor/extensions/reference.js
+++ b/app/assets/javascripts/content_editor/extensions/reference.js
@@ -72,11 +72,20 @@ export default Node.create({
addInputRules() {
const { editor } = this;
const { assetResolver } = this.options;
- const referenceInputRegex = /(?:^|\s)([\w/]*([!&#])\d+(\+?s?))(?:\s|\n)/m;
+ const referenceInputRegex = /(?:^|\s)([\w/]*([#!&%$@~]|\[vulnerability:)[\w.]+(\+?s?\]?))(?:\s|\n)/m;
const referenceTypes = {
'#': 'issue',
'!': 'merge_request',
'&': 'epic',
+ '%': 'milestone',
+ $: 'snippet',
+ '@': 'user',
+ '~': 'label',
+ '[vulnerability:': 'vulnerability',
+ };
+ const nodeTypes = {
+ label: editor.schema.nodes.referenceLabel,
+ default: editor.schema.nodes.reference,
};
return [
@@ -91,22 +100,26 @@ export default Node.create({
text,
expandedText,
fullyExpandedText,
+ backgroundColor,
} = await assetResolver.resolveReference(referenceId);
if (!text) return;
let referenceText = text;
- if (expansionType === '+') referenceText = expandedText;
- if (expansionType === '+s') referenceText = fullyExpandedText;
+ if (expansionType === '+') referenceText = expandedText || text;
+ if (expansionType === '+s') referenceText = fullyExpandedText || text;
const position = findReference(editor, referenceId);
if (!position) return;
+ const nodeType = nodeTypes[referenceType] || nodeTypes.default;
+
editor.view.dispatch(
editor.state.tr.replaceWith(position, position + referenceId.length, [
- this.type.create({
+ nodeType.create({
referenceType,
originalText: referenceId,
+ color: backgroundColor,
href,
text: referenceText,
}),
diff --git a/app/assets/javascripts/content_editor/extensions/suggestions.js b/app/assets/javascripts/content_editor/extensions/suggestions.js
index f7ff2fd6647..d309210404a 100644
--- a/app/assets/javascripts/content_editor/extensions/suggestions.js
+++ b/app/assets/javascripts/content_editor/extensions/suggestions.js
@@ -3,29 +3,20 @@ import { VueRenderer } from '@tiptap/vue-2';
import tippy from 'tippy.js';
import Suggestion from '@tiptap/suggestion';
import { PluginKey } from '@tiptap/pm/state';
-import { isFunction, uniqueId, memoize } from 'lodash';
-import axios from '~/lib/utils/axios_utils';
-import { initEmojiMap, getAllEmoji } from '~/emoji';
+import { uniqueId } from 'lodash';
import SuggestionsDropdown from '../components/suggestions_dropdown.vue';
-function find(haystack, needle) {
- return String(haystack).toLocaleLowerCase().includes(String(needle).toLocaleLowerCase());
-}
-
function createSuggestionPlugin({
editor,
char,
- dataSource,
- search,
- limit = 15,
+ limit = 5,
nodeType,
- nodeProps = {},
+ referenceType,
+ cache = true,
insertionMap = {},
+ serializer,
+ autocompleteHelper,
}) {
- const fetchData = memoize(
- isFunction(dataSource) ? dataSource : async () => (await axios.get(dataSource)).data,
- );
-
return Suggestion({
editor,
char,
@@ -42,16 +33,17 @@ function createSuggestionPlugin({
.run();
},
- async items({ query }) {
- if (!dataSource) return [];
-
- try {
- const items = await fetchData();
-
- return items.filter(search(query)).slice(0, limit);
- } catch {
- return [];
- }
+ async items({ query, editor: tiptapEditor }) {
+ const slice = tiptapEditor.state.doc.slice(0, tiptapEditor.state.selection.to);
+ const markdownLine = serializer.serialize({ doc: slice.content }).split('\n').pop();
+
+ return autocompleteHelper
+ .getDataSource(referenceType, {
+ command: markdownLine.match(/\/\w+/)?.[0],
+ cache,
+ limit,
+ })
+ .search(query);
},
render: () => {
@@ -76,7 +68,7 @@ function createSuggestionPlugin({
...props,
char,
nodeType,
- nodeProps,
+ nodeProps: { referenceType },
loading: true,
},
editor: props.editor,
@@ -132,101 +124,38 @@ export default Node.create({
addOptions() {
return {
- autocompleteDataSources: {},
+ autocompleteHelper: {},
+ serializer: null,
};
},
addProseMirrorPlugins() {
- return [
- createSuggestionPlugin({
- editor: this.editor,
- char: '@',
- dataSource: this.options.autocompleteDataSources.members,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'user',
- },
- search: (query) => ({ name, username }) => find(name, query) || find(username, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '#',
- dataSource: this.options.autocompleteDataSources.issues,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'issue',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '$',
- dataSource: this.options.autocompleteDataSources.snippets,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'snippet',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '~',
- dataSource: this.options.autocompleteDataSources.labels,
- nodeType: 'referenceLabel',
- nodeProps: {
- referenceType: 'label',
- },
- search: (query) => ({ title }) => find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '&',
- dataSource: this.options.autocompleteDataSources.epics,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'epic',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '[vulnerability:',
- dataSource: this.options.autocompleteDataSources.vulnerabilities,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'vulnerability',
- },
- search: (query) => ({ id, title }) => find(id, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '!',
- dataSource: this.options.autocompleteDataSources.mergeRequests,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'merge_request',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
- createSuggestionPlugin({
- editor: this.editor,
- char: '%',
- dataSource: this.options.autocompleteDataSources.milestones,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'milestone',
- },
- search: (query) => ({ iid, title }) => find(iid, query) || find(title, query),
- }),
+ const { serializer, autocompleteHelper } = this.options;
+
+ const createPlugin = (char, nodeType, referenceType, options = {}) =>
createSuggestionPlugin({
editor: this.editor,
- char: '/',
- dataSource: this.options.autocompleteDataSources.commands,
- nodeType: 'reference',
- nodeProps: {
- referenceType: 'command',
- },
- search: (query) => ({ name }) => find(name, query),
+ char,
+ nodeType,
+ referenceType,
+ serializer,
+ autocompleteHelper,
+ ...options,
+ });
+
+ return [
+ createPlugin('@', 'reference', 'user', { limit: 10 }),
+ createPlugin('#', 'reference', 'issue'),
+ createPlugin('$', 'reference', 'snippet'),
+ createPlugin('~', 'referenceLabel', 'label', { limit: 20 }),
+ createPlugin('&', 'reference', 'epic'),
+ createPlugin('!', 'reference', 'merge_request'),
+ createPlugin('[vulnerability:', 'reference', 'vulnerability'),
+ createPlugin('%', 'reference', 'milestone'),
+ createPlugin(':', 'emoji', 'emoji'),
+ createPlugin('/', 'reference', 'command', {
+ cache: false,
+ limit: 100,
insertionMap: {
'/label': '~',
'/unlabel': '~',
@@ -241,18 +170,6 @@ export default Node.create({
'/milestone': '%',
},
}),
- createSuggestionPlugin({
- editor: this.editor,
- char: ':',
- dataSource: () => getAllEmoji(),
- nodeType: 'emoji',
- search: (query) => ({ d, name }) => find(d, query) || find(name, query),
- limit: 10,
- }),
];
},
-
- onCreate() {
- initEmojiMap();
- },
});
diff --git a/app/assets/javascripts/content_editor/extensions/task_list.js b/app/assets/javascripts/content_editor/extensions/task_list.js
index 01e5bddb97a..5ef9cf42f93 100644
--- a/app/assets/javascripts/content_editor/extensions/task_list.js
+++ b/app/assets/javascripts/content_editor/extensions/task_list.js
@@ -27,6 +27,13 @@ export default TaskList.extend({
default: false,
parseHTML: (element) => /^[0-9]+\)/.test(getMarkdownSource(element)),
},
+ bullet: {
+ default: '*',
+ parseHTML(element) {
+ const bullet = getMarkdownSource(element)?.charAt(0);
+ return '*+-'.includes(bullet) ? bullet : '*';
+ },
+ },
};
},
diff --git a/app/assets/javascripts/content_editor/services/asset_resolver.js b/app/assets/javascripts/content_editor/services/asset_resolver.js
index 0d4396fc176..07a69db7428 100644
--- a/app/assets/javascripts/content_editor/services/asset_resolver.js
+++ b/app/assets/javascripts/content_editor/services/asset_resolver.js
@@ -27,10 +27,11 @@ export default class AssetResolver {
if (!a.length) return {};
return {
- href: a[0].getAttribute('href'),
- text: a[0].textContent,
- expandedText: a[1].textContent,
- fullyExpandedText: a[2].textContent,
+ href: a[0]?.getAttribute('href'),
+ text: a[0]?.textContent,
+ expandedText: a[1]?.textContent,
+ fullyExpandedText: a[2]?.textContent,
+ backgroundColor: a[0]?.firstElementChild?.style.backgroundColor,
};
});
diff --git a/app/assets/javascripts/content_editor/services/create_content_editor.js b/app/assets/javascripts/content_editor/services/create_content_editor.js
index 51e41ceefaf..5c48c0b1d43 100644
--- a/app/assets/javascripts/content_editor/services/create_content_editor.js
+++ b/app/assets/javascripts/content_editor/services/create_content_editor.js
@@ -70,6 +70,7 @@ import createGlApiMarkdownDeserializer from './gl_api_markdown_deserializer';
import createRemarkMarkdownDeserializer from './remark_markdown_deserializer';
import AssetResolver from './asset_resolver';
import trackInputRulesAndShortcuts from './track_input_rules_and_shortcuts';
+import DataSourceFactory from './data_source_factory';
const createTiptapEditor = ({ extensions = [], ...options } = {}) =>
new Editor({
@@ -86,6 +87,7 @@ export const createContentEditor = ({
drawioEnabled = false,
enableAutocomplete,
autocompleteDataSources = {},
+ sidebarMediator = {},
codeSuggestionsConfig = {},
} = {}) => {
if (!isFunction(renderMarkdown)) {
@@ -95,6 +97,10 @@ export const createContentEditor = ({
const eventHub = eventHubFactory();
const assetResolver = new AssetResolver({ renderMarkdown });
const serializer = new MarkdownSerializer({ serializerConfig });
+ const autocompleteHelper = new DataSourceFactory({
+ dataSourceUrls: autocompleteDataSources,
+ sidebarMediator,
+ });
const deserializer = window.gon?.features?.preserveUnchangedMarkdown
? createRemarkMarkdownDeserializer()
: createGlApiMarkdownDeserializer({
@@ -166,7 +172,8 @@ export const createContentEditor = ({
const allExtensions = [...builtInContentEditorExtensions, ...extensions];
- if (enableAutocomplete) allExtensions.push(Suggestions.configure({ autocompleteDataSources }));
+ if (enableAutocomplete)
+ allExtensions.push(Suggestions.configure({ autocompleteHelper, serializer }));
if (drawioEnabled) allExtensions.push(DrawioDiagram.configure({ uploadsPath, assetResolver }));
const trackedExtensions = allExtensions.map(trackInputRulesAndShortcuts);
diff --git a/app/assets/javascripts/content_editor/services/data_source_factory.js b/app/assets/javascripts/content_editor/services/data_source_factory.js
new file mode 100644
index 00000000000..a0f0e106f1d
--- /dev/null
+++ b/app/assets/javascripts/content_editor/services/data_source_factory.js
@@ -0,0 +1,213 @@
+import { identity, memoize, throttle } from 'lodash';
+import { sprintf, __ } from '~/locale';
+import { initEmojiMap, getAllEmoji, searchEmoji } from '~/emoji';
+import { parsePikadayDate } from '~/lib/utils/datetime_utility';
+import axios from '~/lib/utils/axios_utils';
+
+export function defaultSorter(searchFields) {
+ return (items, query) => {
+ if (!query) return items;
+
+ const sortOrdersMap = new WeakMap();
+
+ items.forEach((item) => {
+ const sortOrders = searchFields.map((searchField) => {
+ const haystack = String(item[searchField]).toLocaleLowerCase();
+ const needle = query.toLocaleLowerCase();
+
+ const i = haystack.indexOf(needle);
+ if (i < 0) return i;
+ return Number.MAX_SAFE_INTEGER - i;
+ });
+
+ sortOrdersMap.set(item, Math.max(...sortOrders));
+ });
+
+ return items.sort((a, b) => sortOrdersMap.get(b) - sortOrdersMap.get(a));
+ };
+}
+
+export function customSorter(sorter) {
+ return (items) => items.sort(sorter);
+}
+
+const milestonesMap = new WeakMap();
+
+function parseMilestone(milestone) {
+ if (!milestone.title) {
+ return milestone;
+ }
+
+ const dueDate = milestone.due_date ? parsePikadayDate(milestone.due_date) : null;
+ const expired = dueDate ? Date.now() > dueDate.getTime() : false;
+
+ return {
+ id: milestone.iid,
+ title: expired
+ ? sprintf(__('%{milestone} (expired)'), {
+ milestone: milestone.title,
+ })
+ : milestone.title,
+ expired,
+ dueDate,
+ };
+}
+
+function mapMilestone(milestone) {
+ if (!milestonesMap.has(milestone)) {
+ milestonesMap.set(milestone, parseMilestone(milestone));
+ }
+
+ return milestonesMap.get(milestone);
+}
+
+function sortMilestones(milestoneA, milestoneB) {
+ const mappedA = mapMilestone(milestoneA);
+ const mappedB = mapMilestone(milestoneB);
+
+ // Move all expired milestones to the bottom.
+ if (milestoneA.expired) return 1;
+ if (milestoneB.expired) return -1;
+
+ // Move milestones without due dates just above expired milestones.
+ if (!milestoneA.dueDate) return 1;
+ if (!milestoneB.dueDate) return -1;
+
+ return mappedA.dueDate - mappedB.dueDate;
+}
+
+export function createDataSource({
+ source,
+ searchFields,
+ filter,
+ mapper = identity,
+ sorter = defaultSorter(searchFields),
+ cache = true,
+ limit = 15,
+}) {
+ const fetchData = source ? async () => (await axios.get(source)).data : () => [];
+ let items = [];
+
+ const sync = async function sync() {
+ try {
+ items = await fetchData();
+ } catch {
+ items = [];
+ }
+ };
+
+ const init = memoize(sync);
+ const throttledSync = throttle(sync, 5000);
+
+ return {
+ search: async (query) => {
+ await init();
+ if (!cache) throttledSync();
+
+ let results = items.map(mapper);
+ if (filter) results = filter(items, query);
+
+ if (query) {
+ results = results.filter((item) => {
+ if (!searchFields.length) return true;
+ return searchFields.some((field) =>
+ String(item[field]).toLocaleLowerCase().includes(query.toLocaleLowerCase()),
+ );
+ });
+ }
+
+ return sorter(results, query).slice(0, limit);
+ },
+ };
+}
+
+export default class DataSourceFactory {
+ constructor({ dataSourceUrls, sidebarMediator }) {
+ this.dataSourceUrls = dataSourceUrls;
+ this.sidebarMediator = sidebarMediator;
+
+ initEmojiMap();
+ }
+
+ getDataSource = memoize(
+ (referenceType, config = {}) => {
+ const sources = {
+ user: this.dataSourceUrls.members,
+ issue: this.dataSourceUrls.issues,
+ snippet: this.dataSourceUrls.snippets,
+ label: this.dataSourceUrls.labels,
+ epic: this.dataSourceUrls.epics,
+ milestone: this.dataSourceUrls.milestones,
+ merge_request: this.dataSourceUrls.mergeRequests,
+ vulnerability: this.dataSourceUrls.vulnerabilities,
+ command: this.dataSourceUrls.commands,
+ };
+
+ const searchFields = {
+ user: ['username', 'name'],
+ issue: ['iid', 'title'],
+ snippet: ['id', 'title'],
+ label: ['title'],
+ epic: ['iid', 'title'],
+ vulnerability: ['id', 'title'],
+ merge_request: ['iid', 'title'],
+ milestone: ['title', 'iid'],
+ command: ['name'],
+ emoji: [],
+ };
+
+ const filters = {
+ label: (items) =>
+ items.filter((item) => {
+ if (config.command === '/unlabel') return item.set;
+ if (config.command === '/label') return !item.set;
+
+ return true;
+ }),
+ user: (items) =>
+ items.filter((item) => {
+ const assigned = this.sidebarMediator?.store?.assignees.some(
+ (assignee) => assignee.username === item.username,
+ );
+ const assignedReviewer = this.sidebarMediator?.store?.reviewers.some(
+ (reviewer) => reviewer.username === item.username,
+ );
+
+ if (config.command === '/assign') return !assigned;
+ if (config.command === '/assign_reviewer') return !assignedReviewer;
+ if (config.command === '/unassign') return assigned;
+ if (config.command === '/unassign_reviewer') return assignedReviewer;
+
+ return true;
+ }),
+ emoji: (_, query) =>
+ query
+ ? searchEmoji(query)
+ : getAllEmoji().map((emoji) => ({ emoji, fieldValue: emoji.name })),
+ };
+
+ const sorters = {
+ milestone: customSorter(sortMilestones),
+ default: defaultSorter(searchFields[referenceType]),
+ // do not sort emoji
+ emoji: customSorter(() => 0),
+ };
+
+ const mappers = {
+ milestone: mapMilestone,
+ default: identity,
+ };
+
+ return createDataSource({
+ source: sources[referenceType],
+ searchFields: searchFields[referenceType],
+ mapper: mappers[referenceType] || mappers.default,
+ sorter: sorters[referenceType] || sorters.default,
+ filter: filters[referenceType],
+ cache: config.cache,
+ limit: config.limit,
+ });
+ },
+ (referenceType, config) => JSON.stringify({ referenceType, config }),
+ );
+}
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
index 11a11ed43bd..a4abb8dcf38 100644
--- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -16,8 +16,8 @@ const getRangeFromSourcePos = (sourcePos) => {
const [endRow, endCol] = end.split(':');
return {
- start: { row: Number(startRow) - 1, col: Number(startCol) - 1 },
- end: { row: Number(endRow) - 1, col: Number(endCol) - 1 },
+ start: { row: Math.max(0, Number(startRow) - 1), col: Math.max(0, Number(startCol) - 1) },
+ end: { row: Math.max(0, Number(endRow) - 1), col: Math.max(0, Number(endCol) - 1) },
};
};
@@ -33,8 +33,6 @@ export const getMarkdownSource = (element) => {
for (let i = range.start.row; i <= range.end.row; i += 1) {
if (i === range.start.row) {
elSource += source[i].substring(range.start.col);
- } else if (i === range.end.row) {
- elSource += `\n${source[i]?.substring(0, range.start.col)}`;
} else {
elSource += `\n${source[i]}` || '';
}
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 0897232cf89..87959a44560 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -366,14 +366,16 @@ export function renderPlayable(state, node) {
}
export function renderCodeBlock(state, node) {
+ const numBackticks = Math.max(2, node.textContent.match(/```+/g)?.[0]?.length || 0) + 1;
+ const backticks = state.repeat('`', numBackticks);
state.write(
- `\`\`\`${
+ `${backticks}${
(node.attrs.language || '') + (node.attrs.langParams ? `:${node.attrs.langParams}` : '')
}\n`,
);
state.text(node.textContent, false);
state.ensureNewLine();
- state.write('```');
+ state.write(backticks);
state.closeBlock(node);
}
@@ -478,6 +480,22 @@ export function renderReferenceLabel(state, node) {
state.write(node.attrs.originalText || `~${state.quote(node.attrs.text)}`);
}
+const findChildWithMark = (mark, parent) => {
+ let child;
+ let offset;
+ let index;
+
+ parent.forEach((_child, _offset, _index) => {
+ if (mark.isInSet(_child.marks)) {
+ child = _child;
+ offset = _offset;
+ index = _index;
+ }
+ });
+
+ return child ? { child, offset, index } : null;
+};
+
const generateBoldTags = (wrapTagName = openTag) => {
return (_, mark) => {
const type = /^(\*\*|__|<strong|<b).*/.exec(mark.attrs.sourceMarkdown)?.[1];
@@ -529,13 +547,23 @@ export const italic = {
};
const generateCodeTag = (wrapTagName = openTag) => {
- return (_, mark) => {
+ const isOpen = wrapTagName === openTag;
+
+ return (_, mark, parent) => {
const type = /^(`|<code).*/.exec(mark.attrs.sourceMarkdown)?.[1];
if (type === '<code') {
return wrapTagName(type.substring(1));
}
+ const childText = findChildWithMark(mark, parent).child?.text || '';
+ if (childText.includes('`')) {
+ let tag = '``';
+ if (childText.startsWith('`') || childText.endsWith('`'))
+ tag = isOpen ? `${tag} ` : ` ${tag}`;
+ return tag;
+ }
+
return '`';
};
};
@@ -579,22 +607,6 @@ const normalizeUrl = (url) => {
const isValidAutolinkURL = (url) =>
/(https?:\/\/)?([\w-])+\.{1}([a-zA-Z]{2,63})([/\w-]*)*\/?\??([^#\n\r]*)?#?([^\n\r]*)/.test(url);
-const findChildWithMark = (mark, parent) => {
- let child;
- let offset;
- let index;
-
- parent.forEach((_child, _offset, _index) => {
- if (mark.isInSet(_child.marks)) {
- child = _child;
- offset = _offset;
- index = _index;
- }
- });
-
- return child ? { child, offset, index } : null;
-};
-
/**
* This function detects whether a link should be serialized
* as an autolink.
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
deleted file mode 100644
index ab5f01227fb..00000000000
--- a/app/assets/javascripts/contextual_sidebar.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import { GlBreakpointInstance as bp, breakpoints } from '@gitlab/ui/dist/utils';
-import $ from 'jquery';
-import { debounce } from 'lodash';
-import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
-
-export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
-
-export default class ContextualSidebar {
- constructor() {
- this.initDomElements();
- this.render();
- }
-
- initDomElements() {
- this.$page = $('.layout-page');
- this.$sidebar = $('.nav-sidebar');
-
- if (!this.$sidebar.length) return;
-
- this.$innerScroll = $('.nav-sidebar-inner-scroll', this.$sidebar);
- this.$overlay = $('.mobile-overlay');
- this.$openSidebar = $('.toggle-mobile-nav');
- this.$closeSidebar = $('.close-nav-button');
- this.$sidebarToggle = $('.js-toggle-sidebar');
- }
-
- bindEvents() {
- if (!this.$sidebar.length) return;
-
- this.$openSidebar.on('click', () => this.toggleSidebarNav(true));
- this.$closeSidebar.on('click', () => this.toggleSidebarNav(false));
- this.$overlay.on('click', () => this.toggleSidebarNav(false));
- this.$sidebarToggle.on('click', () => {
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- this.toggleSidebarNav(!this.$sidebar.hasClass('sidebar-expanded-mobile'));
- } else {
- const value = !this.$sidebar.hasClass('sidebar-collapsed-desktop');
- this.toggleCollapsedSidebar(value, true);
- }
- });
-
- $(window).on(
- 'resize',
- debounce(() => this.render(), 100),
- );
- }
-
- // See documentation: https://design.gitlab.com/regions/navigation#contextual-navigation
- // NOTE: at 1200px nav sidebar should not overlap the content
- // https://gitlab.com/gitlab-org/gitlab-foss/merge_requests/24555#note_134136110
- static isDesktopBreakpoint = () => bp.windowWidth() >= breakpoints.xl;
- static setCollapsedCookie(value) {
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- return;
- }
- setCookie('sidebar_collapsed', value, { expires: 365 * 10 });
- }
-
- toggleSidebarNav(show) {
- const breakpoint = bp.getBreakpointSize();
- const dbp = ContextualSidebar.isDesktopBreakpoint();
- const supportedSizes = ['xs', 'sm', 'md'];
-
- this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
- this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
- this.$overlay.toggleClass(
- 'mobile-nav-open',
- supportedSizes.includes(breakpoint) ? show : false,
- );
- this.$sidebar.removeClass('sidebar-collapsed-desktop');
- }
-
- toggleCollapsedSidebar(collapsed, saveCookie) {
- const breakpoint = bp.getBreakpointSize();
- const dbp = ContextualSidebar.isDesktopBreakpoint();
- const supportedSizes = ['xs', 'sm', 'md'];
-
- if (this.$sidebar.length) {
- this.$sidebar.toggleClass(`sidebar-collapsed-desktop ${SIDEBAR_COLLAPSED_CLASS}`, collapsed);
- this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? !collapsed : false);
- this.$page.toggleClass(
- 'page-with-icon-sidebar',
- supportedSizes.includes(breakpoint) ? true : collapsed,
- );
- }
-
- if (saveCookie) {
- ContextualSidebar.setCollapsedCookie(collapsed);
- }
-
- requestIdleCallback(() => this.toggleSidebarOverflow());
- }
-
- toggleSidebarOverflow() {
- if (this.$innerScroll.prop('scrollHeight') > this.$innerScroll.prop('offsetHeight')) {
- this.$innerScroll.css('overflow-y', 'scroll');
- } else {
- this.$innerScroll.css('overflow-y', '');
- }
- }
-
- render() {
- if (!this.$sidebar.length) return;
-
- if (!ContextualSidebar.isDesktopBreakpoint()) {
- this.toggleSidebarNav(false);
- } else {
- const collapse = parseBoolean(getCookie('sidebar_collapsed'));
- this.toggleCollapsedSidebar(collapse, true);
- }
- }
-}
diff --git a/app/assets/javascripts/contributors/components/contributor_area_chart.vue b/app/assets/javascripts/contributors/components/contributor_area_chart.vue
new file mode 100644
index 00000000000..51d890078c1
--- /dev/null
+++ b/app/assets/javascripts/contributors/components/contributor_area_chart.vue
@@ -0,0 +1,68 @@
+<script>
+import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import { __ } from '~/locale';
+import { dateFormats } from '~/analytics/shared/constants';
+import dateFormat from '~/lib/dateformat';
+
+export default {
+ name: 'ContributorAreaChart',
+ components: {
+ GlAreaChart,
+ },
+ props: {
+ data: {
+ type: Array,
+ required: true,
+ },
+ option: {
+ type: Object,
+ required: true,
+ },
+ height: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ tooltipTitle: '',
+ tooltipValue: [],
+ };
+ },
+ computed: {
+ tooltipLabel() {
+ return this.option.yAxis?.name || __('Value');
+ },
+ },
+ methods: {
+ formatTooltipText({ seriesData }) {
+ const [dateTime, value] = seriesData[0].data;
+ this.tooltipTitle = dateFormat(dateTime, dateFormats.defaultDate);
+ this.tooltipValue = value;
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-area-chart
+ responsive
+ width="auto"
+ :data="data"
+ :option="option"
+ :height="height"
+ :format-tooltip-text="formatTooltipText"
+ @created="$emit('created', $event)"
+ >
+ <template #tooltip-title>
+ <div data-testid="tooltip-title">{{ tooltipTitle }}</div>
+ </template>
+
+ <template #tooltip-content>
+ <div class="gl-display-flex gl-justify-content-space-between gl-gap-6">
+ <span data-testid="tooltip-label">{{ tooltipLabel }}</span>
+ <span data-testid="tooltip-value">{{ tooltipValue }}</span>
+ </div>
+ </template>
+ </gl-area-chart>
+</template>
diff --git a/app/assets/javascripts/contributors/components/contributors.vue b/app/assets/javascripts/contributors/components/contributors.vue
index 21428ff9eca..9b834793428 100644
--- a/app/assets/javascripts/contributors/components/contributors.vue
+++ b/app/assets/javascripts/contributors/components/contributors.vue
@@ -1,7 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
-import { GlAreaChart } from '@gitlab/ui/dist/charts';
import { debounce, uniq } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
@@ -12,6 +11,7 @@ import { __ } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import { xAxisLabelFormatter, dateFormatter } from '../utils';
+import ContributorAreaChart from './contributor_area_chart.vue';
const GRAPHS_PATH_REGEX = /^(.*?)\/-\/graphs/g;
@@ -24,9 +24,9 @@ export default {
},
},
components: {
- GlAreaChart,
GlButton,
GlLoadingIcon,
+ ContributorAreaChart,
RefSelector,
},
props: {
@@ -249,10 +249,8 @@ export default {
<div data-testid="contributors-charts">
<h4 class="gl-mb-2 gl-mt-5">{{ __('Commits to') }} {{ branch }}</h4>
<span>{{ __('Excluding merge commits. Limited to 6,000 commits.') }}</span>
- <gl-area-chart
+ <contributor-area-chart
class="gl-mb-5"
- responsive
- width="auto"
:data="masterChartData"
:option="masterChartOptions"
:height="masterChartHeight"
@@ -269,9 +267,7 @@ export default {
<p class="gl-mb-3">
{{ n__('%d commit', '%d commits', contributor.commits) }} ({{ contributor.email }})
</p>
- <gl-area-chart
- responsive
- width="auto"
+ <contributor-area-chart
:data="contributor.dates"
:option="individualChartOptions"
:height="individualChartHeight"
diff --git a/app/assets/javascripts/deploy_keys/graphql/client.js b/app/assets/javascripts/deploy_keys/graphql/client.js
new file mode 100644
index 00000000000..3c183963683
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/client.js
@@ -0,0 +1,47 @@
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import typeDefs from './typedefs.graphql';
+import { resolvers } from './resolvers';
+
+export const createApolloProvider = (endpoints) => {
+ const defaultClient = createDefaultClient(resolvers(endpoints), {
+ typeDefs,
+ cacheConfig: {
+ typePolicies: {
+ Query: {
+ fields: {
+ currentScope: {
+ read(data) {
+ return data || 'enabledKeys';
+ },
+ },
+ currentPage: {
+ read(data) {
+ return data || 1;
+ },
+ },
+ pageInfo: {
+ read(data) {
+ return data || {};
+ },
+ },
+ deployKeyToRemove: {
+ read(data) {
+ return data || null;
+ },
+ },
+ },
+ },
+ LocalDeployKey: {
+ deployKeysProjects: {
+ merge(_, incoming) {
+ return incoming;
+ },
+ },
+ },
+ },
+ },
+ });
+
+ return new VueApollo({ defaultClient });
+};
diff --git a/app/assets/javascripts/deploy_keys/graphql/mutations/confirm_action.mutation.graphql b/app/assets/javascripts/deploy_keys/graphql/mutations/confirm_action.mutation.graphql
new file mode 100644
index 00000000000..adc78e6d2d2
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/mutations/confirm_action.mutation.graphql
@@ -0,0 +1,3 @@
+mutation confirmDisable($id: ID) {
+ confirmDisable(id: $id) @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/mutations/disable_key.mutation.graphql b/app/assets/javascripts/deploy_keys/graphql/mutations/disable_key.mutation.graphql
new file mode 100644
index 00000000000..923dd636785
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/mutations/disable_key.mutation.graphql
@@ -0,0 +1,3 @@
+mutation disableKey($id: ID!) {
+ disableKey(id: $id) @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/mutations/enable_key.mutation.graphql b/app/assets/javascripts/deploy_keys/graphql/mutations/enable_key.mutation.graphql
new file mode 100644
index 00000000000..fb978679b7c
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/mutations/enable_key.mutation.graphql
@@ -0,0 +1,3 @@
+mutation enableKey($id: ID!) {
+ enableKey(id: $id) @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_page.mutation.graphql b/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_page.mutation.graphql
new file mode 100644
index 00000000000..8e6438cdad0
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_page.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateCurrentDeployKeyPage($page: String) {
+ currentPage(page: $page) @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_scope.mutation.graphql b/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_scope.mutation.graphql
new file mode 100644
index 00000000000..3502eee5142
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/mutations/update_current_scope.mutation.graphql
@@ -0,0 +1,3 @@
+mutation updateCurrentScope($scope: DeployKeysScope) {
+ currentScope(scope: $scope) @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/queries/confirm_remove_key.query.graphql b/app/assets/javascripts/deploy_keys/graphql/queries/confirm_remove_key.query.graphql
new file mode 100644
index 00000000000..11d6a6ab83c
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/queries/confirm_remove_key.query.graphql
@@ -0,0 +1,5 @@
+query confirmRemoveKey {
+ deployKeyToRemove @client {
+ id
+ }
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/queries/current_page.query.graphql b/app/assets/javascripts/deploy_keys/graphql/queries/current_page.query.graphql
new file mode 100644
index 00000000000..dc02d97531a
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/queries/current_page.query.graphql
@@ -0,0 +1,3 @@
+query getCurrentDeployKeyPage {
+ currentPage @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/queries/current_scope.query.graphql b/app/assets/javascripts/deploy_keys/graphql/queries/current_scope.query.graphql
new file mode 100644
index 00000000000..181f5c52254
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/queries/current_scope.query.graphql
@@ -0,0 +1,3 @@
+query getCurrentScope {
+ currentScope @client
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/queries/deploy_keys.query.graphql b/app/assets/javascripts/deploy_keys/graphql/queries/deploy_keys.query.graphql
new file mode 100644
index 00000000000..c98da2920cc
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/queries/deploy_keys.query.graphql
@@ -0,0 +1,26 @@
+query getDeployKeys($projectPath: ID!, $scope: DeployKeysScope, $page: Integer) {
+ project(fullPath: $projectPath) {
+ id
+ deployKeys(scope: $scope, page: $page) @client {
+ id
+ title
+ fingerprintSha256
+ fingerprint
+ editPath
+ destroyedWhenOrphaned
+ almostOrphaned
+ expiresAt
+ createdAt
+ enablePath
+ disablePath
+ deployKeysProjects {
+ canPush
+ project {
+ id
+ fullPath
+ fullName
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/deploy_keys/graphql/resolvers.js b/app/assets/javascripts/deploy_keys/graphql/resolvers.js
new file mode 100644
index 00000000000..1993801636e
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/resolvers.js
@@ -0,0 +1,106 @@
+import { gql } from '@apollo/client/core';
+import axios from '~/lib/utils/axios_utils';
+import {
+ convertObjectPropsToCamelCase,
+ parseIntPagination,
+ normalizeHeaders,
+} from '~/lib/utils/common_utils';
+import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql';
+import currentPageQuery from './queries/current_page.query.graphql';
+import currentScopeQuery from './queries/current_scope.query.graphql';
+import confirmRemoveKeyQuery from './queries/confirm_remove_key.query.graphql';
+
+export const mapDeployKey = (deployKey) => ({
+ ...convertObjectPropsToCamelCase(deployKey, { deep: true }),
+ __typename: 'LocalDeployKey',
+});
+
+export const resolvers = (endpoints) => ({
+ Project: {
+ deployKeys(_, { scope, page }, { client }) {
+ const key = `${scope}Endpoint`;
+ let { [key]: endpoint } = endpoints;
+
+ if (!endpoint) {
+ endpoint = endpoints.enabledKeysEndpoint;
+ }
+
+ return axios.get(endpoint, { params: { page } }).then(({ headers, data }) => {
+ const normalizedHeaders = normalizeHeaders(headers);
+ const pageInfo = {
+ ...parseIntPagination(normalizedHeaders),
+ __typename: 'LocalPageInfo',
+ };
+ client.writeQuery({
+ query: pageInfoQuery,
+ variables: { input: { page, scope } },
+ data: { pageInfo },
+ });
+ return data?.keys?.map(mapDeployKey) || [];
+ });
+ },
+ },
+ Mutation: {
+ currentPage(_, { page }, { client }) {
+ client.writeQuery({
+ query: currentPageQuery,
+ data: { currentPage: page },
+ });
+ },
+ currentScope(_, { scope }, { client }) {
+ client.writeQuery({
+ query: currentPageQuery,
+ data: { currentPage: 1 },
+ });
+ client.writeQuery({
+ query: currentScopeQuery,
+ data: { currentScope: scope },
+ });
+ },
+ disableKey(_, _variables, { client }) {
+ const {
+ deployKeyToRemove: { id },
+ } = client.readQuery({
+ query: confirmRemoveKeyQuery,
+ });
+
+ const fragment = gql`
+ fragment DisablePath on LocalDeployKey {
+ disablePath
+ }
+ `;
+
+ const { disablePath } = client.readFragment({ fragment, id: `LocalDeployKey:${id}` });
+
+ return axios.put(disablePath).then(({ data }) => {
+ client.cache.evict({ fieldName: 'deployKeyToRemove' });
+ client.cache.evict({ id: `LocalDeployKey:${id}` });
+ client.cache.gc();
+
+ return data;
+ });
+ },
+ enableKey(_, { id }, { client }) {
+ const fragment = gql`
+ fragment EnablePath on LocalDeployKey {
+ enablePath
+ }
+ `;
+
+ const { enablePath } = client.readFragment({ fragment, id: `LocalDeployKey:${id}` });
+
+ return axios.put(enablePath).then(({ data }) => {
+ client.cache.evict({ id: `LocalDeployKey:${id}` });
+ client.cache.gc();
+
+ return data;
+ });
+ },
+ confirmDisable(_, { id }, { client }) {
+ client.writeQuery({
+ query: confirmRemoveKeyQuery,
+ data: { deployKeyToRemove: id ? { id, __type: 'LocalDeployKey' } : null },
+ });
+ },
+ },
+});
diff --git a/app/assets/javascripts/deploy_keys/graphql/typedefs.graphql b/app/assets/javascripts/deploy_keys/graphql/typedefs.graphql
new file mode 100644
index 00000000000..a08dda3da92
--- /dev/null
+++ b/app/assets/javascripts/deploy_keys/graphql/typedefs.graphql
@@ -0,0 +1,45 @@
+#import "~/graphql_shared/client/page_info.typedefs.graphql"
+
+enum DeployKeysScope {
+ enabledKeys
+ availableProjectKeys
+ availablePublicKeys
+}
+
+enum LocalDeployKeyActions {
+ enable
+ disable
+}
+
+type LocalProject {
+ id: ID!
+ fullPath: String
+ fullName: String
+}
+
+type LocalDeployKeysProject {
+ canPush: Boolean
+ projects: [LocalProject]
+}
+
+type LocalDeployKey {
+ id: ID!
+ title: String
+ fingerprintSha256: String
+ fingerprint: String
+ editPath: String
+ isEnabled: Boolean
+ destroyedWhenOrphaned: Boolean
+ almostOrphaned: Boolean
+ expiresAt: String
+ createdAt: String
+ deployKeysProjects: [LocalDeployKeysProject]
+}
+
+extend type LocalPageInfoInput {
+ scope: DeployKeysScope
+}
+
+extend type Project {
+ deployKeys: [LocalDeployKey]
+}
diff --git a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
index f21086185fb..d0885fb8687 100644
--- a/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
+++ b/app/assets/javascripts/deploy_tokens/components/new_deploy_token.vue
@@ -266,7 +266,6 @@ export default {
class="gl-form-input-xl"
name="deploy_token_expires_at"
:value="formattedExpiryDate"
- data-qa-selector="deploy_token_expires_at_field"
/>
</gl-form-group>
<gl-form-group
@@ -298,7 +297,6 @@ export default {
:key="scope.id"
v-model="scope.value"
:name="scope.id"
- :data-qa-selector="`${scope.id}_checkbox`"
>
{{ scope.scopeName }}
<template #help>{{ scope.helpText }}</template>
diff --git a/app/assets/javascripts/deprecated_notes.js b/app/assets/javascripts/deprecated_notes.js
index 9b5b4cef1b9..26500c37acf 100644
--- a/app/assets/javascripts/deprecated_notes.js
+++ b/app/assets/javascripts/deprecated_notes.js
@@ -844,7 +844,7 @@ export default class Notes {
const selector = this.getEditFormSelector($target);
const $editForm = $(selector);
- $editForm.insertBefore('.diffs');
+ $editForm.insertBefore('.js-snippets-note-edit-form-holder');
$editForm.find('.js-comment-save-button').enable();
$editForm.find('.js-finish-edit-warning').hide();
}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 54c276c36b1..00fd9f43a4f 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -115,6 +115,11 @@ export default {
required: false,
default: false,
},
+ codequalityReportAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
endpointCodequality: {
type: String,
required: false,
@@ -147,6 +152,7 @@ export default {
subscribedToVirtualScrollingEvents: false,
autoScrolled: false,
activeProject: undefined,
+ hasScannerError: false,
};
},
apollo: {
@@ -157,26 +163,31 @@ export default {
return { fullPath: this.projectPath, iid: this.iid };
},
skip() {
- const codeQualityBoolean = Boolean(this.endpointCodequality);
+ if (this.hasScannerError) {
+ return true;
+ }
- return !this.sastReportsInInlineDiff || (!codeQualityBoolean && !this.sastReportAvailable);
+ return (
+ !this.sastReportsInInlineDiff ||
+ (!this.codequalityReportAvailable && !this.sastReportAvailable)
+ );
},
update(data) {
- const codeQualityBoolean = Boolean(this.endpointCodequality);
const { codequalityReportsComparer, sastReport } = data?.project?.mergeRequest || {};
this.activeProject = data?.project?.mergeRequest?.project;
if (
(sastReport?.status === FINDINGS_STATUS_PARSED || !this.sastReportAvailable) &&
- (!codeQualityBoolean || codequalityReportsComparer.status === FINDINGS_STATUS_PARSED)
+ (!this.codequalityReportAvailable ||
+ codequalityReportsComparer.status === FINDINGS_STATUS_PARSED)
) {
- this.getMRCodequalityAndSecurityReportStopPolling(
- this.$apollo.queries.getMRCodequalityAndSecurityReports,
- );
+ this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling();
}
if (sastReport?.status === FINDINGS_STATUS_ERROR && this.sastReportAvailable) {
this.fetchScannerFindingsError();
+
+ this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling();
}
if (codequalityReportsComparer?.report?.newErrors) {
@@ -192,6 +203,7 @@ export default {
},
error() {
this.fetchScannerFindingsError();
+ this.$apollo.queries.getMRCodequalityAndSecurityReports.stopPolling();
},
},
},
@@ -432,8 +444,9 @@ export default {
this.setDrawer({});
},
fetchScannerFindingsError() {
+ this.hasScannerError = true;
createAlert({
- message: __('Something went wrong fetching the Scanner Findings. Please try again.'),
+ message: __('Something went wrong fetching the scanner findings. Please try again.'),
});
},
subscribeToEvents() {
@@ -445,9 +458,6 @@ export default {
diffsEventHub.$on(EVT_MR_PREPARED, this.fetchData);
diffsEventHub.$on(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
},
- getMRCodequalityAndSecurityReportStopPolling(query) {
- query.stopPolling();
- },
unsubscribeFromEvents() {
diffsEventHub.$off(EVT_DISCUSSIONS_ASSIGNED, this.handleHash);
diffsEventHub.$off(EVT_MR_PREPARED, this.fetchData);
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 8c1cab20ece..82b721da493 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -9,7 +9,7 @@ import DiffContent from 'jh_else_ce/diffs/components/diff_content.vue';
import { createAlert } from '~/alert';
import { hasDiff } from '~/helpers/diffs_helper';
import { diffViewerErrors } from '~/ide/constants';
-import { scrollToElement } from '~/lib/utils/common_utils';
+import { scrollToElement, isElementStuck } from '~/lib/utils/common_utils';
import { sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import notesEventHub from '~/notes/event_hub';
@@ -170,6 +170,11 @@ export default {
showWarning() {
return this.isCollapsed && this.automaticallyCollapsed && !this.viewDiffsFileByFile;
},
+ expandableWarning() {
+ return this.file.viewer?.generated
+ ? this.$options.i18n.autoCollapsedGenerated
+ : this.$options.i18n.autoCollapsed;
+ },
showContent() {
return !this.isCollapsed && !this.isFileTooLarge;
},
@@ -295,8 +300,13 @@ export default {
collapsed: collapsingNow,
});
- if (collapsingNow && viaUserInteraction && contentElement) {
- scrollToElement(contentElement, { duration: 1 });
+ if (
+ collapsingNow &&
+ viaUserInteraction &&
+ contentElement &&
+ isElementStuck(this.$refs.header.$el)
+ ) {
+ scrollToElement(contentElement, { duration: 0 });
}
if (!this.hasDiff && !collapsingNow) {
@@ -381,6 +391,7 @@ export default {
class="diff-file file-holder gl-border-none gl-mb-0! gl-pb-5"
>
<diff-file-header
+ ref="header"
:can-current-user-fork="canCurrentUserFork"
:diff-file="file"
:collapsible="true"
@@ -419,6 +430,7 @@ export default {
<div
:id="`diff-content-${file.file_hash}`"
:class="hasBodyClasses.contentByHash"
+ class="diff-content"
data-testid="content-area"
>
<gl-alert
@@ -523,7 +535,7 @@ export default {
class="collapsed-file-warning gl-p-7 gl-bg-orange-50 gl-text-center gl-rounded-bottom-left-base gl-rounded-bottom-right-base"
>
<p class="gl-mb-5">
- {{ $options.i18n.autoCollapsed }}
+ {{ expandableWarning }}
</p>
<gl-button data-testid="expand-button" @click.prevent="handleToggle">
{{ $options.i18n.expand }}
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index 20f82500a02..e45fd508a5b 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -211,19 +211,6 @@ export default {
return this.getNoteableData.current_user.can_create_note;
},
},
- watch: {
- 'idState.moreActionsShown': {
- handler(val) {
- const el = this.$el.closest('.vue-recycle-scroller__item-view');
-
- if (el) {
- // We can't add a style with Vue because of the way the virtual
- // scroller library renders the diff files
- el.style.zIndex = val ? '1' : null;
- }
- },
- },
- },
methods: {
...mapActions('diffs', [
'toggleFileDiscussions',
diff --git a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue b/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
deleted file mode 100644
index 6cb1ed4cbcf..00000000000
--- a/app/assets/javascripts/diffs/components/merge_conflict_warning.vue
+++ /dev/null
@@ -1,62 +0,0 @@
-<script>
-import { GlButton, GlAlert, GlModalDirective } from '@gitlab/ui';
-
-export default {
- components: {
- GlAlert,
- GlButton,
- },
- directives: {
- GlModalDirective,
- },
- props: {
- mergeable: {
- type: Boolean,
- required: true,
- },
- resolutionPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-alert
- :dismissible="false"
- :title="__('There are merge conflicts')"
- variant="warning"
- class="gl-mb-5"
- >
- <p class="gl-mb-2">
- {{ __('The comparison view may be inaccurate due to merge conflicts.') }}
- </p>
- <p class="gl-mb-0">
- {{
- __(
- 'Resolve these conflicts, or ask someone with write access to this repository to resolve them locally.',
- )
- }}
- </p>
- <template #actions>
- <gl-button
- v-if="resolutionPath"
- :href="resolutionPath"
- variant="confirm"
- class="gl-mr-3 gl-alert-action"
- >
- {{ __('Resolve conflicts') }}
- </gl-button>
- <gl-button
- v-if="mergeable"
- v-gl-modal-directive="'modal-merge-info'"
- class="gl-alert-action"
- >
- {{ __('Resolve locally') }}
- </gl-button>
- </template>
- </gl-alert>
- </div>
-</template>
diff --git a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
index 2c1a8305935..854f6910fa1 100644
--- a/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
+++ b/app/assets/javascripts/diffs/components/shared/findings_drawer.vue
@@ -1,9 +1,10 @@
<script>
-import { GlBadge, GlDrawer, GlIcon, GlLink } from '@gitlab/ui';
+import { GlBadge, GlDrawer, GlLink, GlButton, GlIcon } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getSeverity } from '~/ci/reports/utils';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
+import { SAST_FINDING_DISMISSED } from '../../constants';
import DrawerItem from './findings_drawer_item.vue';
export const i18n = {
@@ -26,7 +27,7 @@ export const codeQuality = 'codeQuality';
export default {
i18n,
codeQuality,
- components: { GlBadge, GlDrawer, GlIcon, GlLink, DrawerItem },
+ components: { GlBadge, GlDrawer, GlLink, GlButton, GlIcon, DrawerItem },
props: {
drawer: {
type: Object,
@@ -38,19 +39,50 @@ export default {
default: () => {},
},
},
+ data() {
+ return {
+ activeIndex: 0,
+ };
+ },
computed: {
getDrawerHeaderHeight() {
return getContentWrapperHeight();
},
isCodeQuality() {
- return this.drawer.scale === this.$options.codeQuality;
+ return this.activeElement.scale === this.$options.codeQuality;
+ },
+ activeElement() {
+ return this.drawer.findings[this.activeIndex];
+ },
+ findingsStatus() {
+ return this.activeElement.state === SAST_FINDING_DISMISSED ? 'muted' : 'warning';
},
},
DRAWER_Z_INDEX,
+ watch: {
+ drawer(newVal) {
+ this.activeIndex = newVal.index;
+ },
+ },
methods: {
getSeverity,
+ prev() {
+ if (this.activeIndex === 0) {
+ this.activeIndex = this.drawer.findings.length - 1;
+ } else {
+ this.activeIndex -= 1;
+ }
+ },
+ next() {
+ if (this.activeIndex === this.drawer.findings.length - 1) {
+ this.activeIndex = 0;
+ } else {
+ this.activeIndex += 1;
+ }
+ },
+
concatIdentifierName(name, index) {
- return name + (index !== this.drawer.identifiers.length - 1 ? ', ' : '');
+ return name + (index !== this.activeElement.identifiers.length - 1 ? ', ' : '');
},
},
};
@@ -64,36 +96,51 @@ export default {
@close="$emit('close')"
>
<template #title>
- <h2 class="drawer-heading gl-font-base gl-mt-0 gl-mb-0">
+ <h2 class="drawer-heading gl-font-base gl-mt-0 gl-mb-0 gl-w-28">
<gl-icon
:size="12"
- :name="getSeverity(drawer).name"
- :class="getSeverity(drawer).class"
+ :name="getSeverity(activeElement).name"
+ :class="getSeverity(activeElement).class"
class="inline-findings-severity-icon gl-vertical-align-baseline!"
/>
- <span class="drawer-heading-severity">{{ drawer.severity }}</span>
+ <span class="drawer-heading-severity">{{ activeElement.severity }}</span>
{{ isCodeQuality ? $options.i18n.codeQualityFinding : $options.i18n.sastFinding }}
</h2>
+ <div v-if="drawer.findings.length > 1">
+ <gl-button data-testid="findings-drawer-prev-button" class="gl-p-1!" @click="prev">
+ <gl-icon :size="24" name="chevron-left" />
+ </gl-button>
+ <gl-button class="gl-p-1!" @click="next">
+ <gl-icon data-testid="findings-drawer-next-button" :size="24" name="chevron-right" />
+ </gl-button>
+ </div>
</template>
<template #default>
<ul class="gl-list-style-none gl-border-b-initial gl-mb-0 gl-pb-0!">
- <drawer-item v-if="drawer.title" :description="$options.i18n.name" :value="drawer.title" />
+ <drawer-item
+ v-if="activeElement.title"
+ :description="$options.i18n.name"
+ :value="activeElement.title"
+ data-testid="findings-drawer-title"
+ />
- <drawer-item v-if="drawer.state" :description="$options.i18n.status">
+ <drawer-item v-if="activeElement.state" :description="$options.i18n.status">
<template #value>
- <gl-badge variant="warning" class="text-capitalize">{{ drawer.state }}</gl-badge>
+ <gl-badge :variant="findingsStatus" class="text-capitalize">{{
+ activeElement.state
+ }}</gl-badge>
</template>
</drawer-item>
<drawer-item
- v-if="drawer.description"
+ v-if="activeElement.description"
:description="$options.i18n.description"
- :value="drawer.description"
+ :value="activeElement.description"
/>
<drawer-item
- v-if="project && drawer.scale !== $options.codeQuality"
+ v-if="project && activeElement.scale !== $options.codeQuality"
:description="$options.i18n.project"
>
<template #value>
@@ -101,23 +148,31 @@ export default {
</template>
</drawer-item>
- <drawer-item v-if="drawer.location || drawer.webUrl" :description="$options.i18n.file">
+ <drawer-item
+ v-if="activeElement.location || activeElement.webUrl"
+ :description="$options.i18n.file"
+ >
<template #value>
- <span v-if="drawer.webUrl && drawer.filePath && drawer.line">
- <gl-link :href="drawer.webUrl">{{ drawer.filePath }}:{{ drawer.line }}</gl-link>
+ <span v-if="activeElement.webUrl && activeElement.filePath && activeElement.line">
+ <gl-link :href="activeElement.webUrl"
+ >{{ activeElement.filePath }}:{{ activeElement.line }}</gl-link
+ >
</span>
- <span v-else-if="drawer.location">
- {{ drawer.location.file }}:{{ drawer.location.startLine }}
+ <span v-else-if="activeElement.location">
+ {{ activeElement.location.file }}:{{ activeElement.location.startLine }}
</span>
</template>
</drawer-item>
<drawer-item
- v-if="drawer.identifiers && drawer.identifiers.length"
+ v-if="activeElement.identifiers && activeElement.identifiers.length"
:description="$options.i18n.identifiers"
>
<template #value>
- <span v-for="(identifier, index) in drawer.identifiers" :key="identifier.externalId">
+ <span
+ v-for="(identifier, index) in activeElement.identifiers"
+ :key="identifier.externalId"
+ >
<gl-link v-if="identifier.url" :href="identifier.url">
{{ concatIdentifierName(identifier.name, index) }}
</gl-link>
@@ -129,15 +184,15 @@ export default {
</drawer-item>
<drawer-item
- v-if="drawer.scale"
+ v-if="activeElement.scale"
:description="$options.i18n.tool"
:value="isCodeQuality ? $options.i18n.codeQuality : $options.i18n.sast"
/>
<drawer-item
- v-if="drawer.engineName"
+ v-if="activeElement.engineName"
:description="$options.i18n.engine"
- :value="drawer.engineName"
+ :value="activeElement.engineName"
/>
</ul>
</template>
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index e48eb10753c..351df1d1996 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -118,3 +118,6 @@ export const TRACKING_MULTIPLE_FILES_MODE = 'i_code_review_diff_multiple_files';
// UI
export const ZERO_CHANGES_ALT_DISPLAY = '-';
+
+// SAST Findings
+export const SAST_FINDING_DISMISSED = 'dismissed';
diff --git a/app/assets/javascripts/diffs/i18n.js b/app/assets/javascripts/diffs/i18n.js
index 15e3893d22a..651cae11c47 100644
--- a/app/assets/javascripts/diffs/i18n.js
+++ b/app/assets/javascripts/diffs/i18n.js
@@ -22,6 +22,9 @@ export const DIFF_FILE = {
fork: __('Fork'),
cancel: __('Cancel'),
autoCollapsed: __('Files with large changes are collapsed by default.'),
+ autoCollapsedGenerated: __(
+ 'Generated files are collapsed by default. This behavior can be overriden via .gitattributes file if required.',
+ ),
expand: __('Expand file'),
};
export const START_THREAD = __('Start another thread');
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 034dd4cf6d2..15e4225f062 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -36,7 +36,8 @@ export default function initDiffsApp(store = notesStore) {
iid: dataset.iid || '',
endpointCoverage: dataset.endpointCoverage || '',
endpointCodequality: dataset.endpointCodequality || '',
- sastReportAvailable: dataset.endpointSast,
+ codequalityReportAvailable: parseBoolean(dataset.codequalityReportAvailable),
+ sastReportAvailable: parseBoolean(dataset.sastReportAvailable),
helpPagePath: dataset.helpPagePath,
currentUser: JSON.parse(dataset.currentUserData) || {},
changesEmptyStateIllustration: dataset.changesEmptyStateIllustration,
@@ -86,6 +87,7 @@ export default function initDiffsApp(store = notesStore) {
iid: this.iid,
endpointCoverage: this.endpointCoverage,
endpointCodequality: this.endpointCodequality,
+ codequalityReportAvailable: this.codequalityReportAvailable,
sastReportAvailable: this.sastReportAvailable,
currentUser: this.currentUser,
helpPagePath: this.helpPagePath,
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index fcaf8e99b2d..1c0e20183e2 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -15,6 +15,7 @@ import notesEventHub from '~/notes/event_hub';
import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
import { sortTree } from '~/ide/stores/utils';
import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
+import { isCollapsed } from '~/diffs/utils/diff_file';
import {
PARALLEL_DIFF_VIEW_TYPE,
INLINE_DIFF_VIEW_TYPE,
@@ -73,6 +74,7 @@ import {
prepareLineForRenamedFile,
parseUrlHashAsFileHash,
isUrlHashNoteLink,
+ findDiffFile,
} from './utils';
export const setBaseConfig = ({ commit }, options) => {
@@ -658,18 +660,18 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
const { fileHash } = state.treeEntries[path];
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
- document.location.hash = fileHash;
+
+ const newUrl = new URL(window.location);
+ newUrl.hash = fileHash;
+ historyPushState(newUrl, { skipScrolling: true });
+ scrollToElement('.diff-files-holder', { duration: 0 });
if (!getters.isTreePathLoaded(path)) {
- dispatch('fetchFileByFile')
- .then(() => {
- dispatch('scrollToFile', { path });
- })
- .catch(() => {
- createAlert({
- message: LOAD_SINGLE_DIFF_FAILED,
- });
+ dispatch('fetchFileByFile').catch(() => {
+ createAlert({
+ message: LOAD_SINGLE_DIFF_FAILED,
});
+ });
}
}
};
@@ -1041,8 +1043,15 @@ export function reviewFile({ commit, state }, { file, reviewed = true }) {
export const disableVirtualScroller = ({ commit }) => commit(types.DISABLE_VIRTUAL_SCROLLING);
-export const toggleFileCommentForm = ({ commit }, filePath) =>
- commit(types.TOGGLE_FILE_COMMENT_FORM, filePath);
+export const toggleFileCommentForm = ({ state, commit }, filePath) => {
+ const file = findDiffFile(state.diffFiles, filePath, 'file_path');
+ if (isCollapsed(file)) {
+ commit(types.SET_FILE_COMMENT_FORM, { filePath, expanded: true });
+ } else {
+ commit(types.TOGGLE_FILE_COMMENT_FORM, filePath);
+ }
+ commit(types.SET_FILE_COLLAPSED, { filePath, collapsed: false });
+};
export const addDraftToFile = ({ commit }, { filePath, draft }) =>
commit(types.ADD_DRAFT_TO_FILE, { filePath, draft });
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index c2177bacbcc..b155804c70c 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -53,4 +53,5 @@ export const TOGGLE_LINE_DISCUSSIONS = 'TOGGLE_LINE_DISCUSSIONS';
export const DISABLE_VIRTUAL_SCROLLING = 'DISABLE_VIRTUAL_SCROLLING';
export const TOGGLE_FILE_COMMENT_FORM = 'TOGGLE_FILE_COMMENT_FORM';
+export const SET_FILE_COMMENT_FORM = 'SET_FILE_COMMENT_FORM';
export const ADD_DRAFT_TO_FILE = 'ADD_DRAFT_TO_FILE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index 08c195469e3..bc5ed3c40df 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -394,6 +394,11 @@ export default {
file.hasCommentForm = !file.hasCommentForm;
},
+ [types.SET_FILE_COMMENT_FORM](state, { filePath, expanded }) {
+ const file = findDiffFile(state.diffFiles, filePath, 'file_path');
+
+ file.hasCommentForm = expanded;
+ },
[types.ADD_DRAFT_TO_FILE](state, { filePath, draft }) {
const file = findDiffFile(state.diffFiles, filePath, 'file_path');
diff --git a/app/assets/javascripts/drawio/drawio_editor.js b/app/assets/javascripts/drawio/drawio_editor.js
index 37bfde0ed9f..63cc90b5db2 100644
--- a/app/assets/javascripts/drawio/drawio_editor.js
+++ b/app/assets/javascripts/drawio/drawio_editor.js
@@ -1,4 +1,4 @@
-import _ from 'lodash';
+import { isNil } from 'lodash';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { darkModeEnabled } from '~/lib/utils/color_utils';
import { base64DecodeUnicode } from '~/lib/utils/text_utility';
@@ -181,7 +181,7 @@ function configureDrawIOEditor(drawIOEditorState) {
}
function onDrawIOEditorMessage(drawIOEditorState, editorFacade, evt) {
- if (_.isNil(evt) || evt.source !== drawIOEditorState.iframe.contentWindow) {
+ if (isNil(evt) || evt.source !== drawIOEditorState.iframe.contentWindow) {
return;
}
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index f90d29c84b8..9ee4f7cf4aa 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -63,7 +63,7 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
const dropzone = $formDropzone.dropzone({
url: uploadsPath,
dictDefaultMessage: '',
- clickable: true,
+ clickable: form.get(0).querySelector('[data-button-type="attach-file"]') ?? true,
paramName: 'file',
maxFilesize: maxFileSize,
uploadMultiple: false,
diff --git a/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js b/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js
new file mode 100644
index 00000000000..e3b9d273efd
--- /dev/null
+++ b/app/assets/javascripts/editor/extensions/source_editor_security_policy_schema_ext.js
@@ -0,0 +1,84 @@
+import { registerSchema } from '~/ide/utils';
+import axios from '~/lib/utils/axios_utils';
+import { getBaseURL, joinPaths } from '~/lib/utils/url_utility';
+
+export const getSecurityPolicyListUrl = ({ namespacePath, namespaceType = 'group' }) => {
+ const isGroup = namespaceType === 'group';
+ return joinPaths(
+ getBaseURL(),
+ isGroup ? 'groups' : '',
+ namespacePath,
+ '-',
+ 'security',
+ 'policies',
+ );
+};
+
+export const getSecurityPolicySchemaUrl = ({ namespacePath, namespaceType }) => {
+ const policyListUrl = getSecurityPolicyListUrl({ namespacePath, namespaceType });
+ return joinPaths(policyListUrl, 'schema');
+};
+
+export const getSinglePolicySchema = async ({ namespacePath, namespaceType, policyType }) => {
+ try {
+ const { data: schemaForMultiplePolicies } = await axios.get(
+ getSecurityPolicySchemaUrl({ namespacePath, namespaceType }),
+ );
+ return {
+ $id: schemaForMultiplePolicies.$id,
+ title: schemaForMultiplePolicies.title,
+ description: schemaForMultiplePolicies.description,
+ type: schemaForMultiplePolicies.type,
+ properties: {
+ type: {
+ type: 'string',
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ description: 'Specifies the type of policy to be enforced.',
+ enum: policyType,
+ },
+ ...schemaForMultiplePolicies.properties[policyType].items.properties,
+ },
+ };
+ } catch {
+ return {};
+ }
+};
+
+export class SecurityPolicySchemaExtension {
+ static get extensionName() {
+ return 'SecurityPolicySchema';
+ }
+ // eslint-disable-next-line class-methods-use-this
+ provides() {
+ return {
+ registerSecurityPolicyEditorSchema: async (instance, options) => {
+ const { namespacePath, namespaceType, policyType } = options;
+ const singlePolicySchema = await getSinglePolicySchema({
+ namespacePath,
+ namespaceType,
+ policyType,
+ });
+ const modelFileName = instance.getModel().uri.path.split('/').pop();
+
+ registerSchema({
+ uri: getSecurityPolicySchemaUrl({ namespacePath, namespaceType }),
+ schema: singlePolicySchema,
+ fileMatch: [modelFileName],
+ });
+ },
+
+ registerSecurityPolicySchema: (instance, projectPath) => {
+ const uri = getSecurityPolicySchemaUrl({
+ namespacePath: projectPath,
+ namespaceType: 'project',
+ });
+ const modelFileName = instance.getModel().uri.path.split('/').pop();
+
+ registerSchema({
+ uri,
+ fileMatch: [modelFileName],
+ });
+ },
+ };
+ }
+}
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 308a68544bc..1fb68394912 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -58,6 +58,9 @@
"interruptible": {
"$ref": "#/definitions/interruptible"
},
+ "id_tokens": {
+ "$ref": "#/definitions/id_tokens"
+ },
"retry": {
"$ref": "#/definitions/retry"
},
@@ -114,6 +117,9 @@
"name": {
"$ref": "#/definitions/workflowName"
},
+ "auto_cancel": {
+ "$ref": "#/definitions/workflowAutoCancel"
+ },
"rules": {
"type": "array",
"items": {
@@ -327,6 +333,10 @@
"load_performance": {
"$ref": "#/definitions/string_file_list",
"markdownDescription": "Path to file or list of files with load performance testing report(s). [Learn More](https://docs.gitlab.com/ee/ci/yaml/artifacts_reports.html#artifactsreportsload_performance)."
+ },
+ "repository_xray": {
+ "$ref": "#/definitions/string_file_list",
+ "description": "Path to file or list of files with Repository X-Ray report(s)."
}
}
}
@@ -509,6 +519,18 @@
"description": "Command or script that should be executed as the container's entrypoint. It will be translated to Docker's --entrypoint option while creating the container. The syntax is similar to Dockerfile's ENTRYPOINT directive, where each shell token is a separate string in the array.",
"minItems": 1
},
+ "docker": {
+ "type": "object",
+ "markdownDescription": "Options to pass to Runners Docker Executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#imagedocker)",
+ "additionalProperties": false,
+ "properties": {
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Image architecture to pull."
+ }
+ }
+ },
"pull_policy": {
"markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#imagepull_policy).",
"default": "always",
@@ -540,13 +562,6 @@
"required": [
"name"
]
- },
- {
- "type": "array",
- "minLength": 1,
- "items": {
- "type": "string"
- }
}
],
"markdownDescription": "Specifies the docker image to use for the job or globally for all jobs. Job configuration takes precedence over global setting. Requires a certain kind of Gitlab runner executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#image)."
@@ -579,8 +594,20 @@
"type": "string"
}
},
+ "docker": {
+ "type": "object",
+ "markdownDescription": "Options to pass to Runners Docker Executor. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#servicesdocker)",
+ "additionalProperties": false,
+ "properties": {
+ "platform": {
+ "type": "string",
+ "minLength": 1,
+ "description": "Image architecture to pull."
+ }
+ }
+ },
"pull_policy": {
- "markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#servicepull_policy).",
+ "markdownDescription": "Specifies how to pull the image in Runner. It can be one of `always`, `never` or `if-not-present`. The default value is `always`. [Learn more](https://docs.gitlab.com/ee/ci/yaml/#servicespull_policy).",
"default": "always",
"oneOf": [
{
@@ -915,6 +942,21 @@
"minLength": 1,
"maxLength": 255
},
+ "workflowAutoCancel": {
+ "type": "object",
+ "markdownDescription": "Define the rules for when pipeline should be automatically cancelled.",
+ "properties": {
+ "on_job_failure": {
+ "markdownDescription": "Define which jobs to stop after a job fails.",
+ "default": "none",
+ "type": "string",
+ "enum": [
+ "none",
+ "all"
+ ]
+ }
+ }
+ },
"globalVariables": {
"markdownDescription": "Defines default variables for all jobs. Job level property overrides global variables. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#variables).",
"type": "object",
@@ -1480,7 +1522,7 @@
},
{
"const": "data_integrity_failure",
- "description": "Retry if there is a structural integrity problem detected."
+ "description": "Retry if there is an unknown job problem."
}
]
},
diff --git a/app/assets/javascripts/emoji/components/category.vue b/app/assets/javascripts/emoji/components/category.vue
index d8607cbc60b..80850475b96 100644
--- a/app/assets/javascripts/emoji/components/category.vue
+++ b/app/assets/javascripts/emoji/components/category.vue
@@ -52,7 +52,7 @@ export default {
:key="index"
:emojis="emojiGroup"
:render-group="renderGroup"
- :click-emoji="(emoji) => onClick(emoji)"
+ @emoji-click="onClick"
/>
</template>
<p v-else>
diff --git a/app/assets/javascripts/emoji/components/emoji_group.vue b/app/assets/javascripts/emoji/components/emoji_group.vue
index bbac6866636..bb0c3b0a694 100644
--- a/app/assets/javascripts/emoji/components/emoji_group.vue
+++ b/app/assets/javascripts/emoji/components/emoji_group.vue
@@ -1,10 +1,10 @@
<script>
-import { compatFunctionalMixin } from '~/lib/utils/vue3compat/compat_functional_mixin';
+import { GlButton } from '@gitlab/ui';
export default {
- // Temporary mixin for migration from Vue.js 2 to @vue/compat
- mixins: [compatFunctionalMixin],
-
+ components: {
+ GlButton,
+ },
props: {
emojis: {
type: Array,
@@ -14,28 +14,33 @@ export default {
type: Boolean,
required: true,
},
- clickEmoji: {
- type: Function,
- required: true,
+ },
+ methods: {
+ clickEmoji(emoji) {
+ this.$emit('emoji-click', emoji);
},
},
};
</script>
-<!-- eslint-disable-next-line vue/no-deprecated-functional-template -->
-<template functional>
+<template>
<div class="gl-display-flex gl-flex-wrap gl-mb-2">
- <template v-if="props.renderGroup">
- <button
- v-for="emoji in props.emojis"
+ <template v-if="renderGroup">
+ <gl-button
+ v-for="emoji in emojis"
:key="emoji"
type="button"
- class="gl-border-0 gl-bg-transparent gl-px-0 gl-py-2 gl-text-center emoji-picker-emoji"
+ category="tertiary"
+ class="emoji-picker-emoji"
+ :aria-label="emoji"
data-testid="emoji-button"
- @click="props.clickEmoji(emoji)"
+ button-text-classes="gl-display-none!"
+ @click="clickEmoji(emoji)"
>
- <gl-emoji :data-name="emoji" />
- </button>
+ <template #emoji>
+ <gl-emoji :data-name="emoji" class="gl-mr-0!" />
+ </template>
+ </gl-button>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/emoji/components/picker.vue b/app/assets/javascripts/emoji/components/picker.vue
index 462420ba4e5..8b1784ae551 100644
--- a/app/assets/javascripts/emoji/components/picker.vue
+++ b/app/assets/javascripts/emoji/components/picker.vue
@@ -1,6 +1,6 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
-import { GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlButton, GlIcon, GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
import { findLastIndex } from 'lodash';
import VirtualList from 'vue-virtual-scroll-list';
import { getEmojiCategoryMap, state } from '~/emoji';
@@ -11,6 +11,7 @@ import { addToFrequentlyUsed, getEmojiCategories, hasFrequentlyUsedEmojis } from
export default {
components: {
+ GlButton,
GlIcon,
GlDropdown,
GlDropdownItem,
@@ -94,6 +95,11 @@ export default {
this.currentCategory = findLastIndex(Object.values(categories), ({ top }) => offset >= top);
},
+ onHide() {
+ this.currentCategory = 0;
+ this.searchValue = '';
+ this.$emit('hidden');
+ },
},
};
</script>
@@ -111,7 +117,7 @@ export default {
:right="right"
lazy
@shown="$emit('shown')"
- @hidden="$emit('hidden')"
+ @hidden="onHide"
>
<template #button-content>
<slot name="button-content">
@@ -139,19 +145,16 @@ export default {
v-show="!searchValue"
class="gl-display-flex gl-mx-5 gl-border-b-solid gl-border-gray-100 gl-border-b-1"
>
- <button
+ <gl-button
v-for="(category, index) in categoryNames"
:key="category.name"
- :class="{
- 'gl-text-body! emoji-picker-category-active': index === currentCategory,
- }"
- type="button"
- class="gl-border-0 gl-border-b-2 gl-border-b-solid gl-flex-grow-1 gl-text-gray-300 gl-pt-3 gl-pb-3 gl-bg-transparent emoji-picker-category-tab"
+ category="tertiary"
+ :class="{ 'emoji-picker-category-active': index === currentCategory }"
+ class="gl-px-3! gl-rounded-0! gl-border-b-2! gl-border-b-solid! gl-flex-grow-1 emoji-picker-category-tab"
+ :icon="category.icon"
:aria-label="category.name"
@click="scrollToCategory(category.name)"
- >
- <gl-icon :name="category.icon" />
- </button>
+ />
</div>
<emoji-list :search-value="searchValue">
<template #default="{ filteredCategories }">
diff --git a/app/assets/javascripts/emoji/index.js b/app/assets/javascripts/emoji/index.js
index f98369c2fde..c4279e9d8e7 100644
--- a/app/assets/javascripts/emoji/index.js
+++ b/app/assets/javascripts/emoji/index.js
@@ -77,6 +77,8 @@ async function loadEmojiWithNames() {
}
export async function loadCustomEmojiWithNames() {
+ const emojiData = { emojis: {}, names: [] };
+
if (document.body?.dataset?.groupFullPath && window.gon?.features?.customEmoji) {
const client = createApolloClient();
const { data } = await client.query({
@@ -86,26 +88,21 @@ export async function loadCustomEmojiWithNames() {
},
});
- return data?.group?.customEmoji?.nodes?.reduce(
- (acc, e) => {
- // Map the custom emoji into the format of the normal emojis
- acc.emojis[e.name] = {
- c: 'custom',
- d: e.name,
- e: undefined,
- name: e.name,
- src: e.url,
- u: 'custom',
- };
- acc.names.push(e.name);
-
- return acc;
- },
- { emojis: {}, names: [] },
- );
+ data?.group?.customEmoji?.nodes?.forEach((e) => {
+ // Map the custom emoji into the format of the normal emojis
+ emojiData.emojis[e.name] = {
+ c: 'custom',
+ d: e.name,
+ e: undefined,
+ name: e.name,
+ src: e.url,
+ u: 'custom',
+ };
+ emojiData.names.push(e.name);
+ });
}
- return { emojis: {}, names: [] };
+ return emojiData;
}
async function prepareEmojiMap() {
diff --git a/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql b/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
index 951027ec274..ba3911ab091 100644
--- a/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
+++ b/app/assets/javascripts/emoji/queries/custom_emoji.query.graphql
@@ -1,7 +1,7 @@
query getCustomEmoji($groupPath: ID!) {
group(fullPath: $groupPath) {
id
- customEmoji {
+ customEmoji(includeAncestorGroups: true) {
nodes {
id
name
diff --git a/app/assets/javascripts/ensure_data.js b/app/assets/javascripts/ensure_data.js
index ddb34e59144..1a4b57f0beb 100644
--- a/app/assets/javascripts/ensure_data.js
+++ b/app/assets/javascripts/ensure_data.js
@@ -1,4 +1,4 @@
-import emptySvg from '@gitlab/svgs/dist/illustrations/security-dashboard-empty-state.svg?raw';
+import emptySvg from '@gitlab/svgs/dist/illustrations/empty-state/empty-dashboard-md.svg?raw';
import { GlEmptyState } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { __ } from '~/locale';
diff --git a/app/assets/javascripts/environments/components/environment_form.vue b/app/assets/javascripts/environments/components/environment_form.vue
index c6cf6b7e24b..a4c2d4fcc51 100644
--- a/app/assets/javascripts/environments/components/environment_form.vue
+++ b/app/assets/javascripts/environments/components/environment_form.vue
@@ -7,7 +7,6 @@ import {
GlCollapsibleListbox,
GlLink,
GlSprintf,
- GlAlert,
} from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
import { isAbsolute } from '~/lib/utils/url_utility';
@@ -19,9 +18,9 @@ import {
import csrf from '~/lib/utils/csrf';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql';
import getUserAuthorizedAgents from '../graphql/queries/user_authorized_agents.query.graphql';
import EnvironmentFluxResourceSelector from './environment_flux_resource_selector.vue';
+import EnvironmentNamespaceSelector from './environment_namespace_selector.vue';
export default {
components: {
@@ -32,8 +31,8 @@ export default {
GlCollapsibleListbox,
GlLink,
GlSprintf,
- GlAlert,
EnvironmentFluxResourceSelector,
+ EnvironmentNamespaceSelector,
},
mixins: [glFeatureFlagsMixin()],
inject: {
@@ -72,8 +71,6 @@ export default {
urlFeedback: __('The URL should start with http:// or https://'),
agentLabel: s__('Environments|GitLab agent'),
agentHelpText: s__('Environments|Select agent'),
- namespaceLabel: s__('Environments|Kubernetes namespace (optional)'),
- namespaceHelpText: s__('Environments|Select namespace'),
save: __('Save'),
cancel: __('Cancel'),
reset: __('Reset'),
@@ -93,35 +90,9 @@ export default {
selectedAgentId: this.environment.clusterAgentId,
agentSearchTerm: '',
selectedNamespace: this.environment.kubernetesNamespace,
- k8sNamespaces: [],
- namespaceSearchTerm: '',
kubernetesError: '',
};
},
- apollo: {
- k8sNamespaces: {
- query: getNamespacesQuery,
- skip() {
- return !this.showNamespaceSelector;
- },
- variables() {
- return {
- configuration: this.k8sAccessConfiguration,
- };
- },
- update(data) {
- return data?.k8sNamespaces || [];
- },
- error(error) {
- this.kubernetesError = error.message;
- },
- result(result) {
- if (!result?.error && !result.errors?.length) {
- this.kubernetesError = null;
- }
- },
- },
- },
computed: {
loadingNamespacesList() {
return this.$apollo.queries.k8sNamespaces.loading;
@@ -161,26 +132,9 @@ export default {
item.text.toLowerCase().includes(lowerCasedSearchTerm),
);
},
- namespacesList() {
- return this.k8sNamespaces.map((item) => {
- return {
- value: item.metadata.name,
- text: item.metadata.name,
- };
- });
- },
- filteredNamespacesList() {
- const lowerCasedSearchTerm = this.namespaceSearchTerm.toLowerCase();
- return this.namespacesList.filter((item) =>
- item.text.toLowerCase().includes(lowerCasedSearchTerm),
- );
- },
showNamespaceSelector() {
return Boolean(this.selectedAgentId);
},
- namespaceDropdownToggleText() {
- return this.selectedNamespace || this.$options.i18n.namespaceHelpText;
- },
showFluxResourceSelector() {
return Boolean(this.selectedNamespace && this.selectedAgentId);
},
@@ -239,9 +193,6 @@ export default {
fluxResourcePath: null,
});
},
- onNamespaceSearch(search) {
- this.namespaceSearchTerm = search;
- },
},
};
</script>
@@ -334,34 +285,14 @@ export default {
/>
</gl-form-group>
- <gl-form-group
+ <environment-namespace-selector
v-if="showNamespaceSelector"
- :label="$options.i18n.namespaceLabel"
- label-for="environment_namespace"
- >
- <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5">
- {{ kubernetesError }}
- </gl-alert>
- <gl-collapsible-listbox
- v-else
- id="environment_namespace"
- v-model="selectedNamespace"
- class="gl-w-full"
- data-testid="namespace-selector"
- block
- :items="filteredNamespacesList"
- :loading="loadingNamespacesList"
- :toggle-text="namespaceDropdownToggleText"
- :header-text="$options.i18n.namespaceHelpText"
- :reset-button-label="$options.i18n.reset"
- :searchable="true"
- @search="onNamespaceSearch"
- @select="
- onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null })
- "
- @reset="onChange({ ...environment, kubernetesNamespace: null })"
- />
- </gl-form-group>
+ :namespace="selectedNamespace"
+ :configuration="k8sAccessConfiguration"
+ @change="
+ onChange({ ...environment, kubernetesNamespace: $event, fluxResourcePath: null })
+ "
+ />
<environment-flux-resource-selector
v-if="showFluxResourceSelector"
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index 47edec8dcb0..18282bfd2ce 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -12,7 +12,7 @@ import {
import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import CommitComponent from '~/vue_shared/components/commit.vue';
import TooltipOnTruncate from '~/vue_shared/components/tooltip_on_truncate/tooltip_on_truncate.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
@@ -615,7 +615,7 @@ export default {
:title="model.name"
class="environment-name table-mobile-content"
>
- <a data-qa-selector="environment_link" :href="environmentPath">
+ <a :href="environmentPath">
<span v-if="model.size === 1">{{ model.name }}</span>
<span v-else>{{ model.name_without_type }}</span>
</a>
diff --git a/app/assets/javascripts/environments/components/environment_namespace_selector.vue b/app/assets/javascripts/environments/components/environment_namespace_selector.vue
new file mode 100644
index 00000000000..101d70d36f3
--- /dev/null
+++ b/app/assets/javascripts/environments/components/environment_namespace_selector.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlFormGroup, GlCollapsibleListbox, GlAlert, GlButton, GlSprintf } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import getNamespacesQuery from '../graphql/queries/k8s_namespaces.query.graphql';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlCollapsibleListbox,
+ GlAlert,
+ GlButton,
+ GlSprintf,
+ },
+ props: {
+ configuration: {
+ required: true,
+ type: Object,
+ },
+ namespace: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
+ i18n: {
+ namespaceLabel: s__('Environments|Kubernetes namespace (optional)'),
+ namespaceHelpText: s__('Environments|Select namespace'),
+ selectButton: s__('Environments|Or select namespace: %{searchTerm}'),
+ reset: __('Reset'),
+ },
+ data() {
+ return {
+ k8sNamespaces: [],
+ searchTerm: '',
+ kubernetesError: '',
+ };
+ },
+ apollo: {
+ k8sNamespaces: {
+ query: getNamespacesQuery,
+
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sNamespaces?.map((item) => {
+ return {
+ value: item.metadata.name,
+ text: item.metadata.name,
+ };
+ }) || []
+ );
+ },
+ error(error) {
+ this.kubernetesError = error.message;
+ },
+ result(result) {
+ if (!result?.error && !result.errors?.length) {
+ this.kubernetesError = null;
+ }
+ },
+ },
+ },
+ computed: {
+ loadingNamespacesList() {
+ return this.$apollo.queries.k8sNamespaces.loading;
+ },
+ filteredNamespacesList() {
+ const lowerCasedSearchTerm = this.searchTerm.toLowerCase();
+ return this.k8sNamespaces.filter((item) =>
+ item.text.toLowerCase().includes(lowerCasedSearchTerm),
+ );
+ },
+ namespaceDropdownToggleText() {
+ return this.namespace || this.$options.i18n.namespaceHelpText;
+ },
+ shouldRenderSelectButton() {
+ const hasSearchedItem = this.k8sNamespaces.some(
+ (item) => item.text === this.searchTerm.toLowerCase(),
+ );
+ return this.searchTerm && !hasSearchedItem;
+ },
+ },
+ methods: {
+ onChange(namespace) {
+ this.$emit('change', namespace);
+ },
+ onNamespaceSearch(search) {
+ this.searchTerm = search;
+ },
+ onSelect(namespace) {
+ this.onChange(namespace);
+ this.$refs.namespaceSelector.close();
+ },
+ },
+};
+</script>
+<template>
+ <gl-form-group :label="$options.i18n.namespaceLabel" label-for="environment_namespace">
+ <gl-alert v-if="kubernetesError" variant="warning" :dismissible="false" class="gl-mb-5">
+ {{ kubernetesError }}
+ </gl-alert>
+ <gl-collapsible-listbox
+ id="environment_namespace"
+ ref="namespaceSelector"
+ :selected="namespace"
+ class="gl-w-full"
+ block
+ :items="filteredNamespacesList"
+ :loading="loadingNamespacesList"
+ :toggle-text="namespaceDropdownToggleText"
+ :header-text="$options.i18n.namespaceHelpText"
+ :reset-button-label="$options.i18n.reset"
+ :searchable="true"
+ @search="onNamespaceSearch"
+ @select="onChange"
+ @reset="onChange"
+ >
+ <template v-if="shouldRenderSelectButton" #footer>
+ <gl-button
+ category="tertiary"
+ class="gl-justify-content-start! gl-border-t-1! gl-border-t-solid gl-border-t-gray-200 gl-pl-7! gl-rounded-top-left-none! gl-rounded-top-right-none!"
+ :class="{ 'gl-mt-3': !filteredNamespacesList.length }"
+ @click="onSelect(searchTerm)"
+ >
+ <gl-sprintf :message="$options.i18n.selectButton">
+ <template #searchTerm>{{ searchTerm }}</template>
+ </gl-sprintf>
+ </gl-button>
+ </template>
+ </gl-collapsible-listbox>
+ </gl-form-group>
+</template>
diff --git a/app/assets/javascripts/environments/components/environments_app.vue b/app/assets/javascripts/environments/components/environments_app.vue
index 4e8b75536a4..8de0e0266c5 100644
--- a/app/assets/javascripts/environments/components/environments_app.vue
+++ b/app/assets/javascripts/environments/components/environments_app.vue
@@ -4,9 +4,9 @@ import { debounce } from 'lodash';
import { s__, __, sprintf } from '~/locale';
import { updateHistory, setUrlParams, queryToObject } from '~/lib/utils/url_utility';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql';
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';
@@ -56,6 +56,9 @@ export default {
},
pageInfo: {
query: pageInfoQuery,
+ variables() {
+ return { page: this.page };
+ },
},
environmentToDelete: {
query: environmentToDeleteQuery,
diff --git a/app/assets/javascripts/environments/components/kubernetes_overview.vue b/app/assets/javascripts/environments/components/kubernetes_overview.vue
index 36cce29d624..d5a7b43c953 100644
--- a/app/assets/javascripts/environments/components/kubernetes_overview.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_overview.vue
@@ -43,7 +43,7 @@ export default {
return {
isVisible: false,
error: '',
- hasFailedState: false,
+ failedState: {},
podsLoading: false,
workloadTypesLoading: false,
};
@@ -78,6 +78,9 @@ export default {
return this.hasFailedState ? 'error' : 'success';
},
+ hasFailedState() {
+ return Object.values(this.failedState).some((item) => item);
+ },
},
methods: {
toggleCollapse() {
@@ -86,6 +89,12 @@ export default {
onClusterError(message) {
this.error = message;
},
+ onUpdateFailedState(event) {
+ this.failedState = {
+ ...this.failedState,
+ ...event,
+ };
+ },
},
i18n: {
collapse: __('Collapse'),
@@ -126,14 +135,14 @@ export default {
class="gl-mb-5"
@cluster-error="onClusterError"
@loading="podsLoading = $event"
- @failed="hasFailedState = true" />
+ @update-failed-state="onUpdateFailedState" />
<kubernetes-tabs
:configuration="k8sAccessConfiguration"
:namespace="namespace"
class="gl-mb-5"
@cluster-error="onClusterError"
@loading="workloadTypesLoading = $event"
- @failed="hasFailedState = true"
+ @update-failed-state="onUpdateFailedState"
/></template>
</gl-collapse>
</div>
diff --git a/app/assets/javascripts/environments/components/kubernetes_pods.vue b/app/assets/javascripts/environments/components/kubernetes_pods.vue
index 3f040f1f40a..cd21c4d65dc 100644
--- a/app/assets/javascripts/environments/components/kubernetes_pods.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_pods.vue
@@ -1,14 +1,20 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
import { s__ } from '~/locale';
+import {
+ STATUS_RUNNING,
+ STATUS_PENDING,
+ STATUS_SUCCEEDED,
+ STATUS_FAILED,
+ STATUS_LABELS,
+} from '~/kubernetes_dashboard/constants';
+import WorkloadStats from '~/kubernetes_dashboard/components/workload_stats.vue';
import k8sPodsQuery from '../graphql/queries/k8s_pods.query.graphql';
-import { PHASE_RUNNING, PHASE_PENDING, PHASE_SUCCEEDED, PHASE_FAILED } from '../constants';
export default {
components: {
GlLoadingIcon,
- GlSingleStat,
+ WorkloadStats,
},
apollo: {
k8sPods: {
@@ -52,20 +58,20 @@ export default {
return [
{
- value: this.countPodsByPhase(PHASE_RUNNING),
- title: this.$options.i18n.runningPods,
+ value: this.countPodsByPhase(STATUS_RUNNING),
+ title: STATUS_LABELS[STATUS_RUNNING],
},
{
- value: this.countPodsByPhase(PHASE_PENDING),
- title: this.$options.i18n.pendingPods,
+ value: this.countPodsByPhase(STATUS_PENDING),
+ title: STATUS_LABELS[STATUS_PENDING],
},
{
- value: this.countPodsByPhase(PHASE_SUCCEEDED),
- title: this.$options.i18n.succeededPods,
+ value: this.countPodsByPhase(STATUS_SUCCEEDED),
+ title: STATUS_LABELS[STATUS_SUCCEEDED],
},
{
- value: this.countPodsByPhase(PHASE_FAILED),
- title: this.$options.i18n.failedPods,
+ value: this.countPodsByPhase(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
},
];
},
@@ -76,18 +82,15 @@ export default {
methods: {
countPodsByPhase(phase) {
const filteredPods = this.k8sPods.filter((item) => item.status.phase === phase);
- if (phase === PHASE_FAILED && filteredPods.length) {
- this.$emit('failed');
- }
+
+ const hasFailedState = Boolean(phase === STATUS_FAILED && filteredPods.length);
+ this.$emit('update-failed-state', { pods: hasFailedState });
+
return filteredPods.length;
},
},
i18n: {
podsTitle: s__('Environment|Pods'),
- runningPods: s__('Environment|Running'),
- pendingPods: s__('Environment|Pending'),
- succeededPods: s__('Environment|Succeeded'),
- failedPods: s__('Environment|Failed'),
},
};
</script>
@@ -96,18 +99,6 @@ export default {
<p class="gl-text-gray-500">{{ $options.i18n.podsTitle }}</p>
<gl-loading-icon v-if="loading" />
-
- <div
- v-else-if="podStats && !error"
- class="gl-display-flex gl-flex-wrap gl-sm-flex-nowrap gl-mx-n3 gl-mt-n3"
- >
- <gl-single-stat
- v-for="(stat, index) in podStats"
- :key="index"
- class="gl-w-full gl-flex-direction-column gl-align-items-center gl-justify-content-center gl-bg-white gl-border gl-border-gray-a-08 gl-mx-3 gl-p-3 gl-mt-3"
- :value="stat.value"
- :title="stat.title"
- />
- </div>
+ <workload-stats v-else-if="podStats && !error" :stats="podStats" />
</div>
</template>
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index 8ecb61711ce..20ed67f6bd9 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -153,7 +153,7 @@ export default {
},
},
i18n: {
- healthLabel: s__('Environment|Environment health'),
+ healthLabel: s__('Environment|Environment status'),
syncStatusLabel: s__('Environment|Sync status'),
},
badgeContainerClasses: 'gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-mr-3 gl-mb-2',
diff --git a/app/assets/javascripts/environments/components/kubernetes_summary.vue b/app/assets/javascripts/environments/components/kubernetes_summary.vue
index e2fbc6fd2e7..2912fd8f4d8 100644
--- a/app/assets/javascripts/environments/components/kubernetes_summary.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_summary.vue
@@ -140,9 +140,7 @@ export default {
return workloadType.items?.failed?.length > 0;
});
- if (failed) {
- this.$emit('failed');
- }
+ this.$emit('update-failed-state', { summary: failed });
},
},
i18n: {
@@ -159,6 +157,7 @@ export default {
completed: 'success',
failed: 'danger',
suspended: 'neutral',
+ pending: 'info',
},
icons: {
Active: { icon: 'status_success', class: 'gl-text-green-500' },
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 7c699eec412..0d80b1fd797 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -1,8 +1,9 @@
<script>
import { GlTabs, GlTab, GlLoadingIcon, GlBadge, GlTable, GlPagination } from '@gitlab/ui';
import { __, s__ } from '~/locale';
+import { getAge } from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
import k8sServicesQuery from '../graphql/queries/k8s_services.query.graphql';
-import { generateServicePortsString, getServiceAge } from '../helpers/k8s_integration_helper';
+import { generateServicePortsString } from '../helpers/k8s_integration_helper';
import { SERVICES_LIMIT_PER_PAGE } from '../constants';
import KubernetesSummary from './kubernetes_summary.vue';
@@ -62,7 +63,7 @@ export default {
clusterIP: service?.spec?.clusterIP,
externalIP: service?.spec?.externalIP,
ports: generateServicePortsString(service?.spec?.ports),
- age: getServiceAge(service?.metadata?.creationTimestamp),
+ age: getAge(service?.metadata?.creationTimestamp),
};
});
},
@@ -139,7 +140,7 @@ export default {
:namespace="namespace"
:configuration="configuration"
@loading="$emit('loading', $event)"
- @failed="$emit('failed')"
+ @update-failed-state="$emit('update-failed-state', $event)"
@cluster-error="$emit('cluster-error', $event)"
/>
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index e97720312b0..2fe9008c042 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -155,11 +155,6 @@ export const SYNC_STATUS_BADGES = {
export const STATUS_TRUE = 'True';
export const STATUS_FALSE = 'False';
-export const PHASE_RUNNING = 'Running';
-export const PHASE_PENDING = 'Pending';
-export const PHASE_SUCCEEDED = 'Succeeded';
-export const PHASE_FAILED = 'Failed';
-
const ERROR_UNAUTHORIZED = 'unauthorized';
const ERROR_FORBIDDEN = 'forbidden';
const ERROR_NOT_FOUND = 'not found';
@@ -167,7 +162,7 @@ const ERROR_OTHER = 'other';
export const CLUSTER_AGENT_ERROR_MESSAGES = {
[ERROR_UNAUTHORIZED]: s__(
- 'Environment|Unauthorized to access the cluster agent from this environment. Check your authentication and try again.',
+ "Environment|You don't have permission to view all the namespaces in the cluster. If a namespace is not shown, you can still enter its name to select it.",
),
[ERROR_FORBIDDEN]: s__(
'Environment|Forbidden to access the cluster agent from this environment.',
diff --git a/app/assets/javascripts/environments/folder/environments_folder_app.vue b/app/assets/javascripts/environments/folder/environments_folder_app.vue
new file mode 100644
index 00000000000..720397c3089
--- /dev/null
+++ b/app/assets/javascripts/environments/folder/environments_folder_app.vue
@@ -0,0 +1,256 @@
+<script>
+import { GlSkeletonLoader, GlTabs, GlTab, GlBadge, GlPagination } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
+import folderQuery from '../graphql/queries/folder.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 environmentToChangeCanaryQuery from '../graphql/queries/environment_to_change_canary.query.graphql';
+import EnvironmentItem from '../components/new_environment_item.vue';
+import StopEnvironmentModal from '../components/stop_environment_modal.vue';
+import ConfirmRollbackModal from '../components/confirm_rollback_modal.vue';
+import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
+import CanaryUpdateModal from '../components/canary_update_modal.vue';
+import { ENVIRONMENTS_SCOPE } from '../constants';
+
+export default {
+ components: {
+ GlPagination,
+ GlBadge,
+ GlTabs,
+ GlTab,
+ GlSkeletonLoader,
+ EnvironmentItem,
+ StopEnvironmentModal,
+ ConfirmRollbackModal,
+ DeleteEnvironmentModal,
+ CanaryUpdateModal,
+ },
+ props: {
+ folderName: {
+ type: String,
+ required: true,
+ },
+ folderPath: {
+ type: String,
+ required: true,
+ },
+ scope: {
+ type: String,
+ required: true,
+ default: ENVIRONMENTS_SCOPE.ACTIVE,
+ },
+ page: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ environmentToDelete: {},
+ environmentToRollback: {},
+ environmentToStop: {},
+ environmentToChangeCanary: {},
+ weight: 0,
+ lastRowCount: 3,
+ };
+ },
+ apollo: {
+ folder: {
+ query: folderQuery,
+ variables() {
+ return {
+ environment: this.environmentQueryData,
+ scope: this.scope,
+ search: '',
+ perPage: this.$options.perPage,
+ page: this.page,
+ };
+ },
+ pollInterval: 3000,
+ },
+ environmentToDelete: {
+ query: environmentToDeleteQuery,
+ },
+ environmentToRollback: {
+ query: environmentToRollbackQuery,
+ },
+ environmentToStop: {
+ query: environmentToStopQuery,
+ },
+ environmentToChangeCanary: {
+ query: environmentToChangeCanaryQuery,
+ },
+ weight: {
+ query: environmentToChangeCanaryQuery,
+ },
+ },
+ computed: {
+ environmentQueryData() {
+ return { folderPath: this.folderPath };
+ },
+ environments() {
+ return this.folder?.environments;
+ },
+ isLoading() {
+ return this.$apollo.queries.folder.loading;
+ },
+ activeCount() {
+ return this.folder?.activeCount ?? '-';
+ },
+ stoppedCount() {
+ return this.folder?.stoppedCount ?? '-';
+ },
+ activeTab() {
+ return this.scope === ENVIRONMENTS_SCOPE.ACTIVE ? 0 : 1;
+ },
+ totalItems() {
+ const environmentsCount =
+ this.scope === ENVIRONMENTS_SCOPE.ACTIVE
+ ? this.folder?.activeCount
+ : this.folder?.stoppedCount;
+ return Number(environmentsCount);
+ },
+ totalPages() {
+ return Math.ceil(this.totalItems / this.$options.perPage);
+ },
+ hasNextPage() {
+ return this.page !== this.totalPages;
+ },
+ hasPreviousPage() {
+ return this.page > 1;
+ },
+ pageNumber: {
+ get() {
+ return this.page;
+ },
+ set(newPageNumber) {
+ if (newPageNumber !== this.page) {
+ const query = { ...this.$route.query, page: newPageNumber };
+ this.$router.push({ query });
+ }
+ },
+ },
+ },
+ watch: {
+ environments(newEnvironments) {
+ if (newEnvironments?.length) {
+ this.lastRowCount = newEnvironments.length;
+ }
+
+ // When we load a page, if there's next and/or previous pages existing,
+ // we'll load their data as well to improve percepted performance.
+ // The page data is cached by apollo client and is immediately accessible
+ // and won't trigger additional requests
+ if (this.hasNextPage) {
+ this.$apollo.query({
+ query: folderQuery,
+ variables: {
+ environment: this.environmentQueryData,
+ scope: this.scope,
+ search: '',
+ perPage: this.$options.perPage,
+ page: this.page + 1,
+ },
+ });
+ }
+
+ if (this.hasPreviousPage) {
+ this.$apollo.query({
+ query: folderQuery,
+ variables: {
+ environment: this.environmentQueryData,
+ scope: this.scope,
+ search: '',
+ perPage: this.$options.perPage,
+ page: this.page - 1,
+ },
+ });
+ }
+ },
+ },
+ methods: {
+ setScope(scope) {
+ if (scope !== this.scope) {
+ this.$router.push({ query: { scope } });
+ }
+ },
+ },
+ i18n: {
+ pageTitle: s__('Environments|Environments'),
+ active: __('Active'),
+ stopped: __('Stopped'),
+ },
+ perPage: 20,
+ ENVIRONMENTS_SCOPE,
+};
+</script>
+<template>
+ <div>
+ <delete-environment-modal :environment="environmentToDelete" graphql />
+ <stop-environment-modal :environment="environmentToStop" graphql />
+ <confirm-rollback-modal :environment="environmentToRollback" graphql />
+ <canary-update-modal :environment="environmentToChangeCanary" :weight="weight" />
+ <h4 class="gl-font-weight-normal" data-testid="folder-name">
+ {{ $options.i18n.pageTitle }} /
+ <b>{{ folderName }}</b>
+ </h4>
+ <gl-tabs :value="activeTab" query-param-name="scope">
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.ACTIVE"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.ACTIVE)"
+ >
+ <template #title>
+ <span>{{ $options.i18n.active }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ activeCount }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ <gl-tab
+ :query-param-value="$options.ENVIRONMENTS_SCOPE.STOPPED"
+ @click="setScope($options.ENVIRONMENTS_SCOPE.STOPPED)"
+ >
+ <template #title>
+ <span>{{ $options.i18n.stopped }}</span>
+ <gl-badge size="sm" class="gl-tab-counter-badge">
+ {{ stoppedCount }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ <div v-if="isLoading">
+ <div
+ v-for="n in lastRowCount"
+ :key="`skeleton-box-${n}`"
+ class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-py-5 gl-md-pl-7"
+ >
+ <gl-skeleton-loader :lines="2" />
+ </div>
+ </div>
+ <div v-else>
+ <!--
+ We assign each element's key as index intentionally here.
+ Creation and destruction of "environments-item" component is quite taxing and leads
+ to noticeable blocking rendering times for lists of more than 10 items.
+ By assigning indexes we avoid destroying and re-creating the components when page changes,
+ thus getting a much better performance.
+ Correct component state is ensured by deep data-binding of "environment" prop
+ -->
+ <environment-item
+ v-for="(environment, index) in environments"
+ :id="environment.name"
+ :key="index"
+ :environment="environment"
+ class="gl-border-gray-100 gl-border-t-solid gl-border-1 gl-pt-3"
+ in-folder
+ />
+ </div>
+ <gl-pagination
+ v-model="pageNumber"
+ :per-page="$options.perPage"
+ :total-items="totalItems"
+ align="center"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 1a32de30de0..0201fb53f77 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -1,31 +1,78 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
-import EnvironmentsFolderApp from './environments_folder_view.vue';
+import { apolloProvider } from '../graphql/client';
+import EnvironmentsFolderView from './environments_folder_view.vue';
+import EnvironmentsFolderApp from './environments_folder_app.vue';
Vue.use(Translate);
Vue.use(VueApollo);
-const apolloProvider = new VueApollo({
+const legacyApolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
});
export default () => {
const el = document.getElementById('environments-folder-list-view');
+ const environmentsData = el.dataset;
+ if (gon.features.environmentsFolderNewLook) {
+ Vue.use(VueRouter);
+
+ const folderName = environmentsData.environmentsDataFolderName;
+ const folderPath = environmentsData.environmentsDataEndpoint.replace('.json', '');
+ const projectPath = environmentsData.environmentsDataProjectPath;
+ const helpPagePath = environmentsData.environmentsDataHelpPagePath;
+
+ const router = new VueRouter({
+ mode: 'history',
+ base: window.location.pathname,
+ routes: [
+ {
+ path: '/',
+ name: 'environments_folder',
+ component: EnvironmentsFolderApp,
+ props: (route) => ({
+ scope: route.query.scope,
+ page: Number(route.query.page || '1'),
+ folderName,
+ folderPath,
+ }),
+ },
+ ],
+ scrollBehavior(to, from, savedPosition) {
+ if (savedPosition) {
+ return savedPosition;
+ }
+ return { top: 0 };
+ },
+ });
+
+ return new Vue({
+ el,
+ provide: {
+ projectPath,
+ helpPagePath,
+ },
+ apolloProvider,
+ router,
+ render(createElement) {
+ return createElement('router-view');
+ },
+ });
+ }
return new Vue({
el,
components: {
- EnvironmentsFolderApp,
+ EnvironmentsFolderView,
},
- apolloProvider,
+ apolloProvider: legacyApolloProvider,
provide: {
projectPath: el.dataset.projectPath,
},
data() {
- const environmentsData = el.dataset;
-
return {
endpoint: environmentsData.environmentsDataEndpoint,
folderName: environmentsData.environmentsDataFolderName,
@@ -33,7 +80,7 @@ export default () => {
};
},
render(createElement) {
- return createElement('environments-folder-app', {
+ return createElement('environments-folder-view', {
props: {
endpoint: this.endpoint,
folderName: this.folderName,
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index adb14ce3d6f..35a754c757b 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -4,6 +4,7 @@ import DeleteEnvironmentModal from '../components/delete_environment_modal.vue';
import StopEnvironmentModal from '../components/stop_environment_modal.vue';
import environmentsMixin from '../mixins/environments_mixin';
import EnvironmentsPaginationApiMixin from '../mixins/environments_pagination_api_mixin';
+import ConfirmRollbackModal from '../components/confirm_rollback_modal.vue';
export default {
components: {
@@ -12,6 +13,7 @@ export default {
GlTab,
GlTabs,
StopEnvironmentModal,
+ ConfirmRollbackModal,
},
mixins: [environmentsMixin, EnvironmentsPaginationApiMixin],
@@ -42,6 +44,7 @@ export default {
<div :class="cssContainerClass">
<stop-environment-modal :environment="environmentInStopModal" />
<delete-environment-modal :environment="environmentInDeleteModal" />
+ <confirm-rollback-modal :environment="environmentInRollbackModal" />
<h4 class="gl-font-weight-normal" data-testid="folder-name">
{{ s__('Environments|Environments') }} /
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 8f57069d89d..0eb12427914 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -1,7 +1,7 @@
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql';
import environmentApp from './queries/environment_app.query.graphql';
-import pageInfoQuery from './queries/page_info.query.graphql';
import environmentToDeleteQuery from './queries/environment_to_delete.query.graphql';
import environmentToRollbackQuery from './queries/environment_to_rollback.query.graphql';
import environmentToStopQuery from './queries/environment_to_stop.query.graphql';
diff --git a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
index ac6a68e450c..d183f27a8b6 100644
--- a/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/folder.query.graphql
@@ -1,5 +1,12 @@
-query getEnvironmentFolder($environment: NestedLocalEnvironment, $scope: String, $search: String) {
- folder(environment: $environment, scope: $scope, search: $search) @client {
+query getEnvironmentFolder(
+ $environment: NestedLocalEnvironment
+ $scope: String
+ $search: String
+ $perPage: Int
+ $page: Int
+) {
+ folder(environment: $environment, scope: $scope, search: $search, perPage: $perPage, page: $page)
+ @client {
activeCount
environments
stoppedCount
diff --git a/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql b/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql
deleted file mode 100644
index d77ca05d46f..00000000000
--- a/app/assets/javascripts/environments/graphql/queries/page_info.query.graphql
+++ /dev/null
@@ -1,8 +0,0 @@
-query getPageInfo {
- pageInfo @client {
- total
- perPage
- nextPage
- previousPage
- }
-}
diff --git a/app/assets/javascripts/environments/graphql/resolvers/base.js b/app/assets/javascripts/environments/graphql/resolvers/base.js
index 4427b8ff2ef..7d2a0689da2 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/base.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/base.js
@@ -6,13 +6,13 @@ import {
normalizeHeaders,
} from '~/lib/utils/common_utils';
+import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql';
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 environmentToChangeCanaryQuery from '../queries/environment_to_change_canary.query.graphql';
import isEnvironmentStoppingQuery from '../queries/is_environment_stopping.query.graphql';
-import pageInfoQuery from '../queries/page_info.query.graphql';
const buildErrors = (errors = []) => ({
errors,
@@ -59,13 +59,18 @@ export const baseQueries = (endpoint) => ({
};
});
},
- folder(_, { environment: { folderPath }, scope, search }) {
- return axios.get(folderPath, { params: { scope, search, per_page: 3 } }).then((res) => ({
- activeCount: res.data.active_count,
- environments: res.data.environments.map(mapEnvironment),
- stoppedCount: res.data.stopped_count,
- __typename: 'LocalEnvironmentFolder',
- }));
+ folder(_, { environment: { folderPath }, scope, search, perPage, page }) {
+ // eslint-disable-next-line camelcase
+ const per_page = perPage || 3;
+ const pageNumber = page || 1;
+ return axios
+ .get(folderPath, { params: { scope, search, per_page, page: pageNumber } })
+ .then((res) => ({
+ activeCount: res.data.active_count,
+ environments: res.data.environments.map(mapEnvironment),
+ stoppedCount: res.data.stopped_count,
+ __typename: 'LocalEnvironmentFolder',
+ }));
},
isLastDeployment(_, { environment }) {
return environment?.lastDeployment?.isLast;
@@ -73,7 +78,7 @@ export const baseQueries = (endpoint) => ({
});
export const baseMutations = {
- stopEnvironmentREST(_, { environment }, { client }) {
+ stopEnvironmentREST(_, { environment }, { client, cache }) {
client.writeQuery({
query: isEnvironmentStoppingQuery,
variables: { environment },
@@ -82,6 +87,9 @@ export const baseMutations = {
return axios
.post(environment.stopPath)
.then(() => buildErrors())
+ .then(() => {
+ cache.evict({ fieldName: 'folder' });
+ })
.catch(() => {
client.writeQuery({
query: isEnvironmentStoppingQuery,
@@ -93,10 +101,11 @@ export const baseMutations = {
]);
});
},
- deleteEnvironment(_, { environment: { deletePath } }) {
+ deleteEnvironment(_, { environment: { deletePath } }, { cache }) {
return axios
.delete(deletePath)
.then(() => buildErrors())
+ .then(() => cache.evict({ fieldName: 'folder' }))
.catch(() =>
buildErrors([
s__(
@@ -105,10 +114,13 @@ export const baseMutations = {
]),
);
},
- rollbackEnvironment(_, { environment, isLastDeployment }) {
+ rollbackEnvironment(_, { environment, isLastDeployment }, { cache }) {
return axios
.post(environment?.retryUrl)
.then(() => buildErrors())
+ .then(() => {
+ cache.evict({ fieldName: 'folder' });
+ })
.catch(() => {
buildErrors([
isLastDeployment
diff --git a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
index 8375b8793d9..eab25298c36 100644
--- a/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/environments/graphql/resolvers/kubernetes.js
@@ -6,8 +6,16 @@ import {
WatchApi,
EVENT_DATA,
} from '@gitlab/cluster-client';
+import produce from 'immer';
+import {
+ getK8sPods,
+ handleClusterError,
+ buildWatchPath,
+} from '~/kubernetes_dashboard/graphql/helpers/resolver_helpers';
import { humanizeClusterErrors } from '../../helpers/k8s_integration_helper';
import k8sPodsQuery from '../queries/k8s_pods.query.graphql';
+import k8sWorkloadsQuery from '../queries/k8s_workloads.query.graphql';
+import k8sServicesQuery from '../queries/k8s_services.query.graphql';
const mapWorkloadItems = (items, kind) => {
return items.map((item) => {
@@ -52,17 +60,10 @@ const mapWorkloadItems = (items, kind) => {
});
};
-const handleClusterError = async (err) => {
- if (!err.response) {
- throw err;
- }
-
- const errorData = await err.response.json();
- throw errorData;
-};
+const watchWorkloadItems = ({ kind, apiVersion, configuration, namespace, client }) => {
+ const itemKind = kind.toLowerCase().replace('list', 's');
-const watchPods = ({ configuration, namespace, client }) => {
- const path = namespace ? `/api/v1/namespaces/${namespace}/pods` : '/api/v1/pods';
+ const path = buildWatchPath({ resource: itemKind, api: `apis/${apiVersion}`, namespace });
const config = new Configuration(configuration);
const watcherApi = new WatchApi(config);
@@ -72,14 +73,21 @@ const watchPods = ({ configuration, namespace, client }) => {
let result = [];
watcher.on(EVENT_DATA, (data) => {
- result = data.map((item) => {
- return { status: { phase: item.status.phase } };
+ result = mapWorkloadItems(data, kind);
+
+ const sourceData = client.readQuery({
+ query: k8sWorkloadsQuery,
+ variables: { configuration, namespace },
+ });
+
+ const updatedData = produce(sourceData, (draft) => {
+ draft.k8sWorkloads[kind] = result;
});
client.writeQuery({
- query: k8sPodsQuery,
+ query: k8sWorkloadsQuery,
variables: { configuration, namespace },
- data: { k8sPods: result },
+ data: updatedData,
});
});
})
@@ -88,32 +96,53 @@ const watchPods = ({ configuration, namespace, client }) => {
});
};
-export default {
- k8sPods(_, { configuration, namespace }, { client }) {
- const config = new Configuration(configuration);
+const mapServicesItems = (items) => {
+ return items.map((item) => {
+ const { type, clusterIP, externalIP, ports } = item.spec;
+ return {
+ metadata: item.metadata,
+ spec: {
+ type,
+ clusterIP: clusterIP || '-',
+ externalIP: externalIP || '-',
+ ports,
+ },
+ };
+ });
+};
- const coreV1Api = new CoreV1Api(config);
- const podsApi = namespace
- ? coreV1Api.listCoreV1NamespacedPod({ namespace })
- : coreV1Api.listCoreV1PodForAllNamespaces();
+const watchServices = ({ configuration, namespace, client }) => {
+ const path = buildWatchPath({ resource: 'services', namespace });
+ const config = new Configuration(configuration);
+ const watcherApi = new WatchApi(config);
- return podsApi
- .then((res) => {
- if (gon.features?.k8sWatchApi) {
- watchPods({ configuration, namespace, client });
- }
+ watcherApi
+ .subscribeToStream(path, { watch: true })
+ .then((watcher) => {
+ let result = [];
- return res?.items || [];
- })
- .catch(async (err) => {
- try {
- await handleClusterError(err);
- } catch (error) {
- throw new Error(error.message);
- }
+ watcher.on(EVENT_DATA, (data) => {
+ result = mapServicesItems(data);
+
+ client.writeQuery({
+ query: k8sServicesQuery,
+ variables: { configuration, namespace },
+ data: { k8sServices: result },
+ });
});
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+export default {
+ k8sPods(_, { configuration, namespace }, { client }) {
+ const query = k8sPodsQuery;
+ const enableWatch = gon.features?.k8sWatchApi;
+ return getK8sPods({ client, query, configuration, namespace, enableWatch });
},
- k8sServices(_, { configuration, namespace }) {
+ k8sServices(_, { configuration, namespace }, { client }) {
const coreV1Api = new CoreV1Api(new Configuration(configuration));
const servicesApi = namespace
? coreV1Api.listCoreV1NamespacedService({ namespace })
@@ -122,18 +151,12 @@ export default {
return servicesApi
.then((res) => {
const items = res?.items || [];
- return items.map((item) => {
- const { type, clusterIP, externalIP, ports } = item.spec;
- return {
- metadata: item.metadata,
- spec: {
- type,
- clusterIP: clusterIP || '-',
- externalIP: externalIP || '-',
- ports,
- },
- };
- });
+
+ if (gon.features?.k8sWatchApi) {
+ watchServices({ configuration, namespace, client });
+ }
+
+ return mapServicesItems(items);
})
.catch(async (err) => {
try {
@@ -143,7 +166,7 @@ export default {
}
});
},
- k8sWorkloads(_, { configuration, namespace }) {
+ k8sWorkloads(_, { configuration, namespace }, { client }) {
const appsV1api = new AppsV1Api(new Configuration(configuration));
const batchV1api = new BatchV1Api(new Configuration(configuration));
@@ -189,10 +212,12 @@ export default {
}
for (const promiseResult of results) {
if (promiseResult.status === 'fulfilled' && promiseResult?.value) {
- const { kind, items } = promiseResult.value;
+ const { kind, items, apiVersion } = promiseResult.value;
if (items?.length > 0) {
summaryList[kind] = mapWorkloadItems(items, kind);
+
+ watchWorkloadItems({ kind, apiVersion, configuration, namespace, client });
}
}
}
diff --git a/app/assets/javascripts/environments/graphql/typedefs.graphql b/app/assets/javascripts/environments/graphql/typedefs.graphql
index 41f165ad1da..a235e387930 100644
--- a/app/assets/javascripts/environments/graphql/typedefs.graphql
+++ b/app/assets/javascripts/environments/graphql/typedefs.graphql
@@ -1,3 +1,5 @@
+#import "~/graphql_shared/client/page_info.typedefs.graphql"
+
type LocalEnvironment {
id: Int!
globalId: ID!
@@ -55,19 +57,19 @@ type LocalErrors {
errors: [String!]!
}
-type LocalPageInfo {
- total: Int!
- perPage: Int!
- nextPage: Int!
- previousPage: Int!
-}
-
type k8sPodStatus {
phase: String
}
+type k8sPodMetadata {
+ name: String
+ namespace: String
+ creationTimestamp: String
+}
+
type LocalK8sPods {
status: k8sPodStatus
+ metadata: k8sPodMetadata
}
input LocalConfiguration {
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index 164a2d98e90..bb5cab7c279 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -1,4 +1,9 @@
-import { differenceInSeconds } from '~/lib/utils/datetime_utility';
+import {
+ calculateDeploymentStatus,
+ calculateStatefulSetStatus,
+ calculateDaemonSetStatus,
+} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
+import { STATUS_READY, STATUS_FAILED } from '~/kubernetes_dashboard/constants';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants';
export function generateServicePortsString(ports) {
@@ -12,46 +17,29 @@ export function generateServicePortsString(ports) {
.join(', ');
}
-export function getServiceAge(creationTimestamp) {
- if (!creationTimestamp) return '';
-
- const timeDifference = differenceInSeconds(new Date(creationTimestamp), new Date());
-
- const seconds = Math.floor(timeDifference);
- const minutes = Math.floor(seconds / 60) % 60;
- const hours = Math.floor(seconds / 60 / 60) % 24;
- const days = Math.floor(seconds / 60 / 60 / 24);
-
- let ageString;
- if (days > 0) {
- ageString = `${days}d`;
- } else if (hours > 0) {
- ageString = `${hours}h`;
- } else if (minutes > 0) {
- ageString = `${minutes}m`;
- } else {
- ageString = `${seconds}s`;
- }
-
- return ageString;
-}
-
export function getDeploymentsStatuses(items) {
const failed = [];
const ready = [];
+ const pending = [];
items.forEach((item) => {
- const [available, progressing] = item.status?.conditions ?? [];
- // eslint-disable-next-line @gitlab/require-i18n-strings
- if (available.status === 'True') {
- ready.push(item);
- // eslint-disable-next-line @gitlab/require-i18n-strings
- } else if (available.status !== 'True' && progressing.status !== 'True') {
- failed.push(item);
+ const status = calculateDeploymentStatus(item);
+
+ switch (status) {
+ case STATUS_READY:
+ ready.push(item);
+ break;
+ case STATUS_FAILED:
+ failed.push(item);
+ break;
+ default:
+ pending.push(item);
+ break;
}
});
return {
+ ...(pending.length && { pending }),
...(failed.length && { failed }),
...(ready.length && { ready }),
};
@@ -59,16 +47,10 @@ export function getDeploymentsStatuses(items) {
export function getDaemonSetStatuses(items) {
const failed = items.filter((item) => {
- return (
- item.status?.numberMisscheduled > 0 ||
- item.status?.numberReady !== item.status?.desiredNumberScheduled
- );
+ return calculateDaemonSetStatus(item) === STATUS_FAILED;
});
const ready = items.filter((item) => {
- return (
- item.status?.numberReady === item.status?.desiredNumberScheduled &&
- !item.status?.numberMisscheduled
- );
+ return calculateDaemonSetStatus(item) === STATUS_READY;
});
return {
@@ -79,10 +61,10 @@ export function getDaemonSetStatuses(items) {
export function getStatefulSetStatuses(items) {
const failed = items.filter((item) => {
- return item.status?.readyReplicas < item.spec?.replicas;
+ return calculateStatefulSetStatus(item) === STATUS_FAILED;
});
const ready = items.filter((item) => {
- return item.status?.readyReplicas === item.spec?.replicas;
+ return calculateStatefulSetStatus(item) === STATUS_READY;
});
return {
@@ -93,10 +75,10 @@ export function getStatefulSetStatuses(items) {
export function getReplicaSetStatuses(items) {
const failed = items.filter((item) => {
- return item.status?.readyReplicas < item.spec?.replicas;
+ return calculateStatefulSetStatus(item) === STATUS_FAILED;
});
const ready = items.filter((item) => {
- return item.status?.readyReplicas === item.spec?.replicas;
+ return calculateStatefulSetStatus(item) === STATUS_READY;
});
return {
diff --git a/app/assets/javascripts/error_tracking/components/error_details.vue b/app/assets/javascripts/error_tracking/components/error_details.vue
index 01879a092ed..0fabe1779a8 100644
--- a/app/assets/javascripts/error_tracking/components/error_details.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details.vue
@@ -169,16 +169,10 @@ export default {
{
text: this.ignoreBtnLabel,
action: this.onIgnoreStatusUpdate,
- extraAttrs: {
- 'data-qa-selector': 'update_ignore_status_button',
- },
},
{
text: this.resolveBtnLabel,
action: this.onResolveStatusUpdate,
- extraAttrs: {
- 'data-qa-selector': 'update_resolve_status_button',
- },
},
];
},
@@ -187,7 +181,7 @@ export default {
text: __('View issue'),
href: this.error.gitlabIssuePath,
extraAttrs: {
- 'data-qa-selector': 'view_issue_button',
+ 'data-testid': 'view-issue-button',
},
};
},
@@ -342,7 +336,7 @@ export default {
<gl-button
v-if="error.gitlabIssuePath"
class="gl-ml-3"
- data-testid="view_issue_button"
+ data-testid="view-issue-button"
:href="error.gitlabIssuePath"
variant="confirm"
>
diff --git a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
index 4d4bae12570..95ae5e5a92c 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -22,7 +22,7 @@ import { helpPagePath } from '~/helpers/help_page_helper';
import AccessorUtils from '~/lib/utils/accessor';
import { __ } from '~/locale';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
-import { sanitizeUrl } from '~/lib/utils/url_utility';
+import { sanitizeUrl, joinPaths } from '~/lib/utils/url_utility';
import {
trackErrorListViewsOptions,
trackErrorStatusUpdateOptions,
@@ -225,7 +225,7 @@ export default {
if (!isValidErrorId(errorId)) {
return 'about:blank';
}
- return `error_tracking/${errorId}/details`;
+ return joinPaths(this.listPath, errorId, 'details');
},
goToNextPage() {
this.pageValue = this.$options.NEXT_PAGE;
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags.vue b/app/assets/javascripts/feature_flags/components/feature_flags.vue
index e0a5e92564e..0cc136e79a5 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags.vue
@@ -50,12 +50,13 @@ export default {
'instanceId',
'isRotating',
'hasRotateError',
+ 'rotateEndpoint',
]),
topAreaBaseClasses() {
return ['gl-display-flex', 'gl-flex-direction-column'];
},
canUserRotateToken() {
- return this.rotateInstanceIdPath !== '';
+ return this.rotateEndpoint !== '';
},
shouldRenderPagination() {
return (
diff --git a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
index 257c482cf1d..b952e0059bb 100644
--- a/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
+++ b/app/assets/javascripts/feature_flags/components/feature_flags_table.vue
@@ -67,7 +67,7 @@ export default {
return featureFlag.iid ? `^${featureFlag.iid}` : '';
},
canDeleteFlag(flag) {
- return !this.permissions || (flag.scopes || []).every((scope) => scope.can_update);
+ return (flag.scopes || []).every((scope) => scope.can_update);
},
setDeleteModalData(featureFlag) {
this.deleteFeatureFlagUrl = featureFlag.destroy_path;
diff --git a/app/assets/javascripts/feature_highlight/constants.js b/app/assets/javascripts/feature_highlight/constants.js
deleted file mode 100644
index 3e4cd11f7d5..00000000000
--- a/app/assets/javascripts/feature_highlight/constants.js
+++ /dev/null
@@ -1 +0,0 @@
-export const POPOVER_TARGET_ID = 'feature-highlight-trigger';
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js b/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
deleted file mode 100644
index e2218c1ba2e..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_helper.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { __ } from '~/locale';
-
-export const getSelector = (highlightId) => `.js-feature-highlight[data-highlight=${highlightId}]`;
-
-export function dismiss(endpoint, highlightId) {
- return axios
- .post(endpoint, {
- feature_name: highlightId,
- })
- .catch(() =>
- createAlert({
- message: __(
- 'An error occurred while dismissing the feature highlight. Refresh the page and try dismissing again.',
- ),
- }),
- );
-}
diff --git a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue b/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
deleted file mode 100644
index 24f7d567ea7..00000000000
--- a/app/assets/javascripts/feature_highlight/feature_highlight_popover.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
-import clusterPopover from '@gitlab/svgs/dist/illustrations/cluster_popover.svg?raw';
-import { GlPopover, GlSprintf, GlLink, GlButton } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import { __ } from '~/locale';
-import { POPOVER_TARGET_ID } from './constants';
-import { dismiss } from './feature_highlight_helper';
-
-export default {
- components: {
- GlPopover,
- GlSprintf,
- GlLink,
- GlButton,
- },
- directives: {
- SafeHtml,
- },
- props: {
- autoDevopsHelpPath: {
- type: String,
- required: true,
- },
- highlightId: {
- type: String,
- required: true,
- },
- dismissEndpoint: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- dismissed: false,
- triggerHidden: false,
- };
- },
- methods: {
- dismiss() {
- dismiss(this.dismissEndpoint, this.highlightId);
- this.$refs.popover.$emit('close');
- this.dismissed = true;
- },
- hideTrigger() {
- if (this.dismissed) {
- this.triggerHidden = true;
- }
- },
- },
- clusterPopover,
- targetId: POPOVER_TARGET_ID,
- i18n: {
- highlightMessage: __('Allows you to add and manage Kubernetes clusters.'),
- autoDevopsProTipMessage: __(
- 'Protip: %{linkStart}Auto DevOps%{linkEnd} uses Kubernetes clusters to deploy your code!',
- ),
- dismissButtonLabel: __('Got it!'),
- },
-};
-</script>
-<template>
- <div class="gl-ml-3">
- <span v-if="!triggerHidden" :id="$options.targetId" class="feature-highlight"></span>
- <gl-popover
- ref="popover"
- :target="$options.targetId"
- :css-classes="['feature-highlight-popover']"
- container="body"
- placement="right"
- boundary="viewport"
- @hidden="hideTrigger"
- >
- <span
- v-safe-html="$options.clusterPopover"
- class="feature-highlight-illustration gl-display-flex gl-justify-content-center gl-py-4 gl-w-full"
- ></span>
- <div class="gl-px-4 gl-py-5">
- <p>
- {{ $options.i18n.highlightMessage }}
- </p>
- <p>
- <gl-sprintf :message="$options.i18n.autoDevopsProTipMessage">
- <template #link="{ content }">
- <gl-link class="gl-font-sm" :href="autoDevopsHelpPath">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- <gl-button size="small" icon="thumb-up" variant="confirm" @click="dismiss">
- {{ $options.i18n.dismissButtonLabel }}
- </gl-button>
- </div>
- </gl-popover>
- </div>
-</template>
diff --git a/app/assets/javascripts/feature_highlight/index.js b/app/assets/javascripts/feature_highlight/index.js
deleted file mode 100644
index 3a8b211b3c5..00000000000
--- a/app/assets/javascripts/feature_highlight/index.js
+++ /dev/null
@@ -1,28 +0,0 @@
-import Vue from 'vue';
-
-const init = async () => {
- const el = document.querySelector('.js-feature-highlight');
-
- if (!el) {
- return null;
- }
-
- const { autoDevopsHelpPath, highlight: highlightId, dismissEndpoint } = el.dataset;
- const { default: FeatureHighlight } = await import(
- /* webpackChunkName: 'feature_highlight' */ './feature_highlight_popover.vue'
- );
-
- return new Vue({
- el,
- render: (h) =>
- h(FeatureHighlight, {
- props: {
- autoDevopsHelpPath,
- highlightId,
- dismissEndpoint,
- },
- }),
- });
-};
-
-export default init;
diff --git a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
index 74d91734630..698302c5209 100644
--- a/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
+++ b/app/assets/javascripts/filtered_search/add_extra_tokens_for_merge_requests.js
@@ -8,7 +8,10 @@ import {
TOKEN_TYPE_SOURCE_BRANCH,
} from '~/vue_shared/components/filtered_search_bar/constants';
-export default (IssuableTokenKeys, disableBranchFilter = false) => {
+export default (
+ IssuableTokenKeys,
+ { disableBranchFilter = false, disableEnvironmentFilter = false } = {},
+) => {
const reviewerToken = {
formattedKey: TOKEN_TITLE_REVIEWER,
key: TOKEN_TYPE_REVIEWER,
@@ -171,41 +174,43 @@ export default (IssuableTokenKeys, disableBranchFilter = false) => {
);
IssuableTokenKeys.conditions.push(...approvedBy.condition);
- const environmentToken = {
- formattedKey: __('Environment'),
- key: 'environment',
- type: 'string',
- param: '',
- symbol: '',
- icon: 'cloud-gear',
- tag: 'environment',
- };
+ if (!disableEnvironmentFilter) {
+ const environmentToken = {
+ formattedKey: __('Environment'),
+ key: 'environment',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'cloud-gear',
+ tag: 'environment',
+ };
- const deployedBeforeToken = {
- formattedKey: __('Deployed-before'),
- key: 'deployed-before',
- type: 'string',
- param: '',
- symbol: '',
- icon: 'clock',
- tag: 'deployed_before',
- };
+ const deployedBeforeToken = {
+ formattedKey: __('Deployed-before'),
+ key: 'deployed-before',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'clock',
+ tag: 'deployed_before',
+ };
- const deployedAfterToken = {
- formattedKey: __('Deployed-after'),
- key: 'deployed-after',
- type: 'string',
- param: '',
- symbol: '',
- icon: 'clock',
- tag: 'deployed_after',
- };
+ const deployedAfterToken = {
+ formattedKey: __('Deployed-after'),
+ key: 'deployed-after',
+ type: 'string',
+ param: '',
+ symbol: '',
+ icon: 'clock',
+ tag: 'deployed_after',
+ };
- IssuableTokenKeys.tokenKeys.push(environmentToken, deployedBeforeToken, deployedAfterToken);
+ IssuableTokenKeys.tokenKeys.push(environmentToken, deployedBeforeToken, deployedAfterToken);
- IssuableTokenKeys.tokenKeysWithAlternative.push(
- environmentToken,
- deployedBeforeToken,
- deployedAfterToken,
- );
+ IssuableTokenKeys.tokenKeysWithAlternative.push(
+ environmentToken,
+ deployedBeforeToken,
+ deployedAfterToken,
+ );
+ }
};
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index 8ccf7ba92a5..d00c98adc0d 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -1,6 +1,6 @@
import { last } from 'lodash';
import recentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { createFilteredSearchTokenKeys } from '~/filtered_search/issuable_filtered_search_token_keys';
import { createAlert } from '~/alert';
import {
STATUS_ALL,
@@ -40,7 +40,7 @@ export default class FilteredSearchManager {
isGroupAncestor = true,
isGroupDecendent = false,
useDefaultState = false,
- filteredSearchTokenKeys = IssuableFilteredSearchTokenKeys,
+ filteredSearchTokenKeys = createFilteredSearchTokenKeys(),
stateFiltersSelector = '.issues-state-filters',
placeholder = __('Search or filter results…'),
anchor = null,
diff --git a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
index 8aa99ec52f9..5a785de2e66 100644
--- a/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
+++ b/app/assets/javascripts/filtered_search/issuable_filtered_search_token_keys.js
@@ -17,66 +17,75 @@ import {
} from '~/vue_shared/components/filtered_search_bar/constants';
import FilteredSearchTokenKeys from './filtered_search_token_keys';
-export const tokenKeys = [
- {
- formattedKey: TOKEN_TITLE_AUTHOR,
- key: TOKEN_TYPE_AUTHOR,
- type: 'string',
- param: 'username',
- symbol: '@',
- icon: 'pencil',
- tag: '@author',
- },
- {
- formattedKey: TOKEN_TITLE_ASSIGNEE,
- key: TOKEN_TYPE_ASSIGNEE,
- type: 'string',
- param: 'username',
- symbol: '@',
- icon: 'user',
- tag: '@assignee',
- },
- {
- formattedKey: TOKEN_TITLE_MILESTONE,
- key: TOKEN_TYPE_MILESTONE,
- type: 'string',
- param: 'title',
- symbol: '%',
- icon: 'clock',
- tag: '%milestone',
- },
- {
- formattedKey: TOKEN_TITLE_RELEASE,
- key: TOKEN_TYPE_RELEASE,
- type: 'string',
- param: 'tag',
- symbol: '',
- icon: 'rocket',
- tag: __('tag name'),
- },
- {
- formattedKey: TOKEN_TITLE_LABEL,
- key: TOKEN_TYPE_LABEL,
- type: 'array',
- param: 'name[]',
- symbol: '~',
- icon: 'labels',
- tag: '~label',
- },
-];
+export const createTokenKeys = ({ disableReleaseFilter = false } = {}) => {
+ const tokenKeys = [
+ {
+ formattedKey: TOKEN_TITLE_AUTHOR,
+ key: TOKEN_TYPE_AUTHOR,
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'pencil',
+ tag: '@author',
+ },
+ {
+ formattedKey: TOKEN_TITLE_ASSIGNEE,
+ key: TOKEN_TYPE_ASSIGNEE,
+ type: 'string',
+ param: 'username',
+ symbol: '@',
+ icon: 'user',
+ tag: '@assignee',
+ },
+ {
+ formattedKey: TOKEN_TITLE_MILESTONE,
+ key: TOKEN_TYPE_MILESTONE,
+ type: 'string',
+ param: 'title',
+ symbol: '%',
+ icon: 'clock',
+ tag: '%milestone',
+ },
+ {
+ formattedKey: TOKEN_TITLE_LABEL,
+ key: TOKEN_TYPE_LABEL,
+ type: 'array',
+ param: 'name[]',
+ symbol: '~',
+ icon: 'labels',
+ tag: '~label',
+ },
+ ];
-if (gon.current_user_id) {
- // Appending tokenkeys only logged-in
- tokenKeys.push({
- formattedKey: TOKEN_TITLE_MY_REACTION,
- key: TOKEN_TYPE_MY_REACTION,
- type: 'string',
- param: 'emoji',
- symbol: '',
- icon: 'thumb-up',
- tag: 'emoji',
- });
-}
+ if (!disableReleaseFilter) {
+ tokenKeys.push({
+ formattedKey: TOKEN_TITLE_RELEASE,
+ key: TOKEN_TYPE_RELEASE,
+ type: 'string',
+ param: 'tag',
+ symbol: '',
+ icon: 'rocket',
+ tag: __('tag name'),
+ });
+ }
+
+ if (gon.current_user_id) {
+ // Appending tokenkeys only logged-in
+ tokenKeys.push({
+ formattedKey: TOKEN_TITLE_MY_REACTION,
+ key: TOKEN_TYPE_MY_REACTION,
+ type: 'string',
+ param: 'emoji',
+ symbol: '',
+ icon: 'thumb-up',
+ tag: 'emoji',
+ });
+ }
+
+ return tokenKeys;
+};
+
+export const tokenKeys = createTokenKeys();
export const alternativeTokenKeys = [
{
@@ -186,10 +195,7 @@ export const conditions = flattenDeep(
}),
);
-const IssuableFilteredSearchTokenKeys = new FilteredSearchTokenKeys(
- tokenKeys,
- alternativeTokenKeys,
- conditions,
-);
+export const createFilteredSearchTokenKeys = (config = {}) =>
+ new FilteredSearchTokenKeys(createTokenKeys(config), alternativeTokenKeys, conditions);
-export default IssuableFilteredSearchTokenKeys;
+export default createFilteredSearchTokenKeys();
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
deleted file mode 100644
index 0fb70fb831e..00000000000
--- a/app/assets/javascripts/fly_out_nav.js
+++ /dev/null
@@ -1,205 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
-
-const HIDE_INTERVAL_TIMEOUT = 300;
-const COLLAPSED_PANEL_WIDTH = 48;
-const IS_OVER_CLASS = 'is-over';
-const IS_ABOVE_CLASS = 'is-above';
-const IS_SHOWING_FLY_OUT_CLASS = 'is-showing-fly-out';
-let currentOpenMenu = null;
-let menuCornerLocs;
-let timeoutId;
-let sidebar;
-
-export const mousePos = [];
-
-export const setSidebar = (el) => {
- sidebar = el;
-};
-export const getOpenMenu = () => currentOpenMenu;
-export const setOpenMenu = (menu = null) => {
- currentOpenMenu = menu;
-};
-
-export const slope = (a, b) => (b.y - a.y) / (b.x - a.x);
-
-export const getHeaderHeight = () => sidebar?.offsetTop || 0;
-
-export const isSidebarCollapsed = () =>
- sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS);
-
-export const canShowActiveSubItems = (el) => {
- if (el.classList.contains('active') && !isSidebarCollapsed()) {
- return false;
- }
-
- return true;
-};
-
-export const canShowSubItems = () => ['md', 'lg', 'xl'].includes(bp.getBreakpointSize());
-
-export const getHideSubItemsInterval = () => {
- if (!currentOpenMenu || !mousePos.length) return 0;
-
- const currentMousePos = mousePos[mousePos.length - 1];
- const prevMousePos = mousePos[0];
- const currentMousePosY = currentMousePos.y;
- const [menuTop, menuBottom] = menuCornerLocs;
-
- if (currentMousePosY < menuTop.y || currentMousePosY > menuBottom.y) return 0;
-
- if (
- slope(prevMousePos, menuBottom) < slope(currentMousePos, menuBottom) &&
- slope(prevMousePos, menuTop) > slope(currentMousePos, menuTop)
- ) {
- return HIDE_INTERVAL_TIMEOUT;
- }
-
- return 0;
-};
-
-export const calculateTop = (boundingRect, outerHeight) => {
- const windowHeight = window.innerHeight;
- const bottomOverflow = windowHeight - (boundingRect.top + outerHeight);
-
- return bottomOverflow < 0
- ? boundingRect.top - outerHeight + boundingRect.height
- : boundingRect.top;
-};
-
-export const hideMenu = (el) => {
- if (!el) return;
-
- const parentEl = el.parentNode;
-
- el.style.display = '';
- el.style.transform = '';
- el.classList.remove(IS_ABOVE_CLASS);
- el.classList.remove('fly-out-list');
- parentEl.classList.remove(IS_OVER_CLASS);
- parentEl.classList.remove(IS_SHOWING_FLY_OUT_CLASS);
-
- setOpenMenu();
-};
-
-export const moveSubItemsToPosition = (el, subItems) => {
- const hasSubItems = subItems.parentNode.querySelector('.has-sub-items');
- const header = subItems.querySelector('.fly-out-top-item');
- const boundingRect = el.getBoundingClientRect();
- const left = sidebar ? sidebar.offsetWidth : COLLAPSED_PANEL_WIDTH;
- let top = calculateTop(boundingRect, subItems.offsetHeight);
- const isAbove = top < boundingRect.top;
- if (hasSubItems) {
- top = isAbove ? top : top - header.offsetHeight;
- } else {
- top = boundingRect.top;
- }
-
- subItems.classList.add('fly-out-list');
- subItems.style.transform = `translate3d(${left}px, ${Math.floor(top) - getHeaderHeight()}px, 0)`; // eslint-disable-line no-param-reassign
- const subItemsRect = subItems.getBoundingClientRect();
-
- menuCornerLocs = [
- {
- x: subItemsRect.left, // left position of the sub items
- y: subItemsRect.top, // top position of the sub items
- },
- {
- x: subItemsRect.left, // left position of the sub items
- y: subItemsRect.top + subItemsRect.height, // bottom position of the sub items
- },
- ];
-
- if (isAbove) {
- subItems.classList.add(IS_ABOVE_CLASS);
- }
-};
-
-export const showSubLevelItems = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
- const isIconOnly = subItems && subItems.classList.contains('is-fly-out-only');
-
- if (!canShowSubItems() || !canShowActiveSubItems(el)) return;
-
- el.classList.add(IS_OVER_CLASS);
-
- if (!subItems || (!isSidebarCollapsed() && isIconOnly)) return;
-
- subItems.style.display = 'block';
- el.classList.add(IS_SHOWING_FLY_OUT_CLASS);
-
- setOpenMenu(subItems);
- moveSubItemsToPosition(el, subItems);
-};
-
-export const mouseEnterTopItems = (el, timeout = getHideSubItemsInterval()) => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
-
- showSubLevelItems(el);
- }, timeout);
-};
-
-export const mouseLeaveTopItem = (el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
-
- if (
- !canShowSubItems() ||
- !canShowActiveSubItems(el) ||
- (subItems && subItems === currentOpenMenu)
- )
- return;
-
- el.classList.remove(IS_OVER_CLASS);
-};
-
-export const documentMouseMove = (e) => {
- mousePos.push({
- x: e.clientX,
- y: e.clientY,
- });
-
- if (mousePos.length > 6) mousePos.shift();
-};
-
-export const subItemsMouseLeave = (relatedTarget) => {
- clearTimeout(timeoutId);
-
- if (relatedTarget && !relatedTarget.closest(`.${IS_OVER_CLASS}`)) {
- hideMenu(currentOpenMenu);
- }
-};
-
-export default () => {
- sidebar = document.querySelector('.nav-sidebar');
-
- if (!sidebar) return;
-
- const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
-
- const topItems = sidebar.querySelector('.sidebar-top-level-items');
- if (topItems) {
- sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
- }, getHideSubItemsInterval());
- });
- }
-
- items.forEach((el) => {
- const subItems = el.querySelector('.sidebar-sub-level-items');
-
- if (subItems) {
- subItems.addEventListener('mouseleave', (e) => subItemsMouseLeave(e.relatedTarget));
- }
-
- el.addEventListener('mouseenter', (e) => mouseEnterTopItems(e.currentTarget));
- el.addEventListener('mouseleave', (e) => mouseLeaveTopItem(e.currentTarget));
- });
-
- document.addEventListener('mousemove', documentMouseMove);
-};
diff --git a/app/assets/javascripts/forks/components/forks_button.vue b/app/assets/javascripts/forks/components/forks_button.vue
index 40cf74ff4cc..fbea64d568b 100644
--- a/app/assets/javascripts/forks/components/forks_button.vue
+++ b/app/assets/javascripts/forks/components/forks_button.vue
@@ -77,7 +77,7 @@ export default {
:href="forkButtonUrl"
icon="fork"
:title="forkButtonTooltip"
- >{{ s__('ProjectOverview|Forks') }}</gl-button
+ >{{ s__('ProjectOverview|Fork') }}</gl-button
>
<gl-button data-testid="forks-count" :disabled="!canReadCode" :href="projectForksUrl">{{
forksCount
diff --git a/app/assets/javascripts/frequent_items/components/app.vue b/app/assets/javascripts/frequent_items/components/app.vue
deleted file mode 100644
index 947d3053094..00000000000
--- a/app/assets/javascripts/frequent_items/components/app.vue
+++ /dev/null
@@ -1,183 +0,0 @@
-<script>
-import { GlLoadingIcon, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
-import AccessorUtilities from '~/lib/utils/accessor';
-import {
- mapVuexModuleState,
- mapVuexModuleActions,
- mapVuexModuleGetters,
-} from '~/lib/utils/vuex_module_mappers';
-import Tracking from '~/tracking';
-import { FREQUENT_ITEMS, STORAGE_KEY } from '../constants';
-import eventHub from '../event_hub';
-import { isMobile, updateExistingFrequentItem, sanitizeItem } from '../utils';
-import FrequentItemsList from './frequent_items_list.vue';
-import frequentItemsMixin from './frequent_items_mixin';
-import FrequentItemsSearchInput from './frequent_items_search_input.vue';
-
-const trackingMixin = Tracking.mixin();
-
-export default {
- components: {
- FrequentItemsSearchInput,
- FrequentItemsList,
- GlLoadingIcon,
- GlButton,
- GlIcon,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- mixins: [frequentItemsMixin, trackingMixin],
- inject: ['vuexModule'],
- props: {
- currentUserName: {
- type: String,
- required: true,
- },
- currentItem: {
- type: Object,
- required: true,
- },
- searchClass: {
- type: String,
- required: false,
- default: '',
- },
- },
- computed: {
- ...mapVuexModuleState((vm) => vm.vuexModule, [
- 'searchQuery',
- 'isLoadingItems',
- 'isItemsListEditable',
- 'isFetchFailed',
- 'isItemRemovalFailed',
- 'items',
- ]),
- ...mapVuexModuleGetters((vm) => vm.vuexModule, ['hasSearchQuery']),
- translations() {
- return this.getTranslations(['loadingMessage', 'header', 'headerEditToggle']);
- },
- },
- created() {
- const { namespace, currentUserName, currentItem } = this;
- const storageKey = `${currentUserName}/${STORAGE_KEY[namespace]}`;
-
- this.setNamespace(namespace);
- this.setStorageKey(storageKey);
-
- if (currentItem.id) {
- this.logItemAccess(storageKey, currentItem);
- }
-
- eventHub.$on(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
-
- // As we init it through requestIdleCallback it could be that the dropdown is already open
- const namespaceDropdown = document.getElementById(`nav-${this.namespace}-dropdown`);
- if (namespaceDropdown && namespaceDropdown.classList.contains('show')) {
- this.dropdownOpenHandler();
- }
- },
- beforeDestroy() {
- eventHub.$off(`${this.namespace}-dropdownOpen`, this.dropdownOpenHandler);
- },
- methods: {
- ...mapVuexModuleActions((vm) => vm.vuexModule, [
- 'setNamespace',
- 'setStorageKey',
- 'toggleItemsListEditablity',
- 'fetchFrequentItems',
- ]),
- toggleItemsListEditablityTracked() {
- this.track('click_button', {
- label: 'toggle_edit_frequent_items',
- property: 'navigation_top',
- });
- this.toggleItemsListEditablity();
- },
- dropdownOpenHandler() {
- if (this.searchQuery === '' || isMobile()) {
- this.fetchFrequentItems();
- }
- },
- logItemAccess(storageKey, unsanitizedItem) {
- const item = sanitizeItem(unsanitizedItem);
-
- if (!AccessorUtilities.canUseLocalStorage()) {
- return false;
- }
-
- // Check if there's any frequent items list set
- const storedRawItems = localStorage.getItem(storageKey);
- const storedFrequentItems = storedRawItems
- ? JSON.parse(storedRawItems)
- : [{ ...item, frequency: 1 }]; // No frequent items list set, set one up.
-
- // Check if item already exists in list
- const itemMatchIndex = storedFrequentItems.findIndex(
- (frequentItem) => frequentItem.id === item.id,
- );
-
- if (itemMatchIndex > -1) {
- storedFrequentItems[itemMatchIndex] = updateExistingFrequentItem(
- storedFrequentItems[itemMatchIndex],
- item,
- );
- } else {
- if (storedFrequentItems.length === FREQUENT_ITEMS.MAX_COUNT) {
- storedFrequentItems.shift();
- }
-
- storedFrequentItems.push({ ...item, frequency: 1 });
- }
-
- return localStorage.setItem(storageKey, JSON.stringify(storedFrequentItems));
- },
- },
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-flex-direction-column gl-flex-align-items-stretch gl-h-full">
- <frequent-items-search-input
- :namespace="namespace"
- :class="searchClass"
- data-testid="frequent-items-search-input"
- />
- <gl-loading-icon
- v-if="isLoadingItems"
- :label="translations.loadingMessage"
- size="lg"
- class="loading-animation prepend-top-20"
- data-testid="loading"
- />
- <div
- v-if="!isLoadingItems && !hasSearchQuery"
- class="section-header gl-display-flex"
- data-testid="header"
- >
- <span class="gl-flex-grow-1">{{ translations.header }}</span>
- <gl-button
- v-if="items.length"
- v-gl-tooltip.left
- size="small"
- category="tertiary"
- :aria-label="translations.headerEditToggle"
- :title="translations.headerEditToggle"
- :class="{ 'gl-bg-gray-100!': isItemsListEditable }"
- class="gl-p-2!"
- @click="toggleItemsListEditablityTracked"
- >
- <gl-icon name="pencil" :class="{ 'gl-text-gray-900!': isItemsListEditable }" />
- </gl-button>
- </div>
- <frequent-items-list
- v-if="!isLoadingItems"
- :items="items"
- :namespace="namespace"
- :has-search-query="hasSearchQuery"
- :is-fetch-failed="isFetchFailed"
- :is-item-removal-failed="isItemRemovalFailed"
- :matcher="searchQuery"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
deleted file mode 100644
index da1d3bedaf4..00000000000
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list.vue
+++ /dev/null
@@ -1,90 +0,0 @@
-<script>
-import { sanitizeItem } from '../utils';
-import FrequentItemsListItem from './frequent_items_list_item.vue';
-import frequentItemsMixin from './frequent_items_mixin';
-
-export default {
- components: {
- FrequentItemsListItem,
- },
- mixins: [frequentItemsMixin],
- props: {
- items: {
- type: Array,
- required: true,
- },
- hasSearchQuery: {
- type: Boolean,
- required: true,
- },
- isFetchFailed: {
- type: Boolean,
- required: true,
- },
- isItemRemovalFailed: {
- type: Boolean,
- required: true,
- },
- matcher: {
- type: String,
- required: true,
- },
- },
- computed: {
- translations() {
- return this.getTranslations([
- 'itemListEmptyMessage',
- 'itemListErrorMessage',
- 'searchListEmptyMessage',
- 'searchListErrorMessage',
- ]);
- },
- isListEmpty() {
- return this.items.length === 0;
- },
- showListEmptyMessage() {
- return this.isListEmpty || this.isItemRemovalFailed;
- },
- listEmptyMessage() {
- if (this.hasSearchQuery) {
- return this.isFetchFailed
- ? this.translations.searchListErrorMessage
- : this.translations.searchListEmptyMessage;
- }
-
- return this.isFetchFailed || this.isItemRemovalFailed
- ? this.translations.itemListErrorMessage
- : this.translations.itemListEmptyMessage;
- },
- sanitizedItems() {
- return this.items.map(sanitizeItem);
- },
- },
-};
-</script>
-
-<template>
- <div class="frequent-items-list-container">
- <ul data-testid="frequent-items-list" class="list-unstyled">
- <li
- v-if="showListEmptyMessage"
- :class="{ 'section-failure': isFetchFailed }"
- class="section-empty gl-mb-3"
- data-testid="frequent-items-list-empty"
- >
- {{ listEmptyMessage }}
- </li>
- <frequent-items-list-item
- v-for="item in sanitizedItems"
- v-else
- :key="item.id"
- :item-id="item.id"
- :item-name="item.name"
- :namespace="item.namespace"
- :web-url="item.webUrl"
- :avatar-url="item.avatarUrl"
- :matcher="matcher"
- />
- </ul>
- </div>
-</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue b/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
deleted file mode 100644
index 056dedf8757..00000000000
--- a/app/assets/javascripts/frequent_items/components/frequent_items_list_item.vue
+++ /dev/null
@@ -1,132 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import highlight from '~/lib/utils/highlight';
-import { truncateNamespace } from '~/lib/utils/text_utility';
-import { mapVuexModuleState, mapVuexModuleActions } from '~/lib/utils/vuex_module_mappers';
-import Tracking from '~/tracking';
-import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-
-const trackingMixin = Tracking.mixin();
-
-export default {
- components: {
- GlIcon,
- GlButton,
- ProjectAvatar,
- },
- directives: {
- SafeHtml,
- GlTooltip: GlTooltipDirective,
- },
- mixins: [trackingMixin],
- inject: ['vuexModule'],
- props: {
- matcher: {
- type: String,
- required: false,
- default: '',
- },
- itemId: {
- type: Number,
- required: true,
- },
- itemName: {
- type: String,
- required: true,
- },
- namespace: {
- type: String,
- required: false,
- default: '',
- },
- webUrl: {
- type: String,
- required: true,
- },
- avatarUrl: {
- required: true,
- validator(value) {
- return value === null || typeof value === 'string';
- },
- },
- },
- computed: {
- ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType', 'isItemsListEditable']),
- truncatedNamespace() {
- return truncateNamespace(this.namespace);
- },
- highlightedItemName() {
- return highlight(this.itemName, this.matcher);
- },
- itemTrackingLabel() {
- return `${this.dropdownType}_dropdown_frequent_items_list_item`;
- },
- },
- methods: {
- removeFrequentItemTracked(item) {
- this.track('click_button', {
- label: `${this.dropdownType}_dropdown_remove_frequent_item`,
- property: 'navigation_top',
- });
- this.removeFrequentItem(item);
- },
- ...mapVuexModuleActions((vm) => vm.vuexModule, ['removeFrequentItem']),
- },
-};
-</script>
-
-<template>
- <li class="frequent-items-list-item-container gl-relative">
- <gl-button
- category="tertiary"
- :href="webUrl"
- class="gl-text-left gl-w-full"
- button-text-classes="gl-display-flex gl-w-full"
- data-testid="frequent-item-link"
- @click="track('click_link', { label: itemTrackingLabel, property: 'navigation_top' })"
- >
- <div class="gl-flex-grow-1">
- <project-avatar
- class="gl-float-left gl-mr-3"
- :project-avatar-url="avatarUrl"
- :project-id="itemId"
- :project-name="itemName"
- aria-hidden="true"
- />
- <div
- data-testid="frequent-items-item-metadata-container"
- class="frequent-items-item-metadata-container"
- >
- <div
- v-safe-html="highlightedItemName"
- data-testid="frequent-items-item-title"
- :title="itemName"
- class="frequent-items-item-title"
- ></div>
- <div
- v-if="namespace"
- data-testid="frequent-items-item-namespace"
- :title="namespace"
- class="frequent-items-item-namespace"
- >
- {{ truncatedNamespace }}
- </div>
- </div>
- </div>
- </gl-button>
- <gl-button
- v-if="isItemsListEditable"
- v-gl-tooltip.left
- size="small"
- category="tertiary"
- :aria-label="__('Remove')"
- :title="__('Remove')"
- class="gl-align-self-center gl-p-1! gl-absolute! gl-w-auto! gl-right-4 gl-top-half gl-translate-y-n50"
- data-testid="item-remove"
- @click.stop.prevent="removeFrequentItemTracked(itemId)"
- >
- <gl-icon name="close" />
- </gl-button>
- </li>
-</template>
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js b/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
deleted file mode 100644
index 704dc83ca8e..00000000000
--- a/app/assets/javascripts/frequent_items/components/frequent_items_mixin.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { TRANSLATION_KEYS } from '../constants';
-
-export default {
- props: {
- namespace: {
- type: String,
- required: true,
- },
- },
- methods: {
- getTranslations(keys) {
- const translationStrings = keys.reduce(
- (acc, key) => ({
- ...acc,
- [key]: TRANSLATION_KEYS[this.namespace][key],
- }),
- {},
- );
-
- return translationStrings;
- },
- },
-};
diff --git a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue b/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
deleted file mode 100644
index 023245f050b..00000000000
--- a/app/assets/javascripts/frequent_items/components/frequent_items_search_input.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { GlSearchBoxByType } from '@gitlab/ui';
-import { debounce } from 'lodash';
-import { mapVuexModuleActions, mapVuexModuleState } from '~/lib/utils/vuex_module_mappers';
-import Tracking from '~/tracking';
-import frequentItemsMixin from './frequent_items_mixin';
-
-const trackingMixin = Tracking.mixin();
-
-export default {
- components: {
- GlSearchBoxByType,
- },
- mixins: [frequentItemsMixin, trackingMixin],
- inject: ['vuexModule'],
- data() {
- return {
- searchQuery: '',
- };
- },
- computed: {
- ...mapVuexModuleState((vm) => vm.vuexModule, ['dropdownType']),
- translations() {
- return this.getTranslations(['searchInputPlaceholder']);
- },
- },
- watch: {
- searchQuery: debounce(function debounceSearchQuery() {
- this.track('type_search_query', {
- label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
- property: 'navigation_top',
- });
- this.setSearchQuery(this.searchQuery);
- }, 500),
- },
- methods: {
- ...mapVuexModuleActions((vm) => vm.vuexModule, ['setSearchQuery']),
- trackFocus() {
- this.track('focus_input', {
- label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
- property: 'navigation_top',
- });
- },
- trackBlur() {
- this.track('blur_input', {
- label: `${this.dropdownType}_dropdown_frequent_items_search_input`,
- property: 'navigation_top',
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="search-input-container">
- <gl-search-box-by-type
- v-model="searchQuery"
- :placeholder="translations.searchInputPlaceholder"
- @focus="trackFocus"
- @blur="trackBlur"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/frequent_items/constants.js b/app/assets/javascripts/frequent_items/constants.js
deleted file mode 100644
index a7c27abf58e..00000000000
--- a/app/assets/javascripts/frequent_items/constants.js
+++ /dev/null
@@ -1,54 +0,0 @@
-import { s__ } from '~/locale';
-
-export const FREQUENT_ITEMS = {
- MAX_COUNT: 20,
- LIST_COUNT_DESKTOP: 5,
- LIST_COUNT_MOBILE: 3,
- ELIGIBLE_FREQUENCY: 3,
-};
-
-export const FIFTEEN_MINUTES_IN_MS = 900000;
-
-export const STORAGE_KEY = {
- projects: 'frequent-projects',
- groups: 'frequent-groups',
-};
-
-export const TRANSLATION_KEYS = {
- projects: {
- loadingMessage: s__('ProjectsDropdown|Loading projects'),
- header: s__('ProjectsDropdown|Frequently visited'),
- headerEditToggle: s__('ProjectsDropdown|Toggle edit mode'),
- itemListErrorMessage: s__(
- 'ProjectsDropdown|This feature requires browser localStorage support',
- ),
- itemListEmptyMessage: s__('ProjectsDropdown|Projects you visit often will appear here'),
- searchListErrorMessage: s__('ProjectsDropdown|Something went wrong on our end.'),
- searchListEmptyMessage: s__('ProjectsDropdown|Sorry, no projects matched your search'),
- searchInputPlaceholder: s__('ProjectsDropdown|Search your projects'),
- },
- groups: {
- loadingMessage: s__('GroupsDropdown|Loading groups'),
- header: s__('GroupsDropdown|Frequently visited'),
- headerEditToggle: s__('GroupsDropdown|Toggle edit mode'),
- itemListErrorMessage: s__('GroupsDropdown|This feature requires browser localStorage support'),
- itemListEmptyMessage: s__('GroupsDropdown|Groups you visit often will appear here'),
- searchListErrorMessage: s__('GroupsDropdown|Something went wrong on our end.'),
- searchListEmptyMessage: s__('GroupsDropdown|Sorry, no groups matched your search'),
- searchInputPlaceholder: s__('GroupsDropdown|Search your groups'),
- },
-};
-
-export const FREQUENT_ITEMS_PROJECTS = {
- namespace: 'projects',
- key: 'project',
- vuexModule: 'frequentProjects',
-};
-
-export const FREQUENT_ITEMS_GROUPS = {
- namespace: 'groups',
- key: 'group',
- vuexModule: 'frequentGroups',
-};
-
-export const FREQUENT_ITEMS_DROPDOWNS = [FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS];
diff --git a/app/assets/javascripts/frequent_items/event_hub.js b/app/assets/javascripts/frequent_items/event_hub.js
deleted file mode 100644
index e31806ad199..00000000000
--- a/app/assets/javascripts/frequent_items/event_hub.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import createEventHub from '~/helpers/event_hub_factory';
-
-export default createEventHub();
diff --git a/app/assets/javascripts/frequent_items/store/actions.js b/app/assets/javascripts/frequent_items/store/actions.js
deleted file mode 100644
index e5ef49ec402..00000000000
--- a/app/assets/javascripts/frequent_items/store/actions.js
+++ /dev/null
@@ -1,112 +0,0 @@
-import AccessorUtilities from '~/lib/utils/accessor';
-import { isLoggedIn } from '~/lib/utils/common_utils';
-import { getGroups, getProjects } from '~/rest_api';
-import { getTopFrequentItems } from '../utils';
-import * as types from './mutation_types';
-
-export const setNamespace = ({ commit }, namespace) => {
- commit(types.SET_NAMESPACE, namespace);
-};
-
-export const setStorageKey = ({ commit }, key) => {
- commit(types.SET_STORAGE_KEY, key);
-};
-
-export const toggleItemsListEditablity = ({ commit }) => {
- commit(types.TOGGLE_ITEMS_LIST_EDITABILITY);
-};
-
-export const requestFrequentItems = ({ commit }) => {
- commit(types.REQUEST_FREQUENT_ITEMS);
-};
-export const receiveFrequentItemsSuccess = ({ commit }, data) => {
- commit(types.RECEIVE_FREQUENT_ITEMS_SUCCESS, data);
-};
-export const receiveFrequentItemsError = ({ commit }) => {
- commit(types.RECEIVE_FREQUENT_ITEMS_ERROR);
-};
-
-export const fetchFrequentItems = ({ state, dispatch }) => {
- dispatch('requestFrequentItems');
-
- if (AccessorUtilities.canUseLocalStorage()) {
- const storedFrequentItems = JSON.parse(localStorage.getItem(state.storageKey));
-
- dispatch(
- 'receiveFrequentItemsSuccess',
- !storedFrequentItems ? [] : getTopFrequentItems(storedFrequentItems),
- );
- } else {
- dispatch('receiveFrequentItemsError');
- }
-};
-
-export const requestSearchedItems = ({ commit }) => {
- commit(types.REQUEST_SEARCHED_ITEMS);
-};
-export const receiveSearchedItemsSuccess = ({ commit }, data) => {
- commit(types.RECEIVE_SEARCHED_ITEMS_SUCCESS, data);
-};
-export const receiveSearchedItemsError = ({ commit }) => {
- commit(types.RECEIVE_SEARCHED_ITEMS_ERROR);
-};
-export const fetchSearchedItems = ({ state, dispatch }, searchQuery) => {
- dispatch('requestSearchedItems');
-
- const params = {
- simple: true,
- per_page: 20,
- membership: isLoggedIn(),
- };
-
- let searchFunction;
- if (state.namespace === 'projects') {
- searchFunction = getProjects;
- params.order_by = 'last_activity_at';
- } else {
- searchFunction = getGroups;
- }
-
- return searchFunction(searchQuery, params)
- .then((results) => {
- dispatch('receiveSearchedItemsSuccess', results);
- })
- .catch(() => {
- dispatch('receiveSearchedItemsError');
- });
-};
-
-export const setSearchQuery = ({ commit, dispatch }, query) => {
- commit(types.SET_SEARCH_QUERY, query);
-
- if (query) {
- dispatch('fetchSearchedItems', query);
- } else {
- dispatch('fetchFrequentItems');
- }
-};
-
-export const removeFrequentItemSuccess = ({ commit }, itemId) => {
- commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS, itemId);
-};
-
-export const removeFrequentItemError = ({ commit }) => {
- commit(types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR);
-};
-
-export const removeFrequentItem = ({ state, dispatch }, itemId) => {
- if (AccessorUtilities.canUseLocalStorage()) {
- try {
- const storedRawItems = JSON.parse(localStorage.getItem(state.storageKey));
- localStorage.setItem(
- state.storageKey,
- JSON.stringify(storedRawItems.filter((item) => item.id !== itemId)),
- );
- dispatch('removeFrequentItemSuccess', itemId);
- } catch {
- dispatch('removeFrequentItemError');
- }
- } else {
- dispatch('removeFrequentItemError');
- }
-};
diff --git a/app/assets/javascripts/frequent_items/store/getters.js b/app/assets/javascripts/frequent_items/store/getters.js
deleted file mode 100644
index e52678dbec2..00000000000
--- a/app/assets/javascripts/frequent_items/store/getters.js
+++ /dev/null
@@ -1 +0,0 @@
-export const hasSearchQuery = (state) => state.searchQuery !== '';
diff --git a/app/assets/javascripts/frequent_items/store/index.js b/app/assets/javascripts/frequent_items/store/index.js
deleted file mode 100644
index 3e5c9618805..00000000000
--- a/app/assets/javascripts/frequent_items/store/index.js
+++ /dev/null
@@ -1,29 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import { FREQUENT_ITEMS_DROPDOWNS } from '../constants';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import state from './state';
-
-export const createFrequentItemsModule = (initState = {}) => ({
- namespaced: true,
- actions,
- getters,
- mutations,
- state: state(initState),
-});
-
-export const createStoreOptions = () => ({
- modules: FREQUENT_ITEMS_DROPDOWNS.reduce(
- (acc, { namespace, vuexModule }) =>
- Object.assign(acc, {
- [vuexModule]: createFrequentItemsModule({ dropdownType: namespace }),
- }),
- {},
- ),
-});
-
-export const createStore = () => {
- return new Vuex.Store(createStoreOptions());
-};
diff --git a/app/assets/javascripts/frequent_items/store/mutation_types.js b/app/assets/javascripts/frequent_items/store/mutation_types.js
deleted file mode 100644
index 9c9346081e9..00000000000
--- a/app/assets/javascripts/frequent_items/store/mutation_types.js
+++ /dev/null
@@ -1,12 +0,0 @@
-export const SET_NAMESPACE = 'SET_NAMESPACE';
-export const SET_STORAGE_KEY = 'SET_STORAGE_KEY';
-export const SET_SEARCH_QUERY = 'SET_SEARCH_QUERY';
-export const TOGGLE_ITEMS_LIST_EDITABILITY = 'TOGGLE_ITEMS_LIST_EDITABILITY';
-export const REQUEST_FREQUENT_ITEMS = 'REQUEST_FREQUENT_ITEMS';
-export const RECEIVE_FREQUENT_ITEMS_SUCCESS = 'RECEIVE_FREQUENT_ITEMS_SUCCESS';
-export const RECEIVE_FREQUENT_ITEMS_ERROR = 'RECEIVE_FREQUENT_ITEMS_ERROR';
-export const REQUEST_SEARCHED_ITEMS = 'REQUEST_SEARCHED_ITEMS';
-export const RECEIVE_SEARCHED_ITEMS_SUCCESS = 'RECEIVE_SEARCHED_ITEMS_SUCCESS';
-export const RECEIVE_SEARCHED_ITEMS_ERROR = 'RECEIVE_SEARCHED_ITEMS_ERROR';
-export const RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS = 'RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS';
-export const RECEIVE_REMOVE_FREQUENT_ITEM_ERROR = 'RECEIVE_REMOVE_FREQUENT_ITEM_ERROR';
diff --git a/app/assets/javascripts/frequent_items/store/mutations.js b/app/assets/javascripts/frequent_items/store/mutations.js
deleted file mode 100644
index 9882bef444a..00000000000
--- a/app/assets/javascripts/frequent_items/store/mutations.js
+++ /dev/null
@@ -1,88 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.SET_NAMESPACE](state, namespace) {
- Object.assign(state, {
- namespace,
- });
- },
- [types.SET_STORAGE_KEY](state, storageKey) {
- Object.assign(state, {
- storageKey,
- });
- },
- [types.SET_SEARCH_QUERY](state, searchQuery) {
- const hasSearchQuery = searchQuery !== '';
-
- Object.assign(state, {
- searchQuery,
- isLoadingItems: true,
- hasSearchQuery,
- });
- },
- [types.TOGGLE_ITEMS_LIST_EDITABILITY](state) {
- Object.assign(state, {
- isItemsListEditable: !state.isItemsListEditable,
- });
- },
- [types.REQUEST_FREQUENT_ITEMS](state) {
- Object.assign(state, {
- isLoadingItems: true,
- hasSearchQuery: false,
- });
- },
- [types.RECEIVE_FREQUENT_ITEMS_SUCCESS](state, rawItems) {
- Object.assign(state, {
- items: rawItems,
- isLoadingItems: false,
- hasSearchQuery: false,
- isFetchFailed: false,
- });
- },
- [types.RECEIVE_FREQUENT_ITEMS_ERROR](state) {
- Object.assign(state, {
- isLoadingItems: false,
- hasSearchQuery: false,
- isFetchFailed: true,
- });
- },
- [types.REQUEST_SEARCHED_ITEMS](state) {
- Object.assign(state, {
- isLoadingItems: true,
- hasSearchQuery: true,
- });
- },
- [types.RECEIVE_SEARCHED_ITEMS_SUCCESS](state, results) {
- const rawItems = results.data;
- Object.assign(state, {
- items: rawItems.map((rawItem) => ({
- id: rawItem.id,
- name: rawItem.name,
- namespace: rawItem.name_with_namespace || rawItem.full_name,
- webUrl: rawItem.web_url,
- avatarUrl: rawItem.avatar_url,
- })),
- isLoadingItems: false,
- hasSearchQuery: true,
- isFetchFailed: false,
- });
- },
- [types.RECEIVE_SEARCHED_ITEMS_ERROR](state) {
- Object.assign(state, {
- isLoadingItems: false,
- hasSearchQuery: true,
- isFetchFailed: true,
- });
- },
- [types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS](state, itemId) {
- Object.assign(state, {
- items: state.items.filter((item) => item.id !== itemId),
- isItemRemovalFailed: false,
- });
- },
- [types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR](state) {
- Object.assign(state, {
- isItemRemovalFailed: true,
- });
- },
-};
diff --git a/app/assets/javascripts/frequent_items/store/state.js b/app/assets/javascripts/frequent_items/store/state.js
deleted file mode 100644
index ee94e9cd221..00000000000
--- a/app/assets/javascripts/frequent_items/store/state.js
+++ /dev/null
@@ -1,11 +0,0 @@
-export default ({ dropdownType = '' } = {}) => ({
- namespace: '',
- dropdownType,
- storageKey: '',
- searchQuery: '',
- isLoadingItems: false,
- isFetchFailed: false,
- isItemsListEditable: false,
- isItemRemovalFailed: false,
- items: [],
-});
diff --git a/app/assets/javascripts/frequent_items/utils.js b/app/assets/javascripts/frequent_items/utils.js
deleted file mode 100644
index f71405a5bc4..00000000000
--- a/app/assets/javascripts/frequent_items/utils.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-import { take } from 'lodash';
-import { sanitize } from '~/lib/dompurify';
-import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from './constants';
-
-export const isMobile = () => ['md', 'sm', 'xs'].includes(bp.getBreakpointSize());
-
-export const getTopFrequentItems = (items) => {
- if (!items) {
- return [];
- }
- const frequentItemsCount = isMobile()
- ? FREQUENT_ITEMS.LIST_COUNT_MOBILE
- : FREQUENT_ITEMS.LIST_COUNT_DESKTOP;
-
- const frequentItems = items.filter((item) => item.frequency >= FREQUENT_ITEMS.ELIGIBLE_FREQUENCY);
-
- if (!frequentItems || frequentItems.length === 0) {
- return [];
- }
-
- frequentItems.sort((itemA, itemB) => {
- // Sort all frequent items in decending order of frequency
- // and then by lastAccessedOn with recent most first
- if (itemA.frequency !== itemB.frequency) {
- return itemB.frequency - itemA.frequency;
- }
- if (itemA.lastAccessedOn !== itemB.lastAccessedOn) {
- return itemB.lastAccessedOn - itemA.lastAccessedOn;
- }
-
- return 0;
- });
-
- return take(frequentItems, frequentItemsCount);
-};
-
-export const updateExistingFrequentItem = (frequentItem, item) => {
- // `frequentItem` comes from localStorage and it's possible it doesn't have a `lastAccessedOn`
- const neverAccessed = !frequentItem.lastAccessedOn;
- const shouldUpdate =
- neverAccessed ||
- Math.abs(item.lastAccessedOn - frequentItem.lastAccessedOn) / FIFTEEN_MINUTES_IN_MS > 1;
-
- return {
- ...item,
- frequency: shouldUpdate ? frequentItem.frequency + 1 : frequentItem.frequency,
- lastAccessedOn: shouldUpdate ? Date.now() : frequentItem.lastAccessedOn,
- };
-};
-
-export const sanitizeItem = (item) => {
- // Only sanitize if the key exists on the item
- const maybeSanitize = (key) => {
- if (!Object.prototype.hasOwnProperty.call(item, key)) {
- return {};
- }
-
- return { [key]: sanitize(item[key].toString(), { ALLOWED_TAGS: [] }) };
- };
-
- return {
- ...item,
- ...maybeSanitize('name'),
- ...maybeSanitize('namespace'),
- };
-};
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index 39a8b1d0a9c..b11f7b1ba76 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -11,7 +11,7 @@ import { state } from '~/sidebar/components/reviewers/sidebar_reviewers.vue';
import AjaxCache from './lib/utils/ajax_cache';
import { spriteIcon } from './lib/utils/common_utils';
import { parsePikadayDate } from './lib/utils/datetime_utility';
-import glRegexp from './lib/utils/regexp';
+import { unicodeLetters } from './lib/utils/regexp';
const USERS_ALIAS = 'users';
const ISSUES_ALIAS = 'issues';
@@ -82,8 +82,8 @@ export function membersBeforeSave(members) {
const autoCompleteAvatar = member.avatar_url || member.username.charAt(0).toUpperCase();
const rectAvatarClass = member.type === GROUP_TYPE ? 'rect-avatar' : '';
- const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline center s26"/>`;
- const txtAvatar = `<div class="avatar ${rectAvatarClass} center avatar-inline s26">${autoCompleteAvatar}</div>`;
+ const imgAvatar = `<img src="${member.avatar_url}" alt="${member.username}" class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2"/>`;
+ const txtAvatar = `<div class="avatar ${rectAvatarClass} avatar-inline s24 gl-mr-2">${autoCompleteAvatar}</div>`;
const avatarIcon = member.mentionsDisabled
? spriteIcon('notifications-off', 's16 vertical-align-middle gl-ml-2')
: '';
@@ -262,6 +262,38 @@ class GfmAutoComplete {
});
}
+ // eslint-disable-next-line class-methods-use-this
+ setSubmitReviewStates($input) {
+ if (!window.gon.features?.mrRequestChanges) return;
+
+ const REVIEW_STATES = {
+ reviewed: {
+ header: __('Comment'),
+ description: __('Submit general feedback without explicit approval.'),
+ },
+ approve: {
+ header: __('Approve'),
+ description: __('Submit feedback and approve these changes.'),
+ },
+ requested_changes: {
+ header: __('Request changes'),
+ description: __('Submit feedback that should be addressed before merging.'),
+ },
+ };
+
+ $input.filter('[data-supports-quick-actions="true"]').atwho({
+ // Always keep the trailing space otherwise the command won't display correctly
+ at: '/submit_review ',
+ alias: 'submit_review',
+ data: Object.keys(REVIEW_STATES),
+ displayTpl({ name }) {
+ const reviewState = REVIEW_STATES[name];
+
+ return `<li><span class="name gl-font-weight-bold">${reviewState.header}</span><small class="description"><em>${reviewState.description}</em></small></li>`;
+ },
+ });
+ }
+
setupEmoji($input) {
const fetchData = this.fetchData.bind(this);
@@ -275,10 +307,7 @@ class GfmAutoComplete {
callbacks: {
...this.getDefaultCallbacks(),
matcher(flag, subtext) {
- const regexp = new RegExp(
- `(?:[^${glRegexp.unicodeLetters}0-9:]|\n|^):([^ :][^:]*)?$`,
- 'gi',
- );
+ const regexp = new RegExp(`(?:[^${unicodeLetters}0-9:]|\n|^):([^ :][^:]*)?$`, 'gi');
const match = regexp.exec(subtext);
if (match && match.length) {
@@ -851,6 +880,9 @@ class GfmAutoComplete {
} else if (dataSource) {
AjaxCache.retrieve(dataSource, true)
.then((data) => {
+ if (data.some((c) => c.name === 'submit_review')) {
+ this.setSubmitReviewStates($input);
+ }
this.loadData($input, at, data);
})
.catch(() => {
diff --git a/app/assets/javascripts/graphql_shared/client/page_info.query.graphql b/app/assets/javascripts/graphql_shared/client/page_info.query.graphql
new file mode 100644
index 00000000000..958d3eade68
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/page_info.query.graphql
@@ -0,0 +1,8 @@
+query getPageInfo($input: LocalPageInfoInput) {
+ pageInfo(input: $input) @client {
+ total
+ perPage
+ nextPage
+ previousPage
+ }
+}
diff --git a/app/assets/javascripts/graphql_shared/client/page_info.typedefs.graphql b/app/assets/javascripts/graphql_shared/client/page_info.typedefs.graphql
new file mode 100644
index 00000000000..2c74fa4cc34
--- /dev/null
+++ b/app/assets/javascripts/graphql_shared/client/page_info.typedefs.graphql
@@ -0,0 +1,10 @@
+type LocalPageInfoInput {
+ page: Int
+}
+
+type LocalPageInfo {
+ total: Int!
+ perPage: Int!
+ nextPage: Int!
+ previousPage: Int!
+}
diff --git a/app/assets/javascripts/graphql_shared/constants.js b/app/assets/javascripts/graphql_shared/constants.js
index 2863f52bea9..d8b97259730 100644
--- a/app/assets/javascripts/graphql_shared/constants.js
+++ b/app/assets/javascripts/graphql_shared/constants.js
@@ -15,6 +15,7 @@ export const TYPENAME_GROUP = 'Group';
export const TYPENAME_ISSUE = 'Issue';
export const TYPENAME_ITERATION = 'Iteration';
export const TYPENAME_ITERATIONS_CADENCE = 'Iterations::Cadence';
+export const TYPENAME_MEMBER_ROLE = 'MemberRole';
export const TYPENAME_MERGE_REQUEST = 'MergeRequest';
export const TYPENAME_MILESTONE = 'Milestone';
export const TYPENAME_NOTE = 'Note';
@@ -24,9 +25,9 @@ export const TYPENAME_SCANNER_PROFILE = 'DastScannerProfile';
export const TYPENAME_SITE_PROFILE = 'DastSiteProfile';
export const TYPENAME_TODO = 'Todo';
export const TYPENAME_USER = 'User';
-export const TYPENAME_VULNERABILITIES_SCANNER = 'Vulnerabilities::Scanner';
export const TYPENAME_VULNERABILITY = 'Vulnerability';
export const TYPENAME_WORK_ITEM = 'WorkItem';
-export const TYPENAME_ORGANIZATION = 'Organization';
+export const TYPE_ORGANIZATION = 'Organizations::Organization';
export const TYPE_USERS_SAVED_REPLY = 'Users::SavedReply';
export const TYPE_WORKSPACE = 'RemoteDevelopment::Workspace';
+export const TYPE_COMPLIANCE_FRAMEWORK = 'ComplianceManagement::Framework';
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index 9537c9ef8a6..d0ba34b6127 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -146,7 +146,7 @@ export const config = {
},
IssueConnection: {
merge(existing = { nodes: [] }, incoming, { args }) {
- if (!args.after) {
+ if (!args?.after) {
return incoming;
}
return {
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 1439a3181b0..4edad63cc79 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -4,7 +4,8 @@
"AlertManagementPrometheusIntegration"
],
"AmazonS3ConfigurationInterface": [
- "AmazonS3ConfigurationType"
+ "AmazonS3ConfigurationType",
+ "InstanceAmazonS3ConfigurationType"
],
"BaseHeaderInterface": [
"AuditEventStreamingHeader",
@@ -150,6 +151,7 @@
"User": [
"AddOnUser",
"AutocompletedUser",
+ "CurrentUser",
"MergeRequestAssignee",
"MergeRequestAuthor",
"MergeRequestParticipant",
@@ -201,5 +203,11 @@
"WorkItemWidgetStatus",
"WorkItemWidgetTestReports",
"WorkItemWidgetWeight"
+ ],
+ "WorkItemWidgetDefinition": [
+ "WorkItemWidgetDefinitionAssignees",
+ "WorkItemWidgetDefinitionGeneric",
+ "WorkItemWidgetDefinitionHierarchy",
+ "WorkItemWidgetDefinitionLabels"
]
}
diff --git a/app/assets/javascripts/group_settings/constants.js b/app/assets/javascripts/group_settings/constants.js
index d4ac7d94bf4..3b595bac686 100644
--- a/app/assets/javascripts/group_settings/constants.js
+++ b/app/assets/javascripts/group_settings/constants.js
@@ -1,7 +1,7 @@
import { __, s__ } from '~/locale';
export const I18N_CONFIRM_MESSAGE = s__(
- 'Runners|Shared runners will be disabled for all projects and subgroups in this group. If you proceed, you must manually re-enable shared runners in the settings of each project and subgroup.',
+ 'Runners|Shared runners will be disabled for all projects and subgroups in this group.',
);
export const I18N_CONFIRM_OK = s__('Runners|Yes, disable shared runners');
export const I18N_CONFIRM_CANCEL = s__('Runners|No, keep shared runners enabled');
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index af1af86d0c4..3a08e3e546f 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -243,7 +243,7 @@ export default {
</div>
</gl-popover>
</template>
- <user-access-role-badge v-if="group.permission" class="gl-mr-3">
+ <user-access-role-badge v-if="group.permission" size="sm" class="gl-mr-3">
{{ group.permission }}
</user-access-role-badge>
<gl-label
@@ -254,7 +254,7 @@ export default {
size="sm"
/>
</div>
- <div v-if="group.description" class="description">
+ <div v-if="group.description" class="description gl-font-sm gl-mt-1">
<span
v-safe-html:[$options.safeHtmlConfig]="group.description"
:itemprop="microdata.descriptionItemprop"
diff --git a/app/assets/javascripts/groups/components/groups.vue b/app/assets/javascripts/groups/components/groups.vue
index 969b41f4755..f654e349119 100644
--- a/app/assets/javascripts/groups/components/groups.vue
+++ b/app/assets/javascripts/groups/components/groups.vue
@@ -40,7 +40,7 @@ export default {
</script>
<template>
- <div class="groups-list-tree-container" data-qa-selector="groups_list_tree_container">
+ <div class="groups-list-tree-container" data-testid="groups-list-tree-container">
<group-folder :groups="groups" :action="action" />
<pagination-links
:change="change"
diff --git a/app/assets/javascripts/groups/components/item_stats.vue b/app/assets/javascripts/groups/components/item_stats.vue
index d87190edfd2..55c5ef2ae80 100644
--- a/app/assets/javascripts/groups/components/item_stats.vue
+++ b/app/assets/javascripts/groups/components/item_stats.vue
@@ -68,7 +68,7 @@ export default {
css-class="project-stars"
icon-name="star"
/>
- <div v-if="isProject" class="last-updated">
+ <div v-if="isProject" class="last-updated gl-font-sm">
<time-ago-tooltip :time="item.lastActivityAt" tooltip-placement="bottom" />
</div>
</div>
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 90a0582cc9f..8781f03a412 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -228,7 +228,7 @@ export default {
<gl-search-box-by-type
:value="search"
:placeholder="$options.i18n.searchPlaceholder"
- data-qa-selector="groups_filter_field"
+ data-testid="groups-filter-field"
@input="handleSearchInput"
/>
</div>
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index b831ae7b9d6..80dd1d36734 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -21,7 +21,7 @@ export const createRouter = () => {
const router = new VueRouter({
routes,
mode: 'history',
- base: '/',
+ base: gon.relative_url_root || '/',
});
return router;
diff --git a/app/assets/javascripts/groups/service/archived_projects_service.js b/app/assets/javascripts/groups/service/archived_projects_service.js
index b9d48cc660e..7558b8d6713 100644
--- a/app/assets/javascripts/groups/service/archived_projects_service.js
+++ b/app/assets/javascripts/groups/service/archived_projects_service.js
@@ -32,7 +32,7 @@ export default class ArchivedProjectsService {
markdown_description: project.description_html,
visibility: project.visibility,
avatar_url: project.avatar_url,
- relative_path: `/${project.path_with_namespace}`,
+ relative_path: `${gon.relative_url_root}/${project.path_with_namespace}`,
edit_path: null,
leave_path: null,
can_edit: false,
diff --git a/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue b/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue
new file mode 100644
index 00000000000..76b0b819698
--- /dev/null
+++ b/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue
@@ -0,0 +1,154 @@
+<script>
+import {
+ GlButton,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
+ GlIcon,
+ GlTooltipDirective,
+} from '@gitlab/ui';
+import { __, s__, sprintf } from '~/locale';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+
+export default {
+ components: {
+ GlButton,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
+ GlIcon,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ inject: [
+ 'isGroup',
+ 'id',
+ 'leavePath',
+ 'leaveConfirmMessage',
+ 'withdrawPath',
+ 'withdrawConfirmMessage',
+ 'requestAccessPath',
+ ],
+ computed: {
+ namespaceType() {
+ return this.isGroup ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
+ },
+ leaveTitle() {
+ return this.isGroup
+ ? this.$options.i18n.groupLeaveTitle
+ : this.$options.i18n.projectLeaveTitle;
+ },
+ copyTitle() {
+ return this.isGroup ? this.$options.i18n.groupCopyTitle : this.$options.i18n.projectCopyTitle;
+ },
+ copiedToClipboard() {
+ return this.isGroup
+ ? this.$options.i18n.groupCopiedToClipboard
+ : this.$options.i18n.projectCopiedToClipboard;
+ },
+ leaveItem() {
+ return {
+ text: this.leaveTitle,
+ href: this.leavePath,
+ extraAttrs: {
+ 'aria-label': this.leaveTitle,
+ 'data-method': 'delete',
+ 'data-confirm': this.leaveConfirmMessage,
+ 'data-confirm-btn-variant': 'danger',
+ 'data-testid': `leave-${this.namespaceType}-link`,
+ rel: 'nofollow',
+ class: 'gl-text-red-500! js-leave-link',
+ },
+ };
+ },
+ withdrawItem() {
+ return {
+ text: this.$options.i18n.withdrawAccessTitle,
+ href: this.withdrawPath,
+ extraAttrs: {
+ 'data-method': 'delete',
+ 'data-confirm': this.withdrawConfirmMessage,
+ 'data-testid': 'withdraw-access-link',
+ rel: 'nofollow',
+ },
+ };
+ },
+ requestAccessItem() {
+ return {
+ text: this.$options.i18n.requestAccessTitle,
+ href: this.requestAccessPath,
+ extraAttrs: {
+ 'data-method': 'post',
+ 'data-testid': 'request-access-link',
+ rel: 'nofollow',
+ },
+ };
+ },
+ copyIdItem() {
+ return {
+ text: sprintf(this.copyTitle, { id: this.id }),
+ action: () => {
+ this.$toast.show(this.copiedToClipboard);
+ },
+ extraAttrs: {
+ 'data-testid': `copy-${this.namespaceType}-id`,
+ },
+ };
+ },
+ },
+ i18n: {
+ actionsLabel: __('Actions'),
+ groupCopiedToClipboard: s__('GroupPage|Group ID copied to clipboard.'),
+ projectCopiedToClipboard: s__('ProjectPage|Project ID copied to clipboard.'),
+ groupLeaveTitle: __('Leave group'),
+ projectLeaveTitle: __('Leave project'),
+ withdrawAccessTitle: __('Withdraw Access Request'),
+ requestAccessTitle: __('Request Access'),
+ groupCopyTitle: s__('GroupPage|Copy group ID: %{id}'),
+ projectCopyTitle: s__('ProjectPage|Copy project ID: %{id}'),
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ v-gl-tooltip.hover="$options.i18n.actionsLabel"
+ category="tertiary"
+ icon="ellipsis_v"
+ no-caret
+ :toggle-text="$options.i18n.actionsLabel"
+ text-sr-only
+ data-testid="groups-projects-more-actions-dropdown"
+ class="gl-relative gl-w-full gl-sm-w-auto"
+ >
+ <template #toggle>
+ <div class="gl-min-h-7">
+ <gl-button
+ class="gl-md-display-none! gl-new-dropdown-toggle gl-absolute gl-top-0 gl-left-0 gl-w-full gl-sm-w-auto"
+ button-text-classes="gl-w-full"
+ category="secondary"
+ :aria-label="$options.i18n.actionsLabel"
+ :title="$options.i18n.actionsLabel"
+ >
+ <span class="gl-new-dropdown-button-text">{{ $options.i18n.actionsLabel }}</span>
+ <gl-icon class="dropdown-chevron" name="chevron-down" />
+ </gl-button>
+ <gl-button
+ ref="moreActionsDropdown"
+ class="gl-display-none gl-md-display-flex! gl-new-dropdown-toggle gl-new-dropdown-icon-only gl-new-dropdown-toggle-no-caret"
+ category="tertiary"
+ icon="ellipsis_v"
+ :aria-label="$options.i18n.actionsLabel"
+ :title="$options.i18n.actionsLabel"
+ />
+ </div>
+ </template>
+
+ <gl-disclosure-dropdown-item v-if="leavePath" ref="leaveItem" :item="leaveItem" />
+
+ <gl-disclosure-dropdown-item v-else-if="withdrawPath" :item="withdrawItem" />
+
+ <gl-disclosure-dropdown-item v-else-if="requestAccessPath" :item="requestAccessItem" />
+
+ <gl-disclosure-dropdown-item v-if="id" :item="copyIdItem" :data-clipboard-text="id" />
+ </gl-disclosure-dropdown>
+</template>
diff --git a/app/assets/javascripts/groups_projects/components/transfer_locations.vue b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
index 5774065bff9..bce0a217deb 100644
--- a/app/assets/javascripts/groups_projects/components/transfer_locations.vue
+++ b/app/assets/javascripts/groups_projects/components/transfer_locations.vue
@@ -237,7 +237,6 @@ export default {
<gl-form-group :label="label">
<gl-dropdown
:text="selectedText"
- data-qa-selector="namespaces_list"
data-testid="transfer-locations-dropdown"
block
toggle-class="gl-mb-0"
@@ -248,7 +247,7 @@ export default {
<gl-search-box-by-type
v-model.trim="searchTerm"
:is-loading="isSearchLoading"
- data-qa-selector="namespaces_list_search"
+ data-testid="transfer-locations-search"
/>
</template>
<template v-if="showAdditionalDropdownItems">
@@ -265,23 +264,18 @@ export default {
<gl-dropdown-item
v-for="item in userTransferLocations"
:key="item.id"
- data-qa-selector="namespaces_list_item"
@click="handleSelect(item)"
>{{ item.humanName }}</gl-dropdown-item
>
</div>
- <div
- v-if="hasGroupTransferLocations"
- data-qa-selector="namespaces_list_groups"
- data-testid="group-transfer-locations"
- >
+ <div v-if="hasGroupTransferLocations" data-testid="group-transfer-locations">
<gl-dropdown-section-header v-if="showUserTransferLocations">{{
$options.i18n.GROUPS
}}</gl-dropdown-section-header>
<gl-dropdown-item
v-for="item in groupTransferLocations"
:key="item.id"
- data-qa-selector="namespaces_list_item"
+ data-testid="group-transfer-item"
@click="handleSelect(item)"
>{{ item.humanName }}</gl-dropdown-item
>
diff --git a/app/assets/javascripts/groups_projects/init_more_actions_dropdown.js b/app/assets/javascripts/groups_projects/init_more_actions_dropdown.js
new file mode 100644
index 00000000000..5d83f9ed3b2
--- /dev/null
+++ b/app/assets/javascripts/groups_projects/init_more_actions_dropdown.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
+import MoreActionsDropdown from '~/groups_projects/components/more_actions_dropdown.vue';
+
+export default function InitMoreActionsDropdown() {
+ const el = document.querySelector('.js-groups-projects-more-actions-dropdown');
+
+ if (!el) {
+ return false;
+ }
+
+ const {
+ isGroup,
+ id,
+ leavePath,
+ leaveConfirmMessage,
+ withdrawPath,
+ withdrawConfirmMessage,
+ requestAccessPath,
+ } = el.dataset;
+
+ return new Vue({
+ el,
+ name: 'MoreActionsDropdownRoot',
+ provide: {
+ isGroup: parseBoolean(isGroup),
+ id,
+ leavePath,
+ leaveConfirmMessage,
+ withdrawPath,
+ withdrawConfirmMessage,
+ requestAccessPath,
+ },
+ render: (createElement) => createElement(MoreActionsDropdown),
+ });
+}
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
deleted file mode 100644
index 095a2dc1324..00000000000
--- a/app/assets/javascripts/header.js
+++ /dev/null
@@ -1,145 +0,0 @@
-// TODO: Remove this with the removal of the old navigation.
-// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
-
-import Vue from 'vue';
-import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
-import { highCountTrim } from '~/lib/utils/text_utility';
-import Tracking from '~/tracking';
-import Translate from '~/vue_shared/translate';
-import { parseBoolean } from '~/lib/utils/common_utils';
-
-/**
- * Updates todo counter when todos are toggled.
- * When count is 0, we hide the badge.
- *
- * @param {jQuery.Event} e
- * @param {String} count
- */
-export default function initTodoToggle() {
- document.addEventListener('todo:toggle', (e) => {
- const updatedCount = e.detail.count || 0;
- const todoPendingCount = document.querySelector('.js-todos-count');
-
- if (todoPendingCount) {
- todoPendingCount.textContent = highCountTrim(updatedCount);
- if (updatedCount === 0) {
- todoPendingCount.classList.add('hidden');
- } else {
- todoPendingCount.classList.remove('hidden');
- }
- }
- });
-}
-
-export function initStatusTriggers() {
- const setStatusModalTriggerEl = document.querySelector('.js-set-status-modal-trigger');
-
- if (setStatusModalTriggerEl) {
- setStatusModalTriggerEl.addEventListener('click', () => {
- const topNavbar = document.querySelector('.navbar-gitlab');
- const buttonWithinTopNav = topNavbar && topNavbar.contains(setStatusModalTriggerEl);
- Tracking.event(undefined, 'click_button', {
- label: 'user_edit_status',
- property: buttonWithinTopNav ? 'navigation_top' : 'nav_user_menu',
- });
-
- import(
- /* webpackChunkName: 'statusModalBundle' */ './set_status_modal/set_status_modal_wrapper.vue'
- )
- .then(({ default: SetStatusModalWrapper }) => {
- const setStatusModalWrapperEl = document.querySelector('.js-set-status-modal-wrapper');
- const statusModalElement = document.createElement('div');
- setStatusModalWrapperEl.appendChild(statusModalElement);
-
- Vue.use(Translate);
-
- // eslint-disable-next-line no-new
- new Vue({
- el: statusModalElement,
- data() {
- const {
- currentEmoji,
- defaultEmoji,
- currentMessage,
- currentAvailability,
- currentClearStatusAfter,
- } = setStatusModalWrapperEl.dataset;
-
- return {
- currentEmoji,
- defaultEmoji,
- currentMessage,
- currentAvailability,
- currentClearStatusAfter,
- };
- },
- render(createElement) {
- const {
- currentEmoji,
- defaultEmoji,
- currentMessage,
- currentAvailability,
- currentClearStatusAfter,
- } = this;
-
- return createElement(SetStatusModalWrapper, {
- props: {
- currentEmoji,
- defaultEmoji,
- currentMessage,
- currentAvailability,
- currentClearStatusAfter,
- },
- });
- },
- });
- })
- .catch(() => {});
- });
-
- setStatusModalTriggerEl.classList.add('ready');
- }
-}
-
-function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
- const { trackLabel, trackProperty } = elToTrack.dataset;
-
- el.addEventListener('shown.bs.dropdown', () => {
- Tracking.event(document.body.dataset.page, trackEvent, {
- label: trackLabel,
- property: trackProperty,
- });
- });
-}
-
-export function initNavUserDropdownTracking() {
- const el = document.querySelector('.js-nav-user-dropdown');
- const buyEl = document.querySelector('.js-buy-pipeline-minutes-link');
-
- if (el && buyEl) {
- trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el);
- }
-}
-
-function initNewNavToggle() {
- const el = document.querySelector('.js-new-nav-toggle');
- if (!el) return false;
-
- return new Vue({
- el,
- render(h) {
- return h(NewNavToggle, {
- props: {
- enabled: parseBoolean(el.dataset.enabled),
- endpoint: el.dataset.endpoint,
- },
- });
- },
- });
-}
-
-if (!gon?.use_new_navigation) {
- requestIdleCallback(initStatusTriggers);
-}
-requestIdleCallback(initNavUserDropdownTracking);
-requestIdleCallback(initNewNavToggle);
diff --git a/app/assets/javascripts/header_search/components/app.vue b/app/assets/javascripts/header_search/components/app.vue
deleted file mode 100644
index 120b51f07cc..00000000000
--- a/app/assets/javascripts/header_search/components/app.vue
+++ /dev/null
@@ -1,306 +0,0 @@
-<script>
-import {
- GlSearchBoxByType,
- GlIcon,
- GlToken,
- GlTooltipDirective,
- GlResizeObserverDirective,
-} from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapActions, mapGetters } from 'vuex';
-import { debounce } from 'lodash';
-import { visitUrl } from '~/lib/utils/url_utility';
-import { truncate } from '~/lib/utils/text_utility';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import { sprintf } from '~/locale';
-import Tracking from '~/tracking';
-import DropdownKeyboardNavigation from '~/vue_shared/components/dropdown_keyboard_navigation.vue';
-import {
- SEARCH_GITLAB,
- SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
- SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
- SEARCH_DESCRIBED_BY_DEFAULT,
- SEARCH_DESCRIBED_BY_UPDATED,
- SEARCH_RESULTS_LOADING,
- SEARCH_RESULTS_SCOPE,
- KBD_HELP,
-} from '~/vue_shared/global_search/constants';
-import {
- FIRST_DROPDOWN_INDEX,
- SEARCH_BOX_INDEX,
- SEARCH_INPUT_DESCRIPTION,
- SEARCH_RESULTS_DESCRIPTION,
- SEARCH_SHORTCUTS_MIN_CHARACTERS,
- SCOPE_TOKEN_MAX_LENGTH,
- INPUT_FIELD_PADDING,
- IS_SEARCHING,
- IS_FOCUSED,
- IS_NOT_FOCUSED,
- DROPDOWN_CLOSE_TIMEOUT,
-} from '../constants';
-import HeaderSearchAutocompleteItems from './header_search_autocomplete_items.vue';
-import HeaderSearchDefaultItems from './header_search_default_items.vue';
-import HeaderSearchScopedItems from './header_search_scoped_items.vue';
-
-export default {
- name: 'HeaderSearchApp',
- i18n: {
- SEARCH_GITLAB,
- SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN,
- SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN,
- SEARCH_DESCRIBED_BY_DEFAULT,
- SEARCH_DESCRIBED_BY_UPDATED,
- SEARCH_RESULTS_LOADING,
- SEARCH_RESULTS_SCOPE,
- KBD_HELP,
- },
- directives: { GlTooltip: GlTooltipDirective, GlResizeObserverDirective },
- components: {
- GlSearchBoxByType,
- HeaderSearchDefaultItems,
- HeaderSearchScopedItems,
- HeaderSearchAutocompleteItems,
- DropdownKeyboardNavigation,
- GlIcon,
- GlToken,
- },
- data() {
- return {
- isFocused: false,
- currentFocusIndex: SEARCH_BOX_INDEX,
- };
- },
- computed: {
- ...mapState(['search', 'loading', 'searchContext']),
- ...mapGetters(['searchQuery', 'searchOptions']),
- searchText: {
- get() {
- return this.search;
- },
- set(value) {
- this.setSearch(value);
- },
- },
- currentFocusedOption() {
- return this.searchOptions[this.currentFocusIndex];
- },
- currentFocusedId() {
- return this.currentFocusedOption?.html_id;
- },
- isLoggedIn() {
- return Boolean(gon?.current_username);
- },
- showSearchDropdown() {
- if (!this.isFocused || !this.isLoggedIn) {
- return false;
- }
- return this.searchOptions?.length > 0;
- },
- showDefaultItems() {
- return !this.searchText;
- },
- searchTermOverMin() {
- return this.searchText?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
- },
- defaultIndex() {
- if (this.showDefaultItems) {
- return SEARCH_BOX_INDEX;
- }
- return FIRST_DROPDOWN_INDEX;
- },
- searchInputDescribeBy() {
- if (this.isLoggedIn) {
- return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_WITH_DROPDOWN;
- }
- return this.$options.i18n.SEARCH_INPUT_DESCRIBE_BY_NO_DROPDOWN;
- },
- dropdownResultsDescription() {
- if (!this.showSearchDropdown) {
- return ''; // This allows aria-live to see register an update when the dropdown is shown
- }
-
- if (this.showDefaultItems) {
- return sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_DEFAULT, {
- count: this.searchOptions.length,
- });
- }
-
- return this.loading
- ? this.$options.i18n.SEARCH_RESULTS_LOADING
- : sprintf(this.$options.i18n.SEARCH_DESCRIBED_BY_UPDATED, {
- count: this.searchOptions.length,
- });
- },
- searchBarClasses() {
- return {
- [IS_SEARCHING]: this.searchTermOverMin,
- [IS_FOCUSED]: this.isFocused,
- [IS_NOT_FOCUSED]: !this.isFocused,
- };
- },
- showScopeHelp() {
- return this.searchTermOverMin && this.isFocused;
- },
- searchBarItem() {
- return this.searchOptions?.[0];
- },
- infieldHelpContent() {
- return this.searchBarItem?.scope || this.searchBarItem?.description;
- },
- infieldHelpIcon() {
- return this.searchBarItem?.icon;
- },
- scopeTokenTitle() {
- return sprintf(this.$options.i18n.SEARCH_RESULTS_SCOPE, {
- scope: this.infieldHelpContent,
- });
- },
- },
- methods: {
- ...mapActions(['setSearch', 'fetchAutocompleteOptions', 'clearAutocomplete']),
- openDropdown() {
- this.isFocused = true;
- this.$emit('expandSearchBar');
-
- Tracking.event(undefined, 'focus_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- },
- collapseAndCloseSearchBar() {
- // without timeout dropdown closes
- // before click event is dispatched
- setTimeout(() => {
- this.isFocused = false;
- this.$emit('collapseSearchBar');
-
- Tracking.event(undefined, 'blur_input', {
- label: 'global_search',
- property: 'navigation_top',
- });
- }, DROPDOWN_CLOSE_TIMEOUT);
- },
- submitSearch() {
- if (this.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS && this.currentFocusIndex < 0) {
- return null;
- }
- return visitUrl(this.currentFocusedOption?.url || this.searchQuery);
- },
- getAutocompleteOptions: debounce(function debouncedSearch(searchTerm) {
- this.openDropdown();
- if (!searchTerm) {
- this.clearAutocomplete();
- } else {
- this.fetchAutocompleteOptions();
- }
- }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS),
- getTruncatedScope(scope) {
- return truncate(scope, SCOPE_TOKEN_MAX_LENGTH);
- },
- observeTokenWidth({ contentRect: { width } }) {
- const inputField = this.$refs?.searchInputBox?.$el?.querySelector('input');
- if (!inputField) {
- return;
- }
- inputField.style.paddingRight = `${width + INPUT_FIELD_PADDING}px`;
- },
- },
- SEARCH_BOX_INDEX,
- FIRST_DROPDOWN_INDEX,
- SEARCH_INPUT_DESCRIPTION,
- SEARCH_RESULTS_DESCRIPTION,
-};
-</script>
-
-<template>
- <form
- role="search"
- :aria-label="$options.i18n.SEARCH_GITLAB"
- class="header-search-form gl-relative gl-rounded-base gl-w-full"
- :class="searchBarClasses"
- data-testid="header-search-form"
- >
- <gl-search-box-by-type
- id="search"
- ref="searchInputBox"
- v-model="searchText"
- role="searchbox"
- class="gl-z-index-1"
- data-testid="global-search-input"
- autocomplete="off"
- :placeholder="$options.i18n.SEARCH_GITLAB"
- :aria-activedescendant="currentFocusedId"
- :aria-describedby="$options.SEARCH_INPUT_DESCRIPTION"
- @focusin="openDropdown"
- @focusout="collapseAndCloseSearchBar"
- @input="getAutocompleteOptions"
- @keydown.enter.stop.prevent="submitSearch"
- @keydown.esc.stop.prevent="collapseAndCloseSearchBar"
- />
- <gl-token
- v-if="showScopeHelp"
- v-gl-resize-observer-directive="observeTokenWidth"
- class="in-search-scope-help"
- :view-only="true"
- :title="scopeTokenTitle"
- ><gl-icon
- v-if="infieldHelpIcon"
- class="gl-mr-2"
- :aria-label="infieldHelpContent"
- :name="infieldHelpIcon"
- :size="16"
- />{{
- getTruncatedScope(
- sprintf($options.i18n.SEARCH_RESULTS_SCOPE, {
- scope: infieldHelpContent,
- }),
- )
- }}
- </gl-token>
- <kbd
- v-show="!isFocused"
- v-gl-tooltip.bottom.hover.html
- class="gl-absolute gl-right-3 gl-top-0 gl-z-index-1 keyboard-shortcut-helper"
- :title="$options.i18n.KBD_HELP"
- >/</kbd
- >
- <span :id="$options.SEARCH_INPUT_DESCRIPTION" role="region" class="gl-sr-only">{{
- searchInputDescribeBy
- }}</span>
- <span
- role="region"
- :data-testid="$options.SEARCH_RESULTS_DESCRIPTION"
- class="gl-sr-only"
- aria-live="polite"
- aria-atomic="true"
- >
- {{ dropdownResultsDescription }}
- </span>
- <div
- v-if="showSearchDropdown"
- data-testid="header-search-dropdown-menu"
- class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3"
- >
- <div class="header-search-dropdown-content gl-py-2">
- <dropdown-keyboard-navigation
- v-model="currentFocusIndex"
- :max="searchOptions.length - 1"
- :min="$options.FIRST_DROPDOWN_INDEX"
- :default-index="defaultIndex"
- :enable-cycle="true"
- />
- <header-search-default-items
- v-if="showDefaultItems"
- :current-focused-option="currentFocusedOption"
- />
- <template v-else>
- <header-search-scoped-items
- v-if="searchTermOverMin"
- :current-focused-option="currentFocusedOption"
- />
- <header-search-autocomplete-items :current-focused-option="currentFocusedOption" />
- </template>
- </div>
- </div>
- </form>
-</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue b/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
deleted file mode 100644
index a785ae2a859..00000000000
--- a/app/assets/javascripts/header_search/components/header_search_autocomplete_items.vue
+++ /dev/null
@@ -1,168 +0,0 @@
-<script>
-import {
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlDropdownDivider,
- GlAvatar,
- GlAlert,
- GlLoadingIcon,
-} from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapGetters } from 'vuex';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import highlight from '~/lib/utils/highlight';
-import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-import { truncateNamespace } from '~/lib/utils/text_utility';
-import {
- GROUPS_CATEGORY,
- PROJECTS_CATEGORY,
- MERGE_REQUEST_CATEGORY,
- ISSUES_CATEGORY,
- RECENT_EPICS_CATEGORY,
- AUTOCOMPLETE_ERROR_MESSAGE,
-} from '~/vue_shared/global_search/constants';
-import { LARGE_AVATAR_PX, SMALL_AVATAR_PX } from '../constants';
-
-export default {
- name: 'HeaderSearchAutocompleteItems',
- i18n: {
- AUTOCOMPLETE_ERROR_MESSAGE,
- },
- components: {
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlDropdownDivider,
- GlAvatar,
- GlAlert,
- GlLoadingIcon,
- },
- directives: {
- SafeHtml,
- },
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
- },
- computed: {
- ...mapState(['search', 'loading', 'autocompleteError', 'searchContext']),
- ...mapGetters(['autocompleteGroupedSearchOptions']),
- },
- watch: {
- currentFocusedOption() {
- const focusedElement = this.$refs[this.currentFocusedOption?.html_id]?.[0]?.$el;
-
- if (focusedElement) {
- focusedElement.scrollIntoView(false);
- }
- },
- },
- methods: {
- truncateNamespace(string) {
- if (string.split(' / ').length > 2) {
- return truncateNamespace(string);
- }
-
- return string;
- },
- highlightedName(val) {
- return highlight(val, this.search);
- },
- avatarSize(data) {
- if (data.category === GROUPS_CATEGORY || data.category === PROJECTS_CATEGORY) {
- return LARGE_AVATAR_PX;
- }
-
- return SMALL_AVATAR_PX;
- },
- isOptionFocused(data) {
- return this.currentFocusedOption?.html_id === data.html_id;
- },
- isProjectsCategory(data) {
- return data.category === PROJECTS_CATEGORY;
- },
- getEntityId(data) {
- switch (data.category) {
- case GROUPS_CATEGORY:
- case RECENT_EPICS_CATEGORY:
- return data.group_id || data.id || this.searchContext?.group?.id;
- case PROJECTS_CATEGORY:
- case ISSUES_CATEGORY:
- case MERGE_REQUEST_CATEGORY:
- return data.project_id || data.id || this.searchContext?.project?.id;
- default:
- return data.id;
- }
- },
- getEntitytName(data) {
- switch (data.category) {
- case GROUPS_CATEGORY:
- case RECENT_EPICS_CATEGORY:
- return data.group_name || data.value || data.label || this.searchContext?.group?.name;
- case PROJECTS_CATEGORY:
- case ISSUES_CATEGORY:
- case MERGE_REQUEST_CATEGORY:
- return data.project_name || data.value || data.label || this.searchContext?.project?.name;
- default:
- return data.label;
- }
- },
- },
- AVATAR_SHAPE_OPTION_RECT,
-};
-</script>
-
-<template>
- <div>
- <template v-if="!loading">
- <div v-for="(option, index) in autocompleteGroupedSearchOptions" :key="option.category">
- <gl-dropdown-divider v-if="index > 0" />
- <gl-dropdown-section-header>{{ option.category }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="data in option.data"
- :id="data.html_id"
- :ref="data.html_id"
- :key="data.html_id"
- :class="{ 'gl-bg-gray-50': isOptionFocused(data) }"
- :aria-selected="isOptionFocused(data)"
- :aria-label="data.label"
- tabindex="-1"
- :href="data.url"
- >
- <div class="gl-display-flex gl-align-items-center" aria-hidden="true">
- <gl-avatar
- v-if="data.avatar_url !== undefined"
- :src="data.avatar_url"
- :entity-id="getEntityId(data)"
- :entity-name="getEntitytName(data)"
- :size="avatarSize(data)"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- />
- <span class="gl-display-flex gl-flex-direction-column">
- <span
- v-safe-html="highlightedName(data.value || data.label)"
- class="gl-text-gray-900"
- ></span>
- <span
- v-if="data.value"
- v-safe-html="truncateNamespace(data.label)"
- class="gl-font-sm gl-text-gray-500"
- ></span>
- </span>
- </div>
- </gl-dropdown-item>
- </div>
- </template>
- <gl-loading-icon v-else size="lg" class="my-4" />
- <gl-alert
- v-if="autocompleteError"
- class="gl-text-body gl-mt-2"
- :dismissible="false"
- variant="danger"
- >
- {{ $options.i18n.AUTOCOMPLETE_ERROR_MESSAGE }}
- </gl-alert>
- </div>
-</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_default_items.vue b/app/assets/javascripts/header_search/components/header_search_default_items.vue
deleted file mode 100644
index 6afee197c60..00000000000
--- a/app/assets/javascripts/header_search/components/header_search_default_items.vue
+++ /dev/null
@@ -1,59 +0,0 @@
-<script>
-import { GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapGetters } from 'vuex';
-import { ALL_GITLAB } from '~/vue_shared/global_search/constants';
-
-export default {
- name: 'HeaderSearchDefaultItems',
- i18n: {
- ALL_GITLAB,
- },
- components: {
- GlDropdownSectionHeader,
- GlDropdownItem,
- },
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
- },
- computed: {
- ...mapState(['searchContext']),
- ...mapGetters(['defaultSearchOptions']),
- sectionHeader() {
- return (
- this.searchContext?.project?.name ||
- this.searchContext?.group?.name ||
- this.$options.i18n.ALL_GITLAB
- );
- },
- },
- methods: {
- isOptionFocused(option) {
- return this.currentFocusedOption?.html_id === option.html_id;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-dropdown-section-header>{{ sectionHeader }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="option in defaultSearchOptions"
- :id="option.html_id"
- :ref="option.html_id"
- :key="option.html_id"
- :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
- :aria-selected="isOptionFocused(option)"
- :aria-label="option.title"
- tabindex="-1"
- :href="option.url"
- >
- <span aria-hidden="true">{{ option.title }}</span>
- </gl-dropdown-item>
- </div>
-</template>
diff --git a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue b/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
deleted file mode 100644
index 7faef5f9bd7..00000000000
--- a/app/assets/javascripts/header_search/components/header_search_scoped_items.vue
+++ /dev/null
@@ -1,88 +0,0 @@
-<script>
-import { GlDropdownItem, GlIcon, GlToken } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapGetters } from 'vuex';
-import { s__, sprintf } from '~/locale';
-import { truncate } from '~/lib/utils/text_utility';
-import { SCOPED_SEARCH_ITEM_ARIA_LABEL } from '~/vue_shared/global_search/constants';
-import { SCOPE_TOKEN_MAX_LENGTH } from '../constants';
-
-export default {
- name: 'HeaderSearchScopedItems',
- i18n: {
- SCOPED_SEARCH_ITEM_ARIA_LABEL,
- },
- components: {
- GlDropdownItem,
- GlIcon,
- GlToken,
- },
- props: {
- currentFocusedOption: {
- type: Object,
- required: false,
- default: () => null,
- },
- },
- computed: {
- ...mapState(['search']),
- ...mapGetters(['scopedSearchOptions', 'autocompleteGroupedSearchOptions']),
- },
- methods: {
- isOptionFocused(option) {
- return this.currentFocusedOption?.html_id === option.html_id;
- },
- ariaLabel(option) {
- return sprintf(this.$options.i18n.SCOPED_SEARCH_ITEM_ARIA_LABEL, {
- search: this.search,
- description: option.description || option.icon,
- scope: option.scope || '',
- });
- },
- titleLabel(option) {
- return sprintf(s__('GlobalSearch|in %{scope}'), {
- search: this.search,
- scope: option.scope || option.description,
- });
- },
- getTruncatedScope(scope) {
- return truncate(scope, SCOPE_TOKEN_MAX_LENGTH);
- },
- },
-};
-</script>
-
-<template>
- <div>
- <gl-dropdown-item
- v-for="option in scopedSearchOptions"
- :id="option.html_id"
- :ref="option.html_id"
- :key="option.html_id"
- class="gl-max-w-full"
- :class="{ 'gl-bg-gray-50': isOptionFocused(option) }"
- :aria-selected="isOptionFocused(option)"
- :aria-label="ariaLabel(option)"
- tabindex="-1"
- :href="option.url"
- :title="titleLabel(option)"
- >
- <span
- ref="token-text-content"
- class="gl-display-flex gl-justify-content-start search-text-content gl-line-height-24 gl-align-items-start gl-flex-direction-row gl-w-full"
- >
- <gl-icon name="search" class="gl-flex-shrink-0 gl-mr-2 gl-relative gl-pt-2" />
- <span class="gl-flex-grow-1 gl-relative">
- <gl-token
- class="in-dropdown-scope-help has-icon gl-flex-shrink-0 gl-relative gl-white-space-nowrap gl-float-right gl-mr-n3!"
- :view-only="true"
- >
- <gl-icon v-if="option.icon" :name="option.icon" class="gl-mr-2" />
- <span>{{ getTruncatedScope(titleLabel(option)) }}</span>
- </gl-token>
- {{ search }}
- </span>
- </span>
- </gl-dropdown-item>
- </div>
-</template>
diff --git a/app/assets/javascripts/header_search/constants.js b/app/assets/javascripts/header_search/constants.js
deleted file mode 100644
index 47aeb2f9caa..00000000000
--- a/app/assets/javascripts/header_search/constants.js
+++ /dev/null
@@ -1,35 +0,0 @@
-export const ICON_PROJECT = 'project';
-
-export const ICON_GROUP = 'group';
-
-export const ICON_SUBGROUP = 'subgroup';
-
-export const LARGE_AVATAR_PX = 32;
-
-export const SMALL_AVATAR_PX = 16;
-
-export const FIRST_DROPDOWN_INDEX = 0;
-
-export const SEARCH_BOX_INDEX = -1;
-
-export const SEARCH_SHORTCUTS_MIN_CHARACTERS = 2;
-
-export const SEARCH_INPUT_DESCRIPTION = 'search-input-description';
-
-export const SEARCH_RESULTS_DESCRIPTION = 'search-results-description';
-
-export const SCOPE_TOKEN_MAX_LENGTH = 36;
-
-export const INPUT_FIELD_PADDING = 52;
-
-export const HEADER_INIT_EVENTS = ['input', 'focus'];
-
-export const IS_SEARCHING = 'is-searching';
-export const IS_FOCUSED = 'is-focused';
-export const IS_NOT_FOCUSED = 'is-not-focused';
-
-export const FETCH_TYPES = ['generic', 'search'];
-
-export const SEARCH_INPUT_FIELD_MAX_WIDTH = '640px';
-
-export const DROPDOWN_CLOSE_TIMEOUT = 200;
diff --git a/app/assets/javascripts/header_search/index.js b/app/assets/javascripts/header_search/index.js
deleted file mode 100644
index 7b26dd183ad..00000000000
--- a/app/assets/javascripts/header_search/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import Translate from '~/vue_shared/translate';
-import HeaderSearchApp from './components/app.vue';
-import createStore from './store';
-import { SEARCH_INPUT_FIELD_MAX_WIDTH } from './constants';
-
-Vue.use(Translate);
-
-export const initHeaderSearchApp = (search = '') => {
- const el = document.getElementById('js-header-search');
- const headerEl = document.querySelector('.header-content');
-
- if (!el || !headerEl) {
- return false;
- }
-
- const searchContainer = headerEl.querySelector('.global-search-container');
- const newHeader = headerEl.querySelector('.header-search');
-
- const { searchPath, issuesPath, mrPath, autocompletePath } = el.dataset;
- let { searchContext } = el.dataset;
-
- try {
- searchContext = JSON.parse(searchContext);
- newHeader.style.maxWidth = SEARCH_INPUT_FIELD_MAX_WIDTH;
- } catch (error) {
- Sentry.captureException(error);
- }
-
- return new Vue({
- el,
- store: createStore({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
- render(createElement) {
- return createElement(HeaderSearchApp, {
- on: {
- expandSearchBar: () => {
- searchContainer.style.flexGrow = '1';
- },
- collapseSearchBar: () => {
- searchContainer.style.flexGrow = '0';
- },
- },
- });
- },
- });
-};
diff --git a/app/assets/javascripts/header_search/init.js b/app/assets/javascripts/header_search/init.js
deleted file mode 100644
index 1c582ace480..00000000000
--- a/app/assets/javascripts/header_search/init.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import { HEADER_INIT_EVENTS } from './constants';
-
-async function eventHandler(callback = () => {}) {
- const { initHeaderSearchApp } = await import(
- /* webpackChunkName: 'globalSearch' */ '~/header_search'
- ).catch((error) => Sentry.captureException(error));
-
- // In case the user started searching before we bootstrapped,
- // let's pass the search along.
- const initialSearchValue = this.searchInputBox.value;
- initHeaderSearchApp(initialSearchValue);
-
- // this is new #search input element. We need to re-find it.
- // And re-focus in it.
- document.querySelector('#search').focus();
- callback();
-}
-
-function cleanEventListeners() {
- HEADER_INIT_EVENTS.forEach((eventType) => {
- document.querySelector('#search').removeEventListener(eventType, eventHandler);
- });
-}
-
-function initHeaderSearch() {
- const searchInputBox = document.querySelector('#search');
-
- HEADER_INIT_EVENTS.forEach((eventType) => {
- searchInputBox?.addEventListener(
- eventType,
- eventHandler.bind({ searchInputBox }, cleanEventListeners),
- { once: true },
- );
- });
-}
-
-export default initHeaderSearch;
-export { eventHandler, cleanEventListeners };
diff --git a/app/assets/javascripts/header_search/store/actions.js b/app/assets/javascripts/header_search/store/actions.js
deleted file mode 100644
index a0f9e594506..00000000000
--- a/app/assets/javascripts/header_search/store/actions.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import { omitBy, isNil } from 'lodash';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import axios from '~/lib/utils/axios_utils';
-import { FETCH_TYPES } from '../constants';
-import * as types from './mutation_types';
-
-export const autocompleteQuery = ({ state, fetchType }) => {
- const query = omitBy(
- {
- term: state.search,
- project_id: state.searchContext?.project?.id,
- project_ref: state.searchContext?.ref,
- filter: fetchType,
- },
- isNil,
- );
-
- return `${state.autocompletePath}?${objectToQuery(query)}`;
-};
-
-const doFetch = ({ commit, state, fetchType }) => {
- return axios
- .get(autocompleteQuery({ state, fetchType }))
- .then(({ data }) => {
- commit(types.RECEIVE_AUTOCOMPLETE_SUCCESS, data);
- })
- .catch(() => {
- commit(types.RECEIVE_AUTOCOMPLETE_ERROR);
- });
-};
-
-export const fetchAutocompleteOptions = ({ commit, state }) => {
- commit(types.REQUEST_AUTOCOMPLETE);
- const promises = FETCH_TYPES.map((fetchType) => doFetch({ commit, state, fetchType }));
-
- return Promise.all(promises);
-};
-
-export const clearAutocomplete = ({ commit }) => {
- commit(types.CLEAR_AUTOCOMPLETE);
-};
-
-export const setSearch = ({ commit }, value) => {
- commit(types.SET_SEARCH, value);
-};
diff --git a/app/assets/javascripts/header_search/store/getters.js b/app/assets/javascripts/header_search/store/getters.js
deleted file mode 100644
index f86463b94d1..00000000000
--- a/app/assets/javascripts/header_search/store/getters.js
+++ /dev/null
@@ -1,220 +0,0 @@
-import { omitBy, isNil } from 'lodash';
-import { objectToQuery } from '~/lib/utils/url_utility';
-
-import {
- MSG_ISSUES_ASSIGNED_TO_ME,
- MSG_ISSUES_IVE_CREATED,
- MSG_MR_ASSIGNED_TO_ME,
- MSG_MR_IM_REVIEWER,
- MSG_MR_IVE_CREATED,
- MSG_IN_ALL_GITLAB,
- PROJECTS_CATEGORY,
- GROUPS_CATEGORY,
- DROPDOWN_ORDER,
-} from '~/vue_shared/global_search/constants';
-import {
- ICON_GROUP,
- ICON_SUBGROUP,
- ICON_PROJECT,
- SEARCH_SHORTCUTS_MIN_CHARACTERS,
-} from '../constants';
-
-export const searchQuery = (state) => {
- const query = omitBy(
- {
- search: state.search,
- nav_source: 'navbar',
- project_id: state.searchContext?.project?.id,
- group_id: state.searchContext?.group?.id,
- scope: state.searchContext?.scope,
- snippets: state.searchContext?.for_snippets ? true : null,
- search_code: state.searchContext?.code_search ? true : null,
- repository_ref: state.searchContext?.ref,
- },
- isNil,
- );
-
- return `${state.searchPath}?${objectToQuery(query)}`;
-};
-
-export const scopedIssuesPath = (state) => {
- if (state.searchContext?.project?.id && !state.searchContext?.project_metadata?.issues_path) {
- return false;
- }
-
- return (
- state.searchContext?.project_metadata?.issues_path ||
- state.searchContext?.group_metadata?.issues_path ||
- state.issuesPath
- );
-};
-
-export const scopedMRPath = (state) => {
- return (
- state.searchContext?.project_metadata?.mr_path ||
- state.searchContext?.group_metadata?.mr_path ||
- state.mrPath
- );
-};
-
-export const defaultSearchOptions = (state, getters) => {
- const userName = gon.current_username;
-
- const issues = [
- {
- html_id: 'default-issues-assigned',
- title: MSG_ISSUES_ASSIGNED_TO_ME,
- url: `${getters.scopedIssuesPath}/?assignee_username=${userName}`,
- },
- {
- html_id: 'default-issues-created',
- title: MSG_ISSUES_IVE_CREATED,
- url: `${getters.scopedIssuesPath}/?author_username=${userName}`,
- },
- ];
-
- const mergeRequests = [
- {
- html_id: 'default-mrs-assigned',
- title: MSG_MR_ASSIGNED_TO_ME,
- url: `${getters.scopedMRPath}/?assignee_username=${userName}`,
- },
- {
- html_id: 'default-mrs-reviewer',
- title: MSG_MR_IM_REVIEWER,
- url: `${getters.scopedMRPath}/?reviewer_username=${userName}`,
- },
- {
- html_id: 'default-mrs-created',
- title: MSG_MR_IVE_CREATED,
- url: `${getters.scopedMRPath}/?author_username=${userName}`,
- },
- ];
- return [...(getters.scopedIssuesPath ? issues : []), ...mergeRequests];
-};
-
-export const projectUrl = (state) => {
- const query = omitBy(
- {
- search: state.search,
- nav_source: 'navbar',
- project_id: state.searchContext?.project?.id,
- group_id: state.searchContext?.group?.id,
- scope: state.searchContext?.scope,
- snippets: state.searchContext?.for_snippets ? true : null,
- search_code: state.searchContext?.code_search ? true : null,
- repository_ref: state.searchContext?.ref,
- },
- isNil,
- );
-
- return `${state.searchPath}?${objectToQuery(query)}`;
-};
-
-export const groupUrl = (state) => {
- const query = omitBy(
- {
- search: state.search,
- nav_source: 'navbar',
- group_id: state.searchContext?.group?.id,
- scope: state.searchContext?.scope,
- snippets: state.searchContext?.for_snippets ? true : null,
- search_code: state.searchContext?.code_search ? true : null,
- repository_ref: state.searchContext?.ref,
- },
- isNil,
- );
-
- return `${state.searchPath}?${objectToQuery(query)}`;
-};
-
-export const allUrl = (state) => {
- const query = omitBy(
- {
- search: state.search,
- nav_source: 'navbar',
- scope: state.searchContext?.scope,
- snippets: state.searchContext?.for_snippets ? true : null,
- search_code: state.searchContext?.code_search ? true : null,
- repository_ref: state.searchContext?.ref,
- },
- isNil,
- );
-
- return `${state.searchPath}?${objectToQuery(query)}`;
-};
-
-export const scopedSearchOptions = (state, getters) => {
- const options = [];
-
- if (state.searchContext?.project) {
- options.push({
- html_id: 'scoped-in-project',
- scope: state.searchContext.project?.name || '',
- scopeCategory: PROJECTS_CATEGORY,
- icon: ICON_PROJECT,
- url: getters.projectUrl,
- });
- }
-
- if (state.searchContext?.group) {
- options.push({
- html_id: 'scoped-in-group',
- scope: state.searchContext.group?.name || '',
- scopeCategory: GROUPS_CATEGORY,
- icon: state.searchContext.group?.full_name?.includes('/') ? ICON_SUBGROUP : ICON_GROUP,
- url: getters.groupUrl,
- });
- }
-
- options.push({
- html_id: 'scoped-in-all',
- description: MSG_IN_ALL_GITLAB,
- url: getters.allUrl,
- });
-
- return options;
-};
-
-export const autocompleteGroupedSearchOptions = (state) => {
- const groupedOptions = {};
- const results = [];
-
- state.autocompleteOptions.forEach((option) => {
- const category = groupedOptions[option.category];
-
- if (category) {
- category.data.push(option);
- } else {
- groupedOptions[option.category] = {
- category: option.category,
- data: [option],
- };
-
- results.push(groupedOptions[option.category]);
- }
- });
-
- return results.sort(
- (a, b) => DROPDOWN_ORDER.indexOf(a.category) - DROPDOWN_ORDER.indexOf(b.category),
- );
-};
-
-export const searchOptions = (state, getters) => {
- if (!state.search) {
- return getters.defaultSearchOptions;
- }
-
- const sortedAutocompleteOptions = Object.values(getters.autocompleteGroupedSearchOptions).reduce(
- (options, group) => {
- return [...options, ...group.data];
- },
- [],
- );
-
- if (state.search?.length <= SEARCH_SHORTCUTS_MIN_CHARACTERS) {
- return sortedAutocompleteOptions;
- }
-
- return getters.scopedSearchOptions.concat(sortedAutocompleteOptions);
-};
diff --git a/app/assets/javascripts/header_search/store/index.js b/app/assets/javascripts/header_search/store/index.js
deleted file mode 100644
index ca5519f529c..00000000000
--- a/app/assets/javascripts/header_search/store/index.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-import createState from './state';
-
-Vue.use(Vuex);
-
-export const getStoreConfig = ({
- searchPath,
- issuesPath,
- mrPath,
- autocompletePath,
- searchContext,
- search,
-}) => ({
- actions,
- getters,
- mutations,
- state: createState({ searchPath, issuesPath, mrPath, autocompletePath, searchContext, search }),
-});
-
-const createStore = (config) => new Vuex.Store(getStoreConfig(config));
-export default createStore;
diff --git a/app/assets/javascripts/header_search/store/mutation_types.js b/app/assets/javascripts/header_search/store/mutation_types.js
deleted file mode 100644
index 6e65345757f..00000000000
--- a/app/assets/javascripts/header_search/store/mutation_types.js
+++ /dev/null
@@ -1,6 +0,0 @@
-export const REQUEST_AUTOCOMPLETE = 'REQUEST_AUTOCOMPLETE';
-export const RECEIVE_AUTOCOMPLETE_SUCCESS = 'RECEIVE_AUTOCOMPLETE_SUCCESS';
-export const RECEIVE_AUTOCOMPLETE_ERROR = 'RECEIVE_AUTOCOMPLETE_ERROR';
-export const CLEAR_AUTOCOMPLETE = 'CLEAR_AUTOCOMPLETE';
-
-export const SET_SEARCH = 'SET_SEARCH';
diff --git a/app/assets/javascripts/header_search/store/mutations.js b/app/assets/javascripts/header_search/store/mutations.js
deleted file mode 100644
index 19b4d4ec330..00000000000
--- a/app/assets/javascripts/header_search/store/mutations.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import * as types from './mutation_types';
-
-export default {
- [types.REQUEST_AUTOCOMPLETE](state) {
- state.loading = true;
- state.autocompleteOptions = [];
- state.autocompleteError = false;
- },
- [types.RECEIVE_AUTOCOMPLETE_SUCCESS](state, data) {
- state.loading = false;
- state.autocompleteOptions = [...state.autocompleteOptions].concat(
- data.map((d, i) => {
- return { html_id: `autocomplete-${d.category}-${i}`, ...d };
- }),
- );
- state.autocompleteError = false;
- },
- [types.RECEIVE_AUTOCOMPLETE_ERROR](state) {
- state.loading = false;
- state.autocompleteOptions = [];
- state.autocompleteError = true;
- },
- [types.CLEAR_AUTOCOMPLETE](state) {
- state.autocompleteOptions = [];
- state.autocompleteError = false;
- },
- [types.SET_SEARCH](state, value) {
- state.search = value;
- },
-};
diff --git a/app/assets/javascripts/header_search/store/state.js b/app/assets/javascripts/header_search/store/state.js
deleted file mode 100644
index bebdbc7b92e..00000000000
--- a/app/assets/javascripts/header_search/store/state.js
+++ /dev/null
@@ -1,19 +0,0 @@
-const createState = ({
- searchPath,
- issuesPath,
- mrPath,
- autocompletePath,
- searchContext,
- search,
-}) => ({
- searchPath,
- issuesPath,
- mrPath,
- autocompletePath,
- searchContext,
- search,
- autocompleteOptions: [],
- autocompleteError: false,
- loading: false,
-});
-export default createState;
diff --git a/app/assets/javascripts/ide/components/activity_bar.vue b/app/assets/javascripts/ide/components/activity_bar.vue
index 44a94f5fefe..ef808c8218a 100644
--- a/app/assets/javascripts/ide/components/activity_bar.vue
+++ b/app/assets/javascripts/ide/components/activity_bar.vue
@@ -43,7 +43,6 @@ export default {
:aria-label="s__('IDE|Edit')"
data-container="body"
data-placement="right"
- data-qa-selector="edit_mode_tab"
data-testid="edit-mode-button"
type="button"
class="ide-sidebar-link js-ide-edit-mode"
@@ -80,7 +79,6 @@ export default {
:aria-label="s__('IDE|Commit')"
data-container="body"
data-placement="right"
- data-qa-selector="commit_mode_tab"
data-testid="commit-mode-button"
type="button"
class="ide-sidebar-link js-ide-commit-mode"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
index bc8496e359c..8765808cf0c 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue
@@ -78,7 +78,6 @@ export default {
:value="$options.commitToCurrentBranch"
:disabled="!canPushToBranch"
:title="$options.currentBranchPermissionsTooltip"
- data-qa-selector="commit_to_current_branch_radio_container"
>
<span class="ide-option-label">
<gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')">
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/form.vue b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
index 281a3054721..708b5d84e5b 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/form.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/form.vue
@@ -159,7 +159,6 @@ export default {
category="primary"
variant="confirm"
block
- data-qa-selector="begin_commit_button"
data-testid="begin-commit-button"
@click="beginCommit"
>
@@ -187,7 +186,6 @@ export default {
:disabled="commitButtonDisabled"
:loading="submitCommitLoading"
data-testid="commit-button"
- data-qa-selector="commit_button"
category="primary"
variant="confirm"
type="submit"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
index 76d3acb8e1f..d7bf42ff559 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -3,7 +3,7 @@
import { GlButton, GlModal, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions } from 'vuex';
-import { __, sprintf } from '~/locale';
+import { __ } from '~/locale';
import ListItem from './list_item.vue';
export default {
@@ -55,11 +55,6 @@ export default {
},
},
computed: {
- titleText() {
- if (!this.title) return __('Changes');
-
- return sprintf(__('%{title} changes'), { title: this.title });
- },
filesLength() {
return this.fileList.length;
},
@@ -84,7 +79,7 @@ export default {
<div class="ide-commit-list-container">
<header class="multi-file-commit-panel-header gl-display-flex gl-mb-0">
<div class="gl-display-flex gl-align-items-center flex-fill">
- <strong> {{ titleText }} </strong>
+ <strong> {{ __('Changes') }} </strong>
<div class="gl-display-flex gl-ml-auto">
<gl-button
v-if="!stagedList"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 69d84bcc6aa..5ee28ae58bb 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
@@ -86,11 +86,7 @@ export default {
role="button"
@click="openFileInEditor"
>
- <span
- class="multi-file-commit-list-file-path d-flex align-items-center"
- data-qa-selector="file_to_commit_content"
- :data-qa-file-name="file.name"
- >
+ <span class="multi-file-commit-list-file-path d-flex align-items-center">
<file-icon :file-name="file.name" class="gl-mr-3" />
<template v-if="file.prevName && file.prevName !== file.name">
{{ file.prevName }} &#x2192;
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
index d05aa960f01..372ff9812ac 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/message_field.vue
@@ -112,7 +112,6 @@ export default {
:placeholder="placeholder"
:value="text"
class="note-textarea ide-commit-message-textarea"
- data-qa-selector="ide_commit_message_field"
dir="auto"
name="commit-message"
@scroll="handleScroll"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
index 38b71e3da73..0c1faad1573 100644
--- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue
@@ -76,7 +76,6 @@ export default {
:value="value"
:disabled="disabled"
name="commit-action"
- data-qa-selector="commit_type_radio"
@change="updateCommitAction(value)"
>
<span v-if="label" class="ide-option-label">
diff --git a/app/assets/javascripts/ide/components/error_message.vue b/app/assets/javascripts/ide/components/error_message.vue
index ce3d8f53fd2..0eb781e0ba2 100644
--- a/app/assets/javascripts/ide/components/error_message.vue
+++ b/app/assets/javascripts/ide/components/error_message.vue
@@ -53,7 +53,6 @@ export default {
<template>
<gl-alert
- data-qa-selector="flash_alert"
variant="danger"
:dismissible="canDismiss"
:primary-button-text="message.actionText"
diff --git a/app/assets/javascripts/ide/components/file_templates/bar.vue b/app/assets/javascripts/ide/components/file_templates/bar.vue
index 287ebc99662..e24e1468b6c 100644
--- a/app/assets/javascripts/ide/components/file_templates/bar.vue
+++ b/app/assets/javascripts/ide/components/file_templates/bar.vue
@@ -84,7 +84,6 @@ export default {
<div
class="gl-display-flex gl-align-items-center ide-file-templates gl-relative gl-z-index-1"
data-testid="file-templates-bar"
- data-qa-selector="file_templates_container"
>
<strong class="gl-mr-3"> {{ $options.i18n.barLabel }} </strong>
<gl-dropdown
@@ -102,12 +101,11 @@ export default {
<gl-dropdown
v-if="showTemplatesDropdown"
class="gl-mr-6"
- data-qa-selector="file_template_dropdown"
:text="$options.i18n.templateListDropdownLabel"
@show="fetchTemplateTypes"
>
<template #header>
- <gl-search-box-by-type v-model.trim="search" data-qa-selector="dropdown_filter_input" />
+ <gl-search-box-by-type v-model.trim="search" />
</template>
<div>
<gl-loading-icon v-if="isLoading" />
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
index 6cb26643b66..a850d37c4c0 100644
--- a/app/assets/javascripts/ide/components/ide.vue
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -186,7 +186,6 @@ export default {
category="primary"
:title="__('New file')"
:aria-label="__('New file')"
- data-qa-selector="first_file_button"
@click="createNewFile()"
>
{{ __('New file') }}
diff --git a/app/assets/javascripts/ide/components/ide_project_header.vue b/app/assets/javascripts/ide/components/ide_project_header.vue
index 3296dc2060c..6e1a4cd8e99 100644
--- a/app/assets/javascripts/ide/components/ide_project_header.vue
+++ b/app/assets/javascripts/ide/components/ide_project_header.vue
@@ -25,11 +25,7 @@ export default {
/>
<span class="ide-sidebar-project-title">
<span class="sidebar-context-title"> {{ project.name }} </span>
- <span
- class="sidebar-context-title text-secondary"
- data-qa-selector="project_path_content"
- :data-qa-project-path="project.path_with_namespace"
- >
+ <span class="sidebar-context-title text-secondary">
{{ project.path_with_namespace }}
</span>
</span>
diff --git a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
index d8daf3b7ad6..172c622e195 100644
--- a/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
+++ b/app/assets/javascripts/ide/components/ide_sidebar_nav.vue
@@ -71,7 +71,6 @@ export default {
:title="tab.title"
:aria-label="tab.title"
:class="buttonClasses(tab)"
- :data-qa-selector="`${tab.title.toLowerCase()}_tab_button`"
class="ide-sidebar-link"
type="button"
@click="clickTab($event, tab)"
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
index 984dc9edaf1..0c464606a98 100644
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -4,7 +4,7 @@ import { GlIcon, GlTooltipDirective } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
import timeAgoMixin from '~/vue_shared/mixins/timeago';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import { rightSidebarViews } from '../constants';
import IdeStatusList from './ide_status_list.vue';
@@ -106,7 +106,6 @@ export default {
:href="getCommitPath(lastCommit.short_id)"
class="commit-sha"
data-testid="commit-sha-content"
- data-qa-selector="commit_sha_content"
>{{ lastCommit.short_id }}</a
>
by
diff --git a/app/assets/javascripts/ide/components/ide_tree.vue b/app/assets/javascripts/ide/components/ide_tree.vue
index 427b3743961..5999d349ca8 100644
--- a/app/assets/javascripts/ide/components/ide_tree.vue
+++ b/app/assets/javascripts/ide/components/ide_tree.vue
@@ -61,7 +61,6 @@ export default {
:show-label="false"
class="gl-display-flex gl-border-0 gl-p-0 gl-mr-5"
icon="doc-new"
- data-qa-selector="new_file_button"
@click="createNewFile()"
/>
<upload
@@ -75,7 +74,6 @@ export default {
:show-label="false"
class="gl-display-flex gl-border-0 gl-p-0"
icon="folder-new"
- data-qa-selector="new_directory_button"
@click="createNewFolder()"
/>
</div>
diff --git a/app/assets/javascripts/ide/components/ide_tree_list.vue b/app/assets/javascripts/ide/components/ide_tree_list.vue
index f2a97e62190..7c3441b8cd0 100644
--- a/app/assets/javascripts/ide/components/ide_tree_list.vue
+++ b/app/assets/javascripts/ide/components/ide_tree_list.vue
@@ -53,7 +53,7 @@ export default {
</script>
<template>
- <div class="ide-file-list" data-qa-selector="file_list_container">
+ <div class="ide-file-list">
<template v-if="showLoading">
<div v-for="n in 3" :key="n" class="multi-file-loading-container">
<gl-skeleton-loader />
diff --git a/app/assets/javascripts/ide/components/jobs/detail/description.vue b/app/assets/javascripts/ide/components/jobs/detail/description.vue
index f5840661c17..21b12220b37 100644
--- a/app/assets/javascripts/ide/components/jobs/detail/description.vue
+++ b/app/assets/javascripts/ide/components/jobs/detail/description.vue
@@ -1,7 +1,7 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlIcon } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
index 7cd415169cc..5e71470f383 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -66,7 +66,6 @@ export default {
:aria-label="__('Create new file or directory')"
type="button"
class="rounded border-0 d-flex ide-entry-dropdown-toggle"
- data-qa-selector="dropdown_button"
@click.stop="openDropdown()"
>
<gl-icon name="ellipsis_v" />
@@ -100,7 +99,6 @@ export default {
class="d-flex"
icon="pencil"
icon-classes="mr-2"
- data-qa-selector="rename_move_button"
@click="createNewItem($options.modalTypes.rename)"
/>
</li>
@@ -110,7 +108,6 @@ export default {
class="d-flex"
icon="remove"
icon-classes="mr-2"
- data-qa-selector="delete_button"
@click="deleteEntry(path)"
/>
</li>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index ba1258f8b50..1e4eca23cb5 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -157,7 +157,6 @@ export default {
<gl-modal
ref="modal"
modal-id="ide-new-entry"
- data-qa-selector="new_file_modal"
data-testid="ide-new-entry"
:title="modalTitle"
size="lg"
@@ -179,11 +178,7 @@ export default {
:placeholder="placeholder"
/>
</form>
- <ul
- v-if="isCreatingNewFile"
- class="file-templates gl-mt-3 list-inline"
- data-qa-selector="template_list_content"
- >
+ <ul v-if="isCreatingNewFile" class="file-templates gl-mt-3 list-inline">
<li v-for="(template, index) in templateTypes" :key="index" class="list-inline-item">
<gl-button
variant="dashed"
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 9664c5bc597..69cf5c9b252 100644
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -82,7 +82,6 @@ export default {
type="file"
class="hidden"
multiple
- data-qa-selector="file_upload_field"
@change="openFile"
/>
</li>
diff --git a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
index ce55d88437d..95d38da508e 100644
--- a/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
+++ b/app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue
@@ -74,11 +74,7 @@ export default {
</script>
<template>
- <div
- :class="`ide-${side}-sidebar`"
- :data-qa-selector="`ide_${side}_sidebar`"
- class="multi-file-commit-panel ide-sidebar"
- >
+ <div :class="`ide-${side}-sidebar`" class="multi-file-commit-panel ide-sidebar">
<div
v-show="isOpen"
:class="`ide-${side}-sidebar-${currentView}`"
diff --git a/app/assets/javascripts/ide/components/pipelines/list.vue b/app/assets/javascripts/ide/components/pipelines/list.vue
index 0e07cc34dd8..5f0030b0795 100644
--- a/app/assets/javascripts/ide/components/pipelines/list.vue
+++ b/app/assets/javascripts/ide/components/pipelines/list.vue
@@ -5,7 +5,7 @@ import { GlLoadingIcon, GlIcon, GlTabs, GlTab, GlBadge, GlAlert } from '@gitlab/
import { mapActions, mapGetters, mapState } from 'vuex';
import SafeHtml from '~/vue_shared/directives/safe_html';
import IDEServices from '~/ide/services';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import JobsList from '../jobs/list.vue';
import EmptyState from './empty_state.vue';
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 3b59fe86764..8f4f777d396 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -543,7 +543,6 @@ export default {
'is-added': file.tempFile,
}"
class="multi-file-editor-holder"
- data-qa-selector="editor_container"
data-testid="editor-container"
:data-editor-loading="isEditorLoading"
@focusout="triggerFilesChange"
diff --git a/app/assets/javascripts/ide/components/shared/commit_message_field.vue b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
index 428cf7f55ac..e7725054329 100644
--- a/app/assets/javascripts/ide/components/shared/commit_message_field.vue
+++ b/app/assets/javascripts/ide/components/shared/commit_message_field.vue
@@ -121,7 +121,6 @@ export default {
:placeholder="placeholder"
:value="text"
class="gl-absolute gl-w-full gl-h-full gl-z-index-2 gl-font-monospace p-0 gl-outline-0 gl-bg-transparent gl-border-0"
- data-qa-selector="ide_commit_message_field"
dir="auto"
name="commit-message"
@scroll="handleScroll"
diff --git a/app/assets/javascripts/ide/components/terminal/empty_state.vue b/app/assets/javascripts/ide/components/terminal/empty_state.vue
index fa93f6d42a5..e7dcf2ca6b6 100644
--- a/app/assets/javascripts/ide/components/terminal/empty_state.vue
+++ b/app/assets/javascripts/ide/components/terminal/empty_state.vue
@@ -53,13 +53,7 @@ export default {
<template v-else>
<p>{{ __('Run tests against your code live using the Web Terminal') }}</p>
<p>
- <gl-button
- :disabled="!isValid"
- category="primary"
- variant="confirm"
- data-qa-selector="start_web_terminal_button"
- @click="onStart"
- >
+ <gl-button :disabled="!isValid" category="primary" variant="confirm" @click="onStart">
{{ __('Start Web Terminal') }}
</gl-button>
</p>
diff --git a/app/assets/javascripts/ide/components/terminal/terminal.vue b/app/assets/javascripts/ide/components/terminal/terminal.vue
index b2e90a64758..b34febe5f1d 100644
--- a/app/assets/javascripts/ide/components/terminal/terminal.vue
+++ b/app/assets/javascripts/ide/components/terminal/terminal.vue
@@ -95,7 +95,7 @@ export default {
<template>
<div class="d-flex flex-column flex-fill min-height-0 pr-3">
<div class="top-bar d-flex border-left-0 align-items-center">
- <div v-if="loadingText" data-qa-selector="loading_container">
+ <div v-if="loadingText">
<gl-loading-icon size="sm" :inline="true" />
<span>{{ loadingText }}</span>
</div>
@@ -113,7 +113,6 @@ export default {
ref="terminal"
class="ide-terminal-trace flex-fill min-height-0 w-100"
:data-project-path="terminalPath"
- data-qa-selector="terminal_screen"
></div>
</div>
</div>
diff --git a/app/assets/javascripts/ide/init_gitlab_web_ide.js b/app/assets/javascripts/ide/init_gitlab_web_ide.js
index 868830c953a..f5fb4c8be2f 100644
--- a/app/assets/javascripts/ide/init_gitlab_web_ide.js
+++ b/app/assets/javascripts/ide/init_gitlab_web_ide.js
@@ -6,10 +6,13 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import csrf from '~/lib/utils/csrf';
import Tracking from '~/tracking';
-import { getBaseConfig } from './lib/gitlab_web_ide/get_base_config';
-import { setupRootElement } from './lib/gitlab_web_ide/setup_root_element';
+import {
+ getBaseConfig,
+ getOAuthConfig,
+ setupRootElement,
+ handleTracking,
+} from './lib/gitlab_web_ide';
import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from './constants';
-import { handleTracking } from './lib/gitlab_web_ide/handle_tracking_event';
const buildRemoteIdeURL = (ideRemotePath, remoteHost, remotePathArg) => {
const remotePath = cleanLeadingSeparator(remotePathArg);
@@ -51,15 +54,21 @@ export const initGitlabWebIDE = async (el) => {
: null;
const forkInfo = forkInfoJSON ? JSON.parse(forkInfoJSON) : null;
+ const oauthConfig = getOAuthConfig(el.dataset);
+ const httpHeaders = oauthConfig
+ ? undefined
+ : // Use same headers as defined in axios_utils (not needed in oauth)
+ {
+ [csrf.headerKey]: csrf.token,
+ 'X-Requested-With': 'XMLHttpRequest',
+ };
+
// See ClientOnlyConfig https://gitlab.com/gitlab-org/gitlab-web-ide/-/blob/main/packages/web-ide-types/src/config.ts#L17
start(rootEl, {
...getBaseConfig(),
nonce,
- // Use same headers as defined in axios_utils
- httpHeaders: {
- [csrf.headerKey]: csrf.token,
- 'X-Requested-With': 'XMLHttpRequest',
- },
+ httpHeaders,
+ auth: oauthConfig,
projectPath,
ref,
filePath,
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js
new file mode 100644
index 00000000000..5493a9ba7c7
--- /dev/null
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/get_oauth_config.js
@@ -0,0 +1,12 @@
+export const getOAuthConfig = ({ clientId, callbackUrl }) => {
+ if (!clientId) {
+ return undefined;
+ }
+
+ return {
+ type: 'oauth',
+ clientId,
+ callbackUrl,
+ protectRefreshToken: true,
+ };
+};
diff --git a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
index 8311e11672e..87e0002c8c8 100644
--- a/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
+++ b/app/assets/javascripts/ide/lib/gitlab_web_ide/index.js
@@ -1,2 +1,4 @@
export * from './get_base_config';
+export * from './get_oauth_config';
+export * from './handle_tracking_event';
export * from './setup_root_element';
diff --git a/app/assets/javascripts/ide/mount_oauth_callback.js b/app/assets/javascripts/ide/mount_oauth_callback.js
new file mode 100644
index 00000000000..79fffb24f8e
--- /dev/null
+++ b/app/assets/javascripts/ide/mount_oauth_callback.js
@@ -0,0 +1,12 @@
+import { oauthCallback } from '@gitlab/web-ide';
+import { getBaseConfig, getOAuthConfig } from './lib/gitlab_web_ide';
+
+export const mountOAuthCallback = () => {
+ const el = document.getElementById('ide');
+
+ return oauthCallback({
+ ...getBaseConfig(),
+ username: gon.current_username,
+ auth: getOAuthConfig(el.dataset),
+ });
+};
diff --git a/app/assets/javascripts/import/constants.js b/app/assets/javascripts/import/constants.js
index b02eb3c4307..cbb01d0bbf1 100644
--- a/app/assets/javascripts/import/constants.js
+++ b/app/assets/javascripts/import/constants.js
@@ -7,9 +7,12 @@ export const BULK_IMPORT_STATIC_ITEMS = {
epics: __('Epic'),
issues: __('Issue'),
labels: __('Label'),
+ iterations: __('Iteration'),
+ iterations_cadences: s__('Iterations|Iteration cadence'),
members: __('Member'),
merge_requests: __('Merge request'),
milestones: __('Milestone'),
+ namespace_settings: s__('GroupSettings|Namespace setting'),
project: __('Project'),
};
diff --git a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
index 5da16454032..a248dd3d2c4 100644
--- a/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
+++ b/app/assets/javascripts/import/details/components/bulk_import_details_app.vue
@@ -1,10 +1,14 @@
<script>
-import { __ } from '~/locale';
+import { sprintf, s__, __ } from '~/locale';
+import { getParameterValues } from '~/lib/utils/url_utility';
+
import ImportDetailsTable from '~/import/details/components/import_details_table.vue';
export default {
name: 'BulkImportDetailsApp',
- components: { ImportDetailsTable },
+ components: {
+ ImportDetailsTable,
+ },
fields: [
{
@@ -28,12 +32,25 @@ export default {
],
LOCAL_STORAGE_KEY: 'gl-bulk-import-details-page-size',
+
+ gitlabLogo: window.gon.gitlab_logo,
+
+ computed: {
+ title() {
+ const id = getParameterValues('entity_id')[0];
+
+ return sprintf(s__('BulkImport|Items that failed to be imported for %{id}'), { id });
+ },
+ },
};
</script>
<template>
<div>
- <h1>{{ s__('Import|GitLab Migration details') }}</h1>
+ <h1 class="gl-font-size-h1 gl-my-0 gl-py-4 gl-display-flex gl-align-items-center gl-gap-3">
+ <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6" />
+ <span>{{ title }}</span>
+ </h1>
<import-details-table
bulk-import
diff --git a/app/assets/javascripts/import/details/components/import_details_table.vue b/app/assets/javascripts/import/details/components/import_details_table.vue
index 535ccb525ac..8b9bf14e3a3 100644
--- a/app/assets/javascripts/import/details/components/import_details_table.vue
+++ b/app/assets/javascripts/import/details/components/import_details_table.vue
@@ -21,7 +21,6 @@ export default {
GlTable,
PaginationBar,
},
- STATISTIC_ITEMS,
i18n: {
fetchErrorMessage: s__('Import|An error occurred while fetching import details.'),
@@ -141,7 +140,7 @@ export default {
<template>
<div>
- <gl-table :fields="fields" :items="items" class="gl-mt-5" :busy="loading" show-empty>
+ <gl-table :fields="fields" :items="items" :busy="loading" show-empty>
<template #table-busy>
<gl-loading-icon size="lg" class="gl-my-5" />
</template>
diff --git a/app/assets/javascripts/import_entities/constants.js b/app/assets/javascripts/import_entities/constants.js
index 23604c7fb44..6f1ca92d80d 100644
--- a/app/assets/javascripts/import_entities/constants.js
+++ b/app/assets/javascripts/import_entities/constants.js
@@ -15,8 +15,12 @@ export const STATUSES = {
export const PROVIDERS = {
GITHUB: 'github',
+ BITBUCKET_SERVER: 'bitbucket_server',
};
+// Retrieved from value of `PAGE_LENGTH` in lib/bitbucket_server/paginator.rb
+export const BITBUCKET_SERVER_PAGE_LENGTH = 25;
+
const SCHEDULED_STATUS_ICON = {
icon: 'status-scheduled',
text: __('Pending'),
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue b/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue
new file mode 100644
index 00000000000..218e7dee953
--- /dev/null
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_history_link.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { mergeUrlParams } from '~/lib/utils/url_utility';
+
+export default {
+ components: {
+ GlLink,
+ },
+
+ props: {
+ id: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ historyPath: {
+ type: String,
+ required: true,
+ },
+ },
+
+ computed: {
+ historyPathWithId() {
+ return mergeUrlParams({ bulk_import_id: this.id }, this.historyPath);
+ },
+ },
+};
+</script>
+<template>
+ <gl-link :href="historyPathWithId">{{ __('View details') }}</gl-link>
+</template>
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
index df1e50cb433..db354a01899 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_table.vue
@@ -13,8 +13,9 @@ import {
GlFormCheckbox,
GlTooltipDirective,
} from '@gitlab/ui';
-import { debounce } from 'lodash';
+import { debounce, isNumber } from 'lodash';
import { createAlert } from '~/alert';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__, __, n__, sprintf } from '~/locale';
import { HTTP_STATUS_TOO_MANY_REQUESTS } from '~/lib/utils/http_status';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -28,7 +29,6 @@ import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/i
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { STATUSES } from '../../constants';
-import ImportStatusCell from '../../components/import_status.vue';
import importGroupsMutation from '../graphql/mutations/import_groups.mutation.graphql';
import updateImportStatusMutation from '../graphql/mutations/update_import_status.mutation.graphql';
import bulkImportSourceGroupsQuery from '../graphql/queries/bulk_import_source_groups.query.graphql';
@@ -36,7 +36,9 @@ import { NEW_NAME_FIELD, ROOT_NAMESPACE, i18n } from '../constants';
import { StatusPoller } from '../services/status_poller';
import { isFinished, isAvailableForImport, isNameValid, isSameTarget } from '../utils';
import ImportActionsCell from './import_actions_cell.vue';
+import ImportHistoryLink from './import_history_link.vue';
import ImportSourceCell from './import_source_cell.vue';
+import ImportStatusCell from './import_status.vue';
import ImportTargetCell from './import_target_cell.vue';
const VALIDATION_DEBOUNCE_TIME = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
@@ -60,6 +62,7 @@ export default {
ImportTargetCell,
ImportStatusCell,
ImportActionsCell,
+ ImportHistoryLink,
PaginationBar,
HelpPopover,
},
@@ -143,7 +146,7 @@ export default {
{
key: 'progress',
label: __('Status'),
- tdAttr: { 'data-qa-selector': 'import_status_indicator' },
+ tdAttr: { 'data-testid': 'import-status-indicator' },
},
{
key: 'actions',
@@ -283,10 +286,18 @@ export default {
this.statusPoller = new StatusPoller({
pollPath: this.jobsPath,
updateImportStatus: (update) => {
- this.$apollo.mutate({
- mutation: updateImportStatusMutation,
- variables: { id: update.id, status: update.status_name },
- });
+ try {
+ this.$apollo.mutate({
+ mutation: updateImportStatusMutation,
+ variables: {
+ id: update.id,
+ status: update.status_name,
+ hasFailures: update.has_failures,
+ },
+ });
+ } catch (error) {
+ Sentry.captureException(error);
+ }
},
});
@@ -315,7 +326,7 @@ export default {
qaRowAttributes(group, type) {
if (type === 'row') {
return {
- 'data-qa-selector': 'import_item',
+ 'data-testid': 'import-item',
'data-qa-source-group': group.fullPath,
};
}
@@ -339,6 +350,16 @@ export default {
return group.progress?.status || STATUSES.NONE;
},
+ hasFailures(group) {
+ return group.progress?.hasFailures;
+ },
+
+ showHistoryLink(group) {
+ // We need to check for `isNumber` to make sure `id` is passed from the backend
+ // and not "LOCAL-PROGRESS-${id}" as defined by client_factory.js
+ return group.progress?.id && isNumber(group.progress.id);
+ },
+
updateImportTarget(group, changes) {
const newImportTarget = {
...group.importTarget,
@@ -570,11 +591,11 @@ export default {
<template>
<div>
<div
- class="gl-display-flex gl-align-items-center gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1"
+ class="gl-display-flex gl-align-items-center gl-border-solid gl-border-gray-100 gl-border-0 gl-border-b-1"
>
- <h1 class="gl-my-0 gl-py-4 gl-font-size-h1gl-display-flex">
- <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
- {{ s__('BulkImport|Import groups from GitLab') }}
+ <h1 class="gl-font-size-h1 gl-my-0 gl-py-4 gl-display-flex gl-align-items-center gl-gap-3">
+ <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6" />
+ <span>{{ s__('BulkImport|Import groups by direct transfer') }}</span>
</h1>
<gl-link :href="historyPath" class="gl-ml-auto">{{ s__('BulkImport|History') }}</gl-link>
</div>
@@ -735,7 +756,6 @@ export default {
<gl-table
ref="table"
class="gl-w-full import-table"
- data-qa-selector="import_table"
:tbody-tr-class="rowClasses"
:tbody-tr-attr="qaRowAttributes"
thead-class="gl-sticky gl-z-index-2 gl-bg-gray-10"
@@ -786,7 +806,13 @@ export default {
/>
</template>
<template #cell(progress)="{ item: group }">
- <import-status-cell :status="group.visibleStatus" class="gl-line-height-32" />
+ <import-status-cell :status="group.visibleStatus" :has-failures="hasFailures(group)" />
+ <import-history-link
+ v-if="showHistoryLink(group)"
+ :id="group.progress.id"
+ :history-path="historyPath"
+ class="gl-display-inline-block gl-mt-2"
+ />
</template>
<template #cell(actions)="{ item: group, index }">
<import-actions-cell
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
index de0595360bf..4046d25acc3 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/client_factory.js
@@ -111,7 +111,11 @@ export function createResolvers({ endpoints }) {
},
},
Mutation: {
- async updateImportStatus(_, { id, status: newStatus }, { client, getCacheKey }) {
+ updateImportStatus(
+ _,
+ { id, status: newStatus, hasFailures = false },
+ { client, getCacheKey },
+ ) {
const progressItem = client.readFragment({
fragment: bulkImportSourceGroupProgressFragment,
fragmentName: 'BulkImportSourceGroupProgress',
@@ -123,13 +127,14 @@ export function createResolvers({ endpoints }) {
if (!progressItem) return null;
- localStorageCache.updateStatusByJobId(id, newStatus);
+ localStorageCache.updateStatusByJobId(id, newStatus, hasFailures);
return {
__typename: clientTypenames.BulkImportProgress,
...progressItem,
id,
status: newStatus,
+ hasFailures,
};
},
@@ -172,6 +177,7 @@ export function createResolvers({ endpoints }) {
id: response.id || `local-${Date.now()}-${idx}`,
status: response.success ? STATUSES.CREATED : STATUSES.FAILED,
message: response.message || null,
+ hasFailures: !response.success,
};
localStorageCache.set(op.group.webUrl, { progress, lastImportTarget });
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
index 33c564f36a8..43b512946bd 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/fragments/bulk_import_source_group_progress.fragment.graphql
@@ -1,5 +1,6 @@
fragment BulkImportSourceGroupProgress on ClientBulkImportProgress {
id
status
+ hasFailures
message
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
index 39289887b75..b0909840f67 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/import_groups.mutation.graphql
@@ -9,6 +9,7 @@ mutation importGroups($importRequests: [ImportGroupInput!]!) {
progress {
id
status
+ hasFailures
message
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql
index 8c0233b2939..1eaffeb87a1 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/mutations/update_import_status.mutation.graphql
@@ -1,6 +1,7 @@
-mutation updateImportStatus($status: String!, $id: String!) {
- updateImportStatus(status: $status, id: $id) @client {
+mutation updateImportStatus($status: String!, $id: String!, $hasFailures: Boolean) {
+ updateImportStatus(status: $status, id: $id, hasFailures: $hasFailures) @client {
id
status
+ hasFailures
}
}
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
index c2e35ce8270..a186b366ea9 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/services/local_storage_cache.js
@@ -1,8 +1,8 @@
import { debounce, merge } from 'lodash';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-const OLD_KEY = 'gl-bulk-imports-import-state';
-export const KEY = 'gl-bulk-imports-import-state-v2';
+const OLD_KEY = 'gl-bulk-imports-import-state-v2';
+export const KEY = 'gl-bulk-imports-import-state-v3';
export const DEBOUNCE_INTERVAL = DEFAULT_DEBOUNCE_AND_THROTTLE_MS;
export class LocalStorageCache {
@@ -57,13 +57,14 @@ export class LocalStorageCache {
return this.jobsLookupCache[jobId];
}
- updateStatusByJobId(jobId, status) {
+ updateStatusByJobId(jobId, status, hasFailures) {
this.getCacheKeysByJobId(jobId).forEach((webUrl) =>
this.set(webUrl, {
...this.get(webUrl),
progress: {
id: jobId,
status,
+ hasFailures,
},
}),
);
diff --git a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
index 83d17a5baa7..7ea25de9a29 100644
--- a/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
+++ b/app/assets/javascripts/import_entities/import_groups/graphql/typedefs.graphql
@@ -17,6 +17,7 @@ type ClientBulkImportSourceGroupConnection {
type ClientBulkImportProgress {
id: ID!
status: String!
+ hasFailures: Boolean
message: String
}
@@ -79,5 +80,5 @@ input ImportRequestInput {
extend type Mutation {
importGroups(importRequests: [ImportRequestInput!]!): [ClientBulkImportSourceGroup!]!
- updateImportStatus(id: ID, status: String!): ClientBulkImportProgress
+ updateImportStatus(id: ID, status: String!, hasFailures: Boolean): ClientBulkImportProgress
}
diff --git a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
index d98132382c6..bceded18a09 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/import_projects_table.vue
@@ -105,10 +105,7 @@ export default {
mounted() {
this.fetchJobs();
-
- if (!this.paginatable) {
- this.fetchRepos();
- }
+ this.fetchRepos();
},
beforeDestroy() {
diff --git a/app/assets/javascripts/import_entities/import_projects/index.js b/app/assets/javascripts/import_entities/import_projects/index.js
index 6ee637b1ce8..cd2e8d690d6 100644
--- a/app/assets/javascripts/import_entities/import_projects/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/index.js
@@ -21,7 +21,6 @@ export function initStoreFromElement(element) {
importPath,
cancelPath,
defaultTargetNamespace,
- paginatable,
} = element.dataset;
return createStore({
@@ -37,7 +36,6 @@ export function initStoreFromElement(element) {
importPath,
cancelPath,
},
- hasPagination: parseBoolean(paginatable),
});
}
diff --git a/app/assets/javascripts/import_entities/import_projects/store/actions.js b/app/assets/javascripts/import_entities/import_projects/store/actions.js
index e5cbac71ce0..96437736c82 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/actions.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/actions.js
@@ -1,5 +1,5 @@
import Visibility from 'visibilityjs';
-import _ from 'lodash';
+import { isEmpty } from 'lodash';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
@@ -9,7 +9,7 @@ import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import { visitUrl, objectToQuery } from '~/lib/utils/url_utility';
import { s__, sprintf } from '~/locale';
import { isProjectImportable } from '../utils';
-import { PROVIDERS } from '../../constants';
+import { PROVIDERS, BITBUCKET_SERVER_PAGE_LENGTH } from '../../constants';
import * as types from './mutation_types';
let eTagPoll;
@@ -25,7 +25,7 @@ const pathWithParams = ({ path, ...params }) => {
return queryString ? `${path}?${queryString}` : path;
};
const commitPaginationData = ({ state, commit, data }) => {
- const cursorsGitHubResponse = !_.isEmpty(data.pageInfo || {});
+ const cursorsGitHubResponse = !isEmpty(data.pageInfo || {});
if (state.provider === PROVIDERS.GITHUB && cursorsGitHubResponse) {
commit(types.SET_PAGE_CURSORS, data.pageInfo);
@@ -33,6 +33,16 @@ const commitPaginationData = ({ state, commit, data }) => {
const nextPage = state.pageInfo.page + 1;
commit(types.SET_PAGE, nextPage);
}
+
+ // Only BitBucket Server uses pagination with page length
+ if (state.provider === PROVIDERS.BITBUCKET_SERVER) {
+ const reposLength = data.providerRepos.length;
+ if (reposLength > 0 && reposLength % BITBUCKET_SERVER_PAGE_LENGTH === 0) {
+ commit(types.SET_HAS_NEXT_PAGE, true);
+ } else {
+ commit(types.SET_HAS_NEXT_PAGE, false);
+ }
+ }
};
const paginationParams = ({ state }) => {
if (state.provider === PROVIDERS.GITHUB && state.pageInfo.endCursor) {
diff --git a/app/assets/javascripts/import_entities/import_projects/store/index.js b/app/assets/javascripts/import_entities/import_projects/store/index.js
index d3edb48e1db..058c04002d9 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/index.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/index.js
@@ -8,10 +8,10 @@ import state from './state';
Vue.use(Vuex);
-export default ({ initialState, endpoints, hasPagination }) =>
+export default ({ initialState, endpoints }) =>
new Vuex.Store({
state: { ...state(), ...initialState },
- actions: actionsFactory({ endpoints, hasPagination }),
+ actions: actionsFactory({ endpoints }),
mutations,
getters,
});
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
index 74832a03ac1..6adb8fb615b 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutation_types.js
@@ -17,3 +17,5 @@ export const SET_IMPORT_TARGET = 'SET_IMPORT_TARGET';
export const SET_PAGE = 'SET_PAGE';
export const SET_PAGE_CURSORS = 'SET_PAGE_CURSORS';
+
+export const SET_HAS_NEXT_PAGE = 'SET_HAS_NEXT_PAGE';
diff --git a/app/assets/javascripts/import_entities/import_projects/store/mutations.js b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
index df529449f90..5ace3237e5d 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/mutations.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/mutations.js
@@ -143,4 +143,8 @@ export default {
const { startCursor, endCursor, hasNextPage } = pageInfo;
state.pageInfo = { ...state.pageInfo, startCursor, endCursor, hasNextPage };
},
+
+ [types.SET_HAS_NEXT_PAGE](state, hasNextPage) {
+ state.pageInfo.hasNextPage = hasNextPage;
+ },
};
diff --git a/app/assets/javascripts/import_entities/import_projects/store/state.js b/app/assets/javascripts/import_entities/import_projects/store/state.js
index 62dcefd3339..b90039c5f0b 100644
--- a/app/assets/javascripts/import_entities/import_projects/store/state.js
+++ b/app/assets/javascripts/import_entities/import_projects/store/state.js
@@ -9,6 +9,6 @@ export default () => ({
page: 0,
startCursor: null,
endCursor: null,
- hasNextPage: true,
+ hasNextPage: false,
},
});
diff --git a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
index bd45412a481..f3ebbd91a8b 100644
--- a/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
+++ b/app/assets/javascripts/integrations/edit/components/dynamic_field.vue
@@ -106,7 +106,7 @@ export default {
});
},
fieldId() {
- return `service_${this.name}`;
+ return `service-${this.name}`;
},
fieldName() {
return `service[${this.name}]`;
@@ -182,7 +182,7 @@ export default {
autocomplete="new-password"
:placeholder="placeholder"
:required="passwordRequired"
- :data-qa-selector="`${fieldId}_field`"
+ :data-testid="`${fieldId}-field`"
/>
<gl-form-input
v-else
@@ -191,7 +191,7 @@ export default {
:type="type"
:placeholder="placeholder"
:required="required"
- :data-qa-selector="`${fieldId}_field`"
+ :data-testid="`${fieldId}-field`"
/>
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue
index 281666a021d..efd5f395f7f 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue
@@ -256,7 +256,7 @@ export default {
:key="`${currentKey}-${field.name}`"
v-bind="field"
:is-validated="isValidated"
- :data-qa-selector="`${field.name}_div`"
+ :data-testid="`${field.name}-div`"
/>
</section>
diff --git a/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
index a9d7c1ca378..d6eae8a7e23 100644
--- a/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
+++ b/app/assets/javascripts/integrations/edit/components/integration_form_actions.vue
@@ -79,8 +79,7 @@ export default {
variant="confirm"
:loading="isSaving"
:disabled="disableButtons"
- data-testid="save-button"
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
>
{{ __('Save changes') }}
</gl-button>
@@ -93,8 +92,7 @@ export default {
type="submit"
:loading="isSaving"
:disabled="disableButtons"
- data-testid="save-button"
- data-qa-selector="save_changes_button"
+ data-testid="save-changes-button"
@click.prevent="onSaveClick"
>
{{ __('Save changes') }}
diff --git a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
index c1c09cfa3d6..8ee63ca4818 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_issues_fields.vue
@@ -86,7 +86,7 @@ export default {
<gl-form-checkbox
v-model="enableJiraIssues"
:disabled="checkboxDisabled"
- data-qa-selector="service_jira_issues_enabled_checkbox"
+ data-testid="jira-issues-enabled-checkbox"
>
{{ $options.i18n.enableCheckboxLabel }}
<template #help>
@@ -107,7 +107,7 @@ export default {
id="service_project_key"
v-model="projectKey"
name="service[project_key]"
- data-qa-selector="service_jira_project_key_field"
+ data-testid="jira-project-key-field"
:placeholder="$options.i18n.projectKeyPlaceholder"
:required="enableJiraIssues"
:state="validProjectKey"
diff --git a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
index 034867f8b5f..e987cf49997 100644
--- a/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
+++ b/app/assets/javascripts/integrations/edit/components/jira_trigger_fields.vue
@@ -201,7 +201,7 @@ export default {
<gl-form-checkbox
v-model="issueTransitionEnabled"
:disabled="isInheriting"
- data-qa-selector="service_jira_issue_transition_enabled_checkbox"
+ data-testid="jira-issue-transition-enabled-checkbox"
>
{{ s__('JiraService|Enable Jira transitions') }}
</gl-form-checkbox>
@@ -219,7 +219,7 @@ export default {
name="service[jira_issue_transition_automatic]"
:value="issueTransitionOption.value"
:disabled="isInheriting"
- :data-qa-selector="`service_jira_issue_transition_automatic_${issueTransitionOption.value}_radio`"
+ :data-testid="`jira-issue-transition-automatic-${issueTransitionOption.value}-radio`"
>
{{ issueTransitionOption.label }}
@@ -229,7 +229,7 @@ export default {
name="service[jira_issue_transition_id]"
type="text"
class="gl-my-3"
- data-qa-selector="service_jira_issue_transition_id_field"
+ data-testid="jira-issue-transition-id-field"
:placeholder="s__('JiraService|For example, 12, 24')"
:disabled="isInheriting"
:required="true"
diff --git a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
index 052e8d8488d..bd407d4456b 100644
--- a/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
+++ b/app/assets/javascripts/integrations/edit/components/sections/configuration.vue
@@ -34,7 +34,6 @@ export default {
:key="`${currentKey}-${field.name}`"
v-bind="field"
:is-validated="isValidated"
- :data-qa-selector="`${field.name}_div`"
/>
</div>
</template>
diff --git a/app/assets/javascripts/integrations/index/components/integrations_table.vue b/app/assets/javascripts/integrations/index/components/integrations_table.vue
index c94c509e811..8cfc3ea9098 100644
--- a/app/assets/javascripts/integrations/index/components/integrations_table.vue
+++ b/app/assets/javascripts/integrations/index/components/integrations_table.vue
@@ -117,11 +117,7 @@ export default {
</template>
<template #cell(title)="{ item }">
- <gl-avatar-link
- :href="item.edit_path"
- :title="item.title"
- :data-qa-selector="`${item.name}_link`"
- >
+ <gl-avatar-link :href="item.edit_path" :title="item.title" :data-testid="`${item.name}-link`">
<gl-avatar-labeled
:label="item.title"
:sub-label="item.description"
diff --git a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue
index 424a9d3fabd..b0cfe670edc 100644
--- a/app/assets/javascripts/invite_members/components/invite_group_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_group_trigger.vue
@@ -28,12 +28,7 @@ export default {
</script>
<template>
- <gl-button
- :class="classes"
- data-qa-selector="invite_a_group_button"
- data-test-id="invite-group-button"
- @click="openModal"
- >
+ <gl-button :class="classes" data-testid="invite-group-button" @click="openModal">
{{ displayText }}
</gl-button>
</template>
diff --git a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
index ceb9200dfad..9893572ae16 100644
--- a/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_groups_modal.vue
@@ -106,6 +106,9 @@ export default {
inviteDisabled() {
return Object.keys(this.groupToBeSharedWith).length === 0;
},
+ staticRoles() {
+ return { validRoles: this.accessLevels };
+ },
},
mounted() {
if (this.reloadPageOnSubmit) {
@@ -182,7 +185,7 @@ export default {
:modal-id="modalId"
:modal-title="$options.labels.title"
:name="name"
- :access-levels="accessLevels"
+ :access-levels="staticRoles"
:default-access-level="defaultAccessLevel"
:help-link="helpLink"
v-bind="$attrs"
@@ -194,6 +197,7 @@ export default {
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
:full-path="fullPath"
+ is-group-invite
@reset="resetFields"
@submit="sendInvite"
>
diff --git a/app/assets/javascripts/invite_members/components/invite_members_modal.vue b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
index 1a10130e969..dead90eeb71 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -85,6 +85,11 @@ export default {
type: Number,
required: true,
},
+ defaultMemberRoleId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
helpLink: {
type: String,
required: true,
@@ -128,8 +133,6 @@ export default {
invalidMembers: {},
source: 'unknown',
mode: 'default',
- // Kept in sync with "base"
- selectedAccessLevel: undefined,
errorsLimit: 2,
isErrorsSectionExpanded: false,
shouldShowEmptyInvitesAlert: false,
@@ -157,7 +160,7 @@ export default {
labelSearchField() {
return this.isEmailSignupEnabled
? this.$options.labels.searchField
- : s__('InviteMembersModal|Username');
+ : s__('InviteMembersModal|Username or name');
},
isEmptyInvites() {
return Boolean(this.newUsersToInvite.length);
@@ -183,6 +186,9 @@ export default {
showUserLimitNotification() {
return !isEmpty(this.usersLimitDataset.alertVariant);
},
+ staticRoles() {
+ return { validRoles: this.accessLevels };
+ },
limitVariant() {
return this.usersLimitDataset.alertVariant;
},
@@ -269,7 +275,7 @@ export default {
this.shouldShowEmptyInvitesAlert = true;
this.$refs.alerts.focus();
},
- getInvitePayload({ accessLevel, expiresAt }) {
+ getInvitePayload({ accessLevel, expiresAt, memberRoleId }) {
const [usersToInviteByEmail, usersToAddById] = this.partitionNewUsersToInvite();
const email = usersToInviteByEmail !== '' ? { email: usersToInviteByEmail } : {};
@@ -279,12 +285,13 @@ export default {
format: 'json',
expires_at: expiresAt,
access_level: accessLevel,
+ member_role_id: memberRoleId,
invite_source: this.source,
...email,
...userId,
};
},
- async sendInvite({ accessLevel, expiresAt }) {
+ async sendInvite({ accessLevel, expiresAt, memberRoleId }) {
this.isLoading = true;
this.clearValidation();
@@ -298,7 +305,7 @@ export default {
: Api.inviteGroupMembers.bind(Api);
try {
- const payload = this.getInvitePayload({ accessLevel, expiresAt });
+ const payload = this.getInvitePayload({ accessLevel, expiresAt, memberRoleId });
const response = await apiAddByInvite(this.id, payload);
const { error, message } = responseFromSuccess(response);
@@ -355,9 +362,6 @@ export default {
this.closeModal();
},
- onAccessLevelUpdate(val) {
- this.selectedAccessLevel = val;
- },
clearValidation() {
this.invalidFeedbackMessage = '';
this.invalidMembers = {};
@@ -382,14 +386,16 @@ export default {
:modal-id="modalId"
:modal-title="modalTitle"
:name="name"
- :access-levels="accessLevels"
+ :access-levels="staticRoles"
:default-access-level="defaultAccessLevel"
+ :default-member-role-id="defaultMemberRoleId"
:help-link="helpLink"
:label-intro-text="labelIntroText"
:label-search-field="labelSearchField"
:form-group-description="formGroupDescription"
:invalid-feedback-message="invalidFeedbackMessage"
:is-loading="isLoading"
+ :is-project="isProject"
:new-users-to-invite="newUsersToInvite"
:root-group-id="rootId"
:users-limit-dataset="usersLimitDataset"
@@ -398,7 +404,6 @@ export default {
@cancel="onCancel"
@reset="resetFields"
@submit="sendInvite"
- @access-level="onAccessLevelUpdate"
>
<template #intro-text-before>
<div v-if="isCelebration" class="gl-p-4 gl-font-size-h1">
@@ -511,6 +516,7 @@ export default {
:exception-state="exceptionState"
:users-filter="usersFilter"
:filter-id="filterId"
+ :root-group-id="rootId"
:invalid-members="invalidMembers"
@clear="clearValidation"
@token-remove="removeToken"
diff --git a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
index 6efb7a6cdf1..7f76b7ca1ac 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -4,7 +4,6 @@ import { s__ } from '~/locale';
import eventHub from '../event_hub';
import {
TRIGGER_ELEMENT_BUTTON,
- TRIGGER_DEFAULT_QA_SELECTOR,
TRIGGER_ELEMENT_WITH_EMOJI,
TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI,
TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN,
@@ -42,18 +41,12 @@ export default {
required: false,
default: 'button',
},
- qaSelector: {
- type: String,
- required: false,
- default: TRIGGER_DEFAULT_QA_SELECTOR,
- },
},
computed: {
componentAttributes() {
return {
class: this.classes,
- 'data-qa-selector': this.qaSelector,
- 'data-test-id': 'invite-members-button',
+ 'data-testid': 'invite-members-button',
};
},
item() {
diff --git a/app/assets/javascripts/invite_members/components/invite_modal_base.vue b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
index a14dcd38aa7..00b7c3f4bdd 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -1,7 +1,7 @@
<script>
import {
+ GlCollapsibleListbox,
GlFormGroup,
- GlFormSelect,
GlModal,
GlDatepicker,
GlLink,
@@ -12,6 +12,7 @@ import {
import Tracking from '~/tracking';
import { sprintf } from '~/locale';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
+import { initialSelectedRole, roleDropdownItems } from 'ee_else_ce/members/utils';
import {
ACCESS_LEVEL,
ACCESS_EXPIRE_DATE,
@@ -36,8 +37,8 @@ const DEFAULT_SLOTS = [
export default {
components: {
+ GlCollapsibleListbox,
GlFormGroup,
- GlFormSelect,
GlDatepicker,
GlLink,
GlModal,
@@ -68,6 +69,11 @@ export default {
type: Number,
required: true,
},
+ defaultMemberRoleId: {
+ type: Number,
+ required: false,
+ default: null,
+ },
helpLink: {
type: String,
required: true,
@@ -95,6 +101,11 @@ export default {
required: false,
default: false,
},
+ isLoadingRoles: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
invalidFeedbackMessage: {
type: String,
required: false,
@@ -134,14 +145,14 @@ export default {
data() {
// Be sure to check out reset!
return {
- selectedAccessLevel: this.defaultAccessLevel,
+ selectedAccessLevel: null,
selectedDate: undefined,
minDate: new Date(),
};
},
computed: {
- accessLevelsOptions() {
- return Object.entries(this.accessLevels).map(([text, value]) => ({ text, value }));
+ accessLevelOptions() {
+ return roleDropdownItems(this.accessLevels);
},
introText() {
return sprintf(this.labelIntroText, { name: this.name });
@@ -158,11 +169,6 @@ export default {
datepickerId() {
return `${this.modalId}_expires_at`;
},
- selectedRoleName() {
- return Object.keys(this.accessLevels).find(
- (key) => this.accessLevels[key] === Number(this.selectedAccessLevel),
- );
- },
contentSlots() {
return [...DEFAULT_SLOTS, ...(this.extraSlots || [])];
},
@@ -173,7 +179,6 @@ export default {
variant: 'confirm',
disabled: this.submitDisabled,
loading: this.isLoading,
- 'data-qa-selector': 'invite_button',
},
};
},
@@ -195,18 +200,16 @@ export default {
},
},
watch: {
- selectedAccessLevel: {
+ accessLevelOptions: {
immediate: true,
- handler(val) {
- this.$emit('access-level', val);
- },
+ handler: 'resetSelectedAccessLevel',
},
},
methods: {
onReset() {
// This component isn't necessarily disposed,
// so we might need to reset it's state.
- this.selectedAccessLevel = this.defaultAccessLevel;
+ this.resetSelectedAccessLevel();
this.selectedDate = undefined;
this.$emit('reset');
@@ -230,14 +233,27 @@ export default {
// We never want to hide when submitting
e.preventDefault();
+ const { accessLevel, memberRoleId } = this.accessLevelOptions.flatten.find(
+ (item) => item.value === this.selectedAccessLevel,
+ );
this.$emit('submit', {
- accessLevel: this.selectedAccessLevel,
+ accessLevel,
+ memberRoleId,
expiresAt: this.selectedDate,
});
},
onClose() {
this.$emit('close');
},
+ resetSelectedAccessLevel() {
+ const accessLevel = {
+ integerValue: this.defaultAccessLevel,
+ memberRoleId: this.defaultMemberRoleId,
+ };
+ this.selectedAccessLevel = initialSelectedRole(this.accessLevelOptions.flatten, {
+ accessLevel,
+ });
+ },
},
HEADER_CLOSE_LABEL,
ACCESS_EXPIRE_DATE,
@@ -308,11 +324,13 @@ export default {
</template>
</gl-sprintf>
</template>
- <gl-form-select
+ <gl-collapsible-listbox
:id="dropdownId"
v-model="selectedAccessLevel"
- data-qa-selector="access_level_dropdown"
- :options="accessLevelsOptions"
+ data-testid="access-level-dropdown"
+ :items="accessLevelOptions.formatted"
+ :loading="isLoadingRoles"
+ block
/>
</gl-form-group>
@@ -338,10 +356,10 @@ export default {
<template #modal-footer>
<div
- class="gl-m-0 gl-w-full gl-display-flex gl-xs-flex-direction-column! gl-flex-direction-row-reverse"
+ class="gl-m-0 gl-w-full gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row-reverse"
>
<gl-button
- class="gl-w-full gl-sm-w-auto gl-xs-mb-3! gl-sm-ml-3!"
+ class="gl-w-full gl-sm-w-auto gl-sm-ml-3!"
data-testid="invite-modal-submit"
v-bind="actionPrimary.attributes"
@click="onSubmit"
diff --git a/app/assets/javascripts/invite_members/components/members_token_select.vue b/app/assets/javascripts/invite_members/components/members_token_select.vue
index 0be04b7af35..0e3f2890b29 100644
--- a/app/assets/javascripts/invite_members/components/members_token_select.vue
+++ b/app/assets/javascripts/invite_members/components/members_token_select.vue
@@ -2,7 +2,9 @@
import { GlTokenSelector, GlAvatar, GlAvatarLabeled, GlIcon, GlSprintf } from '@gitlab/ui';
import { debounce, isEmpty } from 'lodash';
import { __ } from '~/locale';
-import { getUsers } from '~/rest_api';
+import { getUsers, getGroupUsers } from '~/rest_api';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { memberName } from '../utils/member_utils';
import {
SEARCH_DELAY,
@@ -20,6 +22,7 @@ export default {
GlIcon,
GlSprintf,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
canUseEmailToken: {
type: Boolean,
@@ -59,6 +62,10 @@ export default {
required: false,
default: '',
},
+ rootGroupId: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -67,8 +74,6 @@ export default {
originalInput: '',
users: [],
selectedTokens: [],
- hasBeenFocused: false,
- hideDropdownWithNoItems: true,
};
},
computed: {
@@ -89,9 +94,16 @@ export default {
},
queryOptions() {
if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID) {
+ if (!this.glFeatures.groupUserSaml) {
+ return {
+ saml_provider_id: this.filterId,
+ ...this.$options.defaultQueryOptions,
+ };
+ }
return {
- saml_provider_id: this.filterId,
- ...this.$options.defaultQueryOptions,
+ active: true,
+ include_saml_users: true,
+ include_service_accounts: true,
};
}
return this.$options.defaultQueryOptions;
@@ -102,7 +114,6 @@ export default {
textInputAttrs() {
return {
'data-testid': 'members-token-select-input',
- 'data-qa-selector': 'members_token_select_input',
id: this.inputId,
};
},
@@ -125,7 +136,6 @@ export default {
},
methods: {
handleTextInput(inputQuery) {
- this.hideDropdownWithNoItems = false;
this.originalInput = inputQuery;
this.query = inputQuery.trim();
this.loading = true;
@@ -137,19 +147,27 @@ export default {
class: this.tokenClass(token),
}));
},
- retrieveUsers: debounce(function debouncedRetrieveUsers() {
- return getUsers(this.query, this.queryOptions)
- .then((response) => {
- this.users = response.data.map((token) => ({
- id: token.id,
- name: token.name,
- username: token.username,
- avatar_url: token.avatar_url,
- }));
- })
- .finally(() => {
- this.loading = false;
- });
+ retrieveUsersRequest() {
+ if (this.usersFilter === USERS_FILTER_SAML_PROVIDER_ID && this.glFeatures.groupUserSaml) {
+ return getGroupUsers(this.query, this.rootGroupId, this.queryOptions);
+ }
+
+ return getUsers(this.query, this.queryOptions);
+ },
+ retrieveUsers: debounce(async function debouncedRetrieveUsers() {
+ try {
+ const { data } = await this.retrieveUsersRequest();
+ this.users = data.map((token) => ({
+ id: token.id,
+ name: token.name,
+ username: token.username,
+ avatar_url: token.avatar_url,
+ }));
+ } catch (error) {
+ Sentry.captureException(error);
+ }
+
+ this.loading = false;
}, SEARCH_DELAY),
tokenClass(token) {
if (this.hasError(token)) {
@@ -162,18 +180,10 @@ export default {
handleInput() {
this.$emit('input', this.selectedTokens);
},
- handleBlur() {
- this.hideDropdownWithNoItems = false;
- },
handleFocus() {
- // The modal auto-focuses on the input when opened.
- // This prevents the dropdown from opening when the modal opens.
- if (this.hasBeenFocused) {
- this.loading = true;
- this.retrieveUsers();
- }
-
- this.hasBeenFocused = true;
+ // Search for users when focused on the input
+ this.loading = true;
+ this.retrieveUsers();
},
handleTokenRemove(value) {
if (this.selectedTokens.length) {
@@ -184,6 +194,12 @@ export default {
this.$emit('clear');
},
+ handleTab(event) {
+ if (this.originalInput.length > 0) {
+ event.preventDefault();
+ this.$refs.tokenSelector.handleEnter();
+ }
+ },
hasError(token) {
return Object.keys(this.invalidMembers).includes(memberName(token));
},
@@ -197,20 +213,20 @@ export default {
<template>
<gl-token-selector
+ ref="tokenSelector"
v-model="selectedTokens"
:state="exceptionState"
:dropdown-items="users"
:loading="loading"
:allow-user-defined-tokens="emailIsValid"
- :hide-dropdown-with-no-items="hideDropdownWithNoItems"
:placeholder="placeholderText"
:aria-labelledby="ariaLabelledby"
:text-input-attrs="textInputAttrs"
- @blur="handleBlur"
@text-input="handleTextInput"
@input="handleInput"
@focus="handleFocus"
@token-remove="handleTokenRemove"
+ @keydown.tab="handleTab"
>
<template #token-content="{ token }">
<gl-icon
diff --git a/app/assets/javascripts/invite_members/constants.js b/app/assets/javascripts/invite_members/constants.js
index 93386e5504b..f2a6cccbe35 100644
--- a/app/assets/javascripts/invite_members/constants.js
+++ b/app/assets/javascripts/invite_members/constants.js
@@ -20,7 +20,6 @@ export const TRIGGER_ELEMENT_WITH_EMOJI = 'text-emoji';
export const TRIGGER_ELEMENT_DROPDOWN_WITH_EMOJI = 'dropdown-text-emoji';
export const TRIGGER_ELEMENT_DISCLOSURE_DROPDOWN = 'dropdown-text';
export const INVITE_MEMBER_MODAL_TRACKING_CATEGORY = 'invite_members_modal';
-export const TRIGGER_DEFAULT_QA_SELECTOR = 'invite_members_button';
export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_CATEGORY = 'invite_project_members_modal';
export const IMPORT_PROJECT_MEMBERS_MODAL_TRACKING_LABEL = 'project-members-page';
export const MEMBERS_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite members');
@@ -40,7 +39,7 @@ export const MEMBERS_TO_PROJECT_DEFAULT_INTRO_TEXT = s__(
export const MEMBERS_TO_PROJECT_CELEBRATE_INTRO_TEXT = s__(
"InviteMembersModal|Congratulations on creating your project, you're almost there!",
);
-export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username or email address');
+export const MEMBERS_SEARCH_FIELD = s__('InviteMembersModal|Username, name or email address');
export const MEMBERS_PLACEHOLDER = s__('InviteMembersModal|Select members or type email addresses');
export const GROUP_MODAL_DEFAULT_TITLE = s__('InviteMembersModal|Invite a group');
diff --git a/app/assets/javascripts/invite_members/init_invite_members_modal.js b/app/assets/javascripts/invite_members/init_invite_members_modal.js
index 8dfe697e2cb..bd291ecc90f 100644
--- a/app/assets/javascripts/invite_members/init_invite_members_modal.js
+++ b/app/assets/javascripts/invite_members/init_invite_members_modal.js
@@ -36,6 +36,7 @@ export default (function initInviteMembersModal() {
isProject: parseBoolean(el.dataset.isProject),
accessLevels: JSON.parse(el.dataset.accessLevels),
defaultAccessLevel: parseInt(el.dataset.defaultAccessLevel, 10),
+ defaultMemberRoleId: parseInt(el.dataset.defaultMemberRoleId, 10) || null,
usersFilter: el.dataset.usersFilter,
filterId: parseInt(el.dataset.filterId, 10),
usersLimitDataset: convertObjectPropsToCamelCase(
diff --git a/app/assets/javascripts/issuable/components/locked_badge.vue b/app/assets/javascripts/issuable/components/locked_badge.vue
index 652d02e8f9d..1bd399dc257 100644
--- a/app/assets/javascripts/issuable/components/locked_badge.vue
+++ b/app/assets/javascripts/issuable/components/locked_badge.vue
@@ -32,7 +32,7 @@ export default {
</script>
<template>
- <gl-badge v-gl-tooltip :title="title" variant="warning">
+ <gl-badge v-gl-tooltip :title="title" variant="warning" data-testid="locked-badge">
<gl-icon name="lock" />
<span class="gl-sr-only">{{ __('Locked') }}</span>
</gl-badge>
diff --git a/app/assets/javascripts/issuable/components/related_issuable_item.vue b/app/assets/javascripts/issuable/components/related_issuable_item.vue
index 126a3a84d66..09ecf9eb5bc 100644
--- a/app/assets/javascripts/issuable/components/related_issuable_item.vue
+++ b/app/assets/javascripts/issuable/components/related_issuable_item.vue
@@ -8,7 +8,7 @@ import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isMetaKey } from '~/lib/utils/common_utils';
import { setUrlParams, updateHistory } from '~/lib/utils/url_utility';
import { sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import relatedIssuableMixin from '../mixins/related_issuable_mixin';
diff --git a/app/assets/javascripts/issuable/issuable_label_selector.js b/app/assets/javascripts/issuable/issuable_label_selector.js
index 804f7384732..9065fc9c1fd 100644
--- a/app/assets/javascripts/issuable/issuable_label_selector.js
+++ b/app/assets/javascripts/issuable/issuable_label_selector.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
+import { parseBoolean } from '~/lib/utils/common_utils';
import { VARIANT_EMBEDDED } from '~/sidebar/components/labels/labels_select_widget/constants';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import IssuableLabelSelector from '~/vue_shared/issuable/create/components/issuable_label_selector.vue';
@@ -25,6 +26,7 @@ export default () => {
issuableType,
labelsFilterBasePath,
labelsManagePath,
+ supportsLockOnMerge,
} = el.dataset;
return new Vue({
@@ -40,6 +42,7 @@ export default () => {
fullPath,
initialLabels: JSON.parse(initialLabels),
issuableType,
+ issuableSupportsLockOnMerge: parseBoolean(supportsLockOnMerge),
labelType: WORKSPACE_PROJECT,
labelsFilterBasePath,
labelsManagePath,
diff --git a/app/assets/javascripts/issuable/popover/components/mr_popover.vue b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
index 80ae8ed8cf6..6c207d21ff2 100644
--- a/app/assets/javascripts/issuable/popover/components/mr_popover.vue
+++ b/app/assets/javascripts/issuable/popover/components/mr_popover.vue
@@ -2,7 +2,7 @@
import { GlBadge, GlPopover, GlSkeletonLoader } from '@gitlab/ui';
import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
import { __ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import query from '../queries/merge_request.query.graphql';
diff --git a/app/assets/javascripts/issues/constants.js b/app/assets/javascripts/issues/constants.js
index 3d8017e6e07..0a762b161ef 100644
--- a/app/assets/javascripts/issues/constants.js
+++ b/app/assets/javascripts/issues/constants.js
@@ -6,6 +6,7 @@ export const STATUS_MERGED = 'merged';
export const STATUS_OPEN = 'opened';
export const STATUS_REOPENED = 'reopened';
export const STATUS_LOCKED = 'locked';
+export const STATUS_EMPTY = 'empty';
export const TITLE_LENGTH_MAX = 255;
diff --git a/app/assets/javascripts/issues/dashboard/index.js b/app/assets/javascripts/issues/dashboard/index.js
index 74633b251b2..30cc1c5b822 100644
--- a/app/assets/javascripts/issues/dashboard/index.js
+++ b/app/assets/javascripts/issues/dashboard/index.js
@@ -23,6 +23,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature,
+ hasIssueDateFilterFeature,
hasIssueWeightsFeature,
hasScopedLabelsFeature,
initialSort,
@@ -47,6 +48,7 @@ export async function mountIssuesDashboardApp() {
emptyStateWithoutFilterSvgPath,
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
+ hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
hasScopedLabelsFeature: parseBoolean(hasScopedLabelsFeature),
initialSort,
diff --git a/app/assets/javascripts/issues/index.js b/app/assets/javascripts/issues/index.js
index eea5207801c..b7b39d0ce08 100644
--- a/app/assets/javascripts/issues/index.js
+++ b/app/assets/javascripts/issues/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { initIssuableSidebar } from '~/issuable';
@@ -22,7 +23,7 @@ export function initForm() {
new IssuableForm($('.issue-form')); // eslint-disable-line no-new
IssuableLabelSelector();
new LabelsSelect(); // eslint-disable-line no-new
- new ShortcutsNavigation(); // eslint-disable-line no-new
+ addShortcutsExtension(ShortcutsNavigation);
initTitleSuggestions();
initTypePopover();
@@ -32,7 +33,7 @@ export function initForm() {
export function initShow() {
new Issue(); // eslint-disable-line no-new
- new ShortcutsIssuable(); // eslint-disable-line no-new
+ addShortcutsExtension(ShortcutsIssuable);
new ZenMode(); // eslint-disable-line no-new
initAwardsApp(document.getElementById('js-vue-awards-block'));
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 72bb88ef1d5..adc789a205b 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -107,6 +107,7 @@ import {
getInitialPageParams,
getSortKey,
getSortOptions,
+ groupMultiSelectFilterTokens,
isSortKey,
mapWorkItemWidgetsToIssueFields,
updateUpvotesCount,
@@ -384,6 +385,7 @@ export default {
isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-author`,
preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
},
{
type: TOKEN_TYPE_ASSIGNEE,
@@ -396,6 +398,7 @@ export default {
isProject: this.isProject,
recentSuggestionsStorageKey: `${this.fullPath}-issues-recent-tokens-assignee`,
preloadedUsers,
+ multiSelect: this.glFeatures.groupMultiSelectTokens,
},
{
type: TOKEN_TYPE_MILESTONE,
@@ -803,7 +806,12 @@ export default {
sortKey = defaultSortKey;
}
- this.filterTokens = getFilterTokens(window.location.search);
+ const tokens = getFilterTokens(window.location.search);
+ if (this.glFeatures.groupMultiSelectTokens) {
+ this.filterTokens = groupMultiSelectFilterTokens(tokens, this.searchTokens);
+ } else {
+ this.filterTokens = tokens;
+ }
this.exportCsvPathWithQuery = this.getExportCsvPathWithQuery();
this.pageParams = getInitialPageParams(
diff --git a/app/assets/javascripts/issues/list/constants.js b/app/assets/javascripts/issues/list/constants.js
index 682c7629962..d6ff5c952c2 100644
--- a/app/assets/javascripts/issues/list/constants.js
+++ b/app/assets/javascripts/issues/list/constants.js
@@ -164,6 +164,12 @@ export const specialFilterValues = [
FILTER_STARTED,
];
+export const TYPE_TOKEN_EPIC_OPTION = {
+ icon: 'epic',
+ title: __('Epic'),
+ value: 'epic',
+};
+
export const TYPE_TOKEN_OBJECTIVE_OPTION = {
icon: 'issue-type-objective',
title: s__('WorkItem|Objective'),
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 8c60ad6dc4e..5a836e3e40a 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -70,6 +70,7 @@ export async function mountIssuesListApp() {
hasAnyIssues,
hasAnyProjects,
hasBlockedIssuesFeature,
+ hasEpicsFeature,
hasIssuableHealthStatusFeature,
hasIssueDateFilterFeature,
hasIssueWeightsFeature,
@@ -127,6 +128,7 @@ export async function mountIssuesListApp() {
hasAnyIssues: parseBoolean(hasAnyIssues),
hasAnyProjects: parseBoolean(hasAnyProjects),
hasBlockedIssuesFeature: parseBoolean(hasBlockedIssuesFeature),
+ hasEpicsFeature: parseBoolean(hasEpicsFeature),
hasIssuableHealthStatusFeature: parseBoolean(hasIssuableHealthStatusFeature),
hasIssueDateFilterFeature: parseBoolean(hasIssueDateFilterFeature),
hasIssueWeightsFeature: parseBoolean(hasIssueWeightsFeature),
diff --git a/app/assets/javascripts/issues/list/utils.js b/app/assets/javascripts/issues/list/utils.js
index 37df0c8f9ff..c1e10285a92 100644
--- a/app/assets/javascripts/issues/list/utils.js
+++ b/app/assets/javascripts/issues/list/utils.js
@@ -7,6 +7,7 @@ import {
OPERATOR_NOT,
OPERATOR_OR,
OPERATOR_AFTER,
+ OPERATORS_TO_GROUP,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -233,6 +234,41 @@ export const getFilterTokens = (locationSearch) =>
};
});
+export function groupMultiSelectFilterTokens(filterTokensToGroup, tokenDefs) {
+ const groupedTokens = [];
+
+ const multiSelectTokenTypes = tokenDefs.filter((t) => t.multiSelect).map((t) => t.type);
+
+ filterTokensToGroup.forEach((token) => {
+ const shouldGroup =
+ OPERATORS_TO_GROUP.includes(token.value.operator) &&
+ multiSelectTokenTypes.includes(token.type);
+
+ if (!shouldGroup) {
+ groupedTokens.push(token);
+ return;
+ }
+
+ const sameTypeAndOperator = (t) =>
+ t.type === token.type && t.value.operator === token.value.operator;
+ const existingToken = groupedTokens.find(sameTypeAndOperator);
+
+ if (!existingToken) {
+ groupedTokens.push({
+ ...token,
+ value: {
+ ...token.value,
+ data: [token.value.data],
+ },
+ });
+ } else if (!existingToken.value.data.includes(token.value.data)) {
+ existingToken.value.data.push(token.value.data);
+ }
+ });
+
+ return groupedTokens;
+}
+
export const isNotEmptySearchToken = (token) =>
!(token.type === FILTERED_SEARCH_TERM && !token.value.data);
diff --git a/app/assets/javascripts/issues/show/components/app.vue b/app/assets/javascripts/issues/show/components/app.vue
index 756585683c8..27646df506b 100644
--- a/app/assets/javascripts/issues/show/components/app.vue
+++ b/app/assets/javascripts/issues/show/components/app.vue
@@ -22,6 +22,8 @@ import PinnedLinks from './pinned_links.vue';
import StickyHeader from './sticky_header.vue';
import TitleComponent from './title.vue';
+const STICKY_HEADER_VISIBLE_CLASS = 'issuable-sticky-header-visible';
+
export default {
components: {
HeaderActions,
@@ -322,6 +324,7 @@ export default {
eventHub.$off('close.form', this.closeForm);
eventHub.$off('open.form', this.openForm);
window.removeEventListener('beforeunload', this.handleBeforeUnloadEvent);
+ this.hideStickyHeader();
},
methods: {
handleBeforeUnloadEvent(e) {
@@ -472,6 +475,8 @@ export default {
hideStickyHeader() {
this.isStickyHeaderShowing = false;
+
+ document.body.classList?.remove(STICKY_HEADER_VISIBLE_CLASS);
},
showStickyHeader() {
@@ -479,6 +484,8 @@ export default {
if (this.$refs.title.$el.offsetTop < window.pageYOffset) {
this.isStickyHeaderShowing = true;
}
+
+ document.body.classList?.add(STICKY_HEADER_VISIBLE_CLASS);
},
handleSaveDescription(description) {
@@ -555,7 +562,7 @@ export default {
<slot name="header">
<issue-header
- class="gl-p-0 gl-mt-2 gl-sm-mt-0"
+ class="gl-p-0 gl-mt-2"
:class="headerClasses"
:author="author"
:confidential="isConfidential"
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index 32df19dfe44..dcdfd06fbf1 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -142,8 +142,8 @@ export default {
deleteButtonText() {
return sprintf(__('Delete %{issuableType}'), { issuableType: this.issueTypeText });
},
- qaSelector() {
- return this.isClosed ? 'reopen_issue_button' : 'close_issue_button';
+ testId() {
+ return this.isClosed ? 'reopen-issue-button' : 'close-issue-button';
},
dropdownText() {
return sprintf(__('%{issueType} actions'), {
@@ -308,7 +308,7 @@ export default {
<template>
<div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-sm-gap-3">
- <div class="gl-sm-display-none! w-100">
+ <div class="gl-md-display-none! gl-w-full">
<gl-disclosure-dropdown
v-if="hasMobileDropdown"
ref="issuableActionsDropdownMobile"
@@ -320,7 +320,7 @@ export default {
:loading="isToggleStateButtonLoading"
placement="right"
>
- <template v-if="showMovedSidebarOptions">
+ <template v-if="showMovedSidebarOptions && !glFeatures.notificationsTodosButtons">
<sidebar-subscriptions-widget
:iid="String(iid)"
:full-path="fullPath"
@@ -340,7 +340,7 @@ export default {
</gl-disclosure-dropdown-item>
<gl-disclosure-dropdown-item
v-if="showToggleIssueStateButton"
- :data-testid="`mobile_${qaSelector}`"
+ :data-testid="`mobile-${testId}`"
@action="toggleIssueState"
>
<template #list-item>{{ buttonText }}</template>
@@ -352,7 +352,7 @@ export default {
<template v-if="isMrSidebarMoved">
<gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
+ class="js-copy-reference"
data-testid="copy-reference"
@action="copyReference"
><template #list-item>{{
@@ -398,7 +398,7 @@ export default {
v-gl-tooltip.bottom
:title="$options.i18n.editTitleAndDescription"
:aria-label="$options.i18n.editTitleAndDescription"
- class="js-issuable-edit gl-display-none! gl-sm-display-block!"
+ class="js-issuable-edit gl-display-none! gl-md-display-block!"
data-testid="edit-button"
@click="edit"
>
@@ -410,7 +410,7 @@ export default {
id="new-actions-header-dropdown"
ref="issuableActionsDropdownDesktop"
v-gl-tooltip.hover
- class="gl-display-none gl-sm-display-inline-flex!"
+ class="gl-display-none gl-md-display-inline-flex!"
icon="ellipsis_v"
category="tertiary"
placement="left"
@@ -453,7 +453,7 @@ export default {
<template v-if="isMrSidebarMoved">
<gl-disclosure-dropdown-item
:data-clipboard-text="issuableReference"
- button-class="js-copy-reference"
+ class="js-copy-reference"
data-testid="copy-reference"
@action="copyReference"
><template #list-item>{{
diff --git a/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue b/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue
index d509f0dbc09..4b046b990aa 100644
--- a/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/highlight_bar.vue
@@ -39,7 +39,7 @@ export default {
<template>
<div
v-show="showHighlightBar"
- class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-xs-flex-direction-column"
+ class="gl-border-solid gl-border-1 gl-border-gray-100 gl-p-5 gl-mb-3 gl-rounded-base gl-display-flex gl-justify-content-space-between gl-flex-direction-column gl-sm-flex-direction-row"
>
<div v-if="alert" class="gl-mr-3">
<span class="gl-font-weight-bold">{{ s__('HighlightBar|Original alert:') }}</span>
diff --git a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
index 2909a4d2666..c84fba23837 100644
--- a/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
+++ b/app/assets/javascripts/issues/show/components/incidents/incident_tabs.vue
@@ -133,8 +133,7 @@ export default {
item.classList.toggle('gl-display-none', !isSummaryTab);
});
- editButton?.classList.toggle('gl-display-none', !isSummaryTab);
- editButton?.classList.toggle('gl-sm-display-inline-flex!', isSummaryTab);
+ editButton?.classList.toggle('gl-md-display-block!', isSummaryTab);
}
},
},
diff --git a/app/assets/javascripts/issues/show/components/title.vue b/app/assets/javascripts/issues/show/components/title.vue
index 375180446d9..fef08ca22cf 100644
--- a/app/assets/javascripts/issues/show/components/title.vue
+++ b/app/assets/javascripts/issues/show/components/title.vue
@@ -54,7 +54,7 @@ export default {
<template>
<div
- class="gl-display-flex gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
+ class="gl-display-flex gl-align-items-flex-start gl-flex-direction-column gl-md-flex-direction-row gl-gap-3 gl-pt-3"
>
<h1
v-safe-html="titleHtml"
diff --git a/app/assets/javascripts/jira_connect/subscriptions/components/feedback_banner.vue b/app/assets/javascripts/jira_connect/subscriptions/components/feedback_banner.vue
index 5d6117b836d..352d729794a 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/components/feedback_banner.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/components/feedback_banner.vue
@@ -1,6 +1,6 @@
<script>
import { GlBanner } from '@gitlab/ui';
-import ChatBubbleSvg from '@gitlab/svgs/dist/illustrations/chat-bubble-sm.svg?url';
+import ChatBubbleSvg from '@gitlab/svgs/dist/illustrations/chat-sm.svg?url';
import { s__, __ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
diff --git a/app/assets/javascripts/jira_connect/subscriptions/constants.js b/app/assets/javascripts/jira_connect/subscriptions/constants.js
index 85e250b14a0..b26f65616bb 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/constants.js
+++ b/app/assets/javascripts/jira_connect/subscriptions/constants.js
@@ -46,6 +46,12 @@ export const OAUTH_SELF_MANAGED_DOC_LINK = helpPagePath('administration/settings
export const SET_UP_INSTANCE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'set-up-your-instance',
});
+export const JIRA_USER_REQUIREMENTS_DOC_LINK = helpPagePath(
+ 'administration/settings/jira_cloud_app',
+ {
+ anchor: 'jira-user-requirements',
+ },
+);
export const FAILED_TO_UPDATE_DOC_LINK = helpPagePath('administration/settings/jira_cloud_app', {
anchor: 'failed-to-update-the-gitlab-instance',
});
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/dot_com_alert.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/dot_com_alert.vue
new file mode 100644
index 00000000000..4bb3c8b58e5
--- /dev/null
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/dot_com_alert.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlAlert, GlLink, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { JIRA_USER_REQUIREMENTS_DOC_LINK } from '~/jira_connect/subscriptions/constants';
+
+export default {
+ components: {
+ GlAlert,
+ GlLink,
+ GlSprintf,
+ },
+ i18n: {
+ title: s__('JiraConnect|Are you a Jira administrator?'),
+ body: s__(
+ 'JiraConnect|To complete the setup, you must meet %{linkStart}certain user requirements%{linkEnd} in Jira.',
+ ),
+ },
+ JIRA_USER_REQUIREMENTS_DOC_LINK,
+};
+</script>
+
+<template>
+ <gl-alert variant="warning" :title="$options.i18n.title" :dismissible="false">
+ <div>
+ <gl-sprintf :message="$options.i18n.body">
+ <template #link="{ content }">
+ <gl-link :href="$options.JIRA_USER_REQUIREMENTS_DOC_LINK" target="_blank">
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </gl-alert>
+</template>
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
index 9f8fae5b476..75f3ff936bd 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/setup_instructions.vue
@@ -5,6 +5,7 @@ import {
PREREQUISITES_DOC_LINK,
OAUTH_SELF_MANAGED_DOC_LINK,
SET_UP_INSTANCE_DOC_LINK,
+ JIRA_USER_REQUIREMENTS_DOC_LINK,
} from '~/jira_connect/subscriptions/constants';
export default {
@@ -31,6 +32,11 @@ export default {
link: SET_UP_INSTANCE_DOC_LINK,
checked: false,
},
+ {
+ name: s__('JiraConnect|Jira user requirements'),
+ link: JIRA_USER_REQUIREMENTS_DOC_LINK,
+ checked: false,
+ },
],
};
},
@@ -46,11 +52,7 @@ export default {
<div class="gl-mt-5">
<h3>{{ s__('JiraConnect|Continue setup in GitLab') }}</h3>
<p>
- {{
- s__(
- 'JiraConnect|In order to complete the set up, you’ll need to complete a few steps in GitLab:',
- )
- }}
+ {{ s__('JiraConnect|To complete the setup, you must follow a few steps in GitLab:') }}
</p>
<div class="gl-mb-5">
<div v-for="step in requiredSteps" :key="step.name" class="gl-mb-2">
diff --git a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
index d3770cc310a..28bf974b8f1 100644
--- a/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
+++ b/app/assets/javascripts/jira_connect/subscriptions/pages/sign_in/sign_in_gitlab_multiversion/version_select_form.vue
@@ -11,6 +11,7 @@ import { __, s__ } from '~/locale';
import { GITLAB_COM_BASE_PATH } from '~/jira_connect/subscriptions/constants';
import SelfManagedAlert from './self_managed_alert.vue';
+import DotComAlert from './dot_com_alert.vue';
import SetupInstructions from './setup_instructions.vue';
const RADIO_OPTIONS = {
@@ -30,6 +31,7 @@ export default {
GlFormRadio,
GlButton,
SelfManagedAlert,
+ DotComAlert,
SetupInstructions,
},
props: {
@@ -113,6 +115,7 @@ export default {
</gl-form-radio>
</gl-form-radio-group>
<self-managed-alert v-if="isSelfManagedSelected" />
+ <dot-com-alert v-else />
<div class="gl-display-flex gl-justify-content-end gl-mt-5">
<gl-button variant="confirm" type="submit" :loading="loading" data-testid="submit-button">{{
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/page_title.vue b/app/assets/javascripts/kubernetes_dashboard/components/page_title.vue
new file mode 100644
index 00000000000..0f0fbd61798
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/page_title.vue
@@ -0,0 +1,28 @@
+<script>
+import { GlIcon, GlSprintf } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ i18n: {
+ agentTitle: s__('KubernetesDashboard|Agent %{name} ID #%{id}'),
+ },
+ components: {
+ GlSprintf,
+ GlIcon,
+ },
+ inject: ['agent'],
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-align-items-center">
+ <h1 class="gl-heading-2"><slot></slot></h1>
+ <div class="gl-ml-4 gl-mb-5">
+ <gl-icon name="kubernetes-agent" class="gl-text-gray-500" />
+ <gl-sprintf :message="$options.i18n.agentTitle">
+ <template #name> {{ agent.name }} </template>
+ <template #id>{{ agent.id }}</template>
+ </gl-sprintf>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
new file mode 100644
index 00000000000..0d219f915c9
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
@@ -0,0 +1,81 @@
+<script>
+import { GlBadge, GlTruncate } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { WORKLOAD_STATUS_BADGE_VARIANTS } from '../constants';
+import WorkloadDetailsItem from './workload_details_item.vue';
+
+export default {
+ components: {
+ GlBadge,
+ GlTruncate,
+ WorkloadDetailsItem,
+ },
+ props: {
+ item: {
+ type: Object,
+ required: true,
+ validator: (item) =>
+ ['name', 'kind', 'labels', 'annotations', 'status'].every((key) => item[key]),
+ },
+ },
+ computed: {
+ itemLabels() {
+ const { labels } = this.item;
+ return Object.entries(labels).map(this.getLabelBadgeText);
+ },
+ itemAnnotations() {
+ const { annotations } = this.item;
+ return Object.entries(annotations).map(this.getAnnotationsText);
+ },
+ },
+ methods: {
+ getLabelBadgeText([key, value]) {
+ return `${key}=${value}`;
+ },
+
+ getAnnotationsText([key, value]) {
+ return `${key}: ${value}`;
+ },
+ },
+ i18n: {
+ name: s__('KubernetesDashboard|Name'),
+ kind: s__('KubernetesDashboard|Kind'),
+ labels: s__('KubernetesDashboard|Labels'),
+ status: s__('KubernetesDashboard|Status'),
+ annotations: s__('KubernetesDashboard|Annotations'),
+ },
+ WORKLOAD_STATUS_BADGE_VARIANTS,
+};
+</script>
+
+<template>
+ <ul class="gl-list-style-none">
+ <workload-details-item :label="$options.i18n.name">
+ {{ item.name }}
+ </workload-details-item>
+ <workload-details-item :label="$options.i18n.kind">
+ {{ item.kind }}
+ </workload-details-item>
+ <workload-details-item v-if="itemLabels.length" :label="$options.i18n.labels">
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2">
+ <gl-badge v-for="label of itemLabels" :key="label" class="gl-max-w-full">
+ <gl-truncate :text="label" with-tooltip />
+ </gl-badge>
+ </div>
+ </workload-details-item>
+ <workload-details-item :label="$options.i18n.status">
+ <gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{
+ item.status
+ }}</gl-badge></workload-details-item
+ >
+ <workload-details-item v-if="itemAnnotations.length" :label="$options.i18n.annotations">
+ <p
+ v-for="annotation of itemAnnotations"
+ :key="annotation"
+ class="gl-mb-2 gl-overflow-wrap-anywhere"
+ >
+ {{ annotation }}
+ </p>
+ </workload-details-item>
+ </ul>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details_item.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details_item.vue
new file mode 100644
index 00000000000..2ac748418ff
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details_item.vue
@@ -0,0 +1,19 @@
+<script>
+export default {
+ props: {
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="gl-line-height-20 gl-py-3 gl-border-b-solid gl-border-b-2 gl-border-b-gray-100">
+ <label class="gl-font-weight-bold gl-mb-2"> {{ label }} </label>
+ <div class="gl-text-gray-500 gl-mb-0">
+ <slot></slot>
+ </div>
+ </li>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
new file mode 100644
index 00000000000..8c6a08ad504
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
@@ -0,0 +1,78 @@
+<script>
+import { GlLoadingIcon, GlAlert, GlDrawer } from '@gitlab/ui';
+import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import WorkloadStats from './workload_stats.vue';
+import WorkloadTable from './workload_table.vue';
+import WorkloadDetails from './workload_details.vue';
+
+export default {
+ components: {
+ GlLoadingIcon,
+ GlAlert,
+ GlDrawer,
+ WorkloadStats,
+ WorkloadTable,
+ WorkloadDetails,
+ },
+ props: {
+ loading: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ errorMessage: {
+ type: String,
+ default: '',
+ required: false,
+ },
+ stats: {
+ type: Array,
+ required: true,
+ },
+ items: {
+ type: Array,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ showDetailsDrawer: false,
+ selectedItem: {},
+ };
+ },
+ methods: {
+ closeDetailsDrawer() {
+ this.showDetailsDrawer = false;
+ },
+ onItemSelect(item) {
+ this.selectedItem = item;
+ this.showDetailsDrawer = true;
+ },
+ },
+ DRAWER_Z_INDEX,
+};
+</script>
+<template>
+ <gl-loading-icon v-if="loading" />
+ <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false" class="gl-mb-5">
+ {{ errorMessage }}
+ </gl-alert>
+ <div v-else>
+ <workload-stats :stats="stats" />
+ <workload-table :items="items" @select-item="onItemSelect" />
+
+ <gl-drawer
+ :open="showDetailsDrawer"
+ header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
+ :z-index="$options.DRAWER_Z_INDEX"
+ @close="closeDetailsDrawer"
+ >
+ <template #title>
+ <h4 class="gl-font-weight-bold gl-font-size-h2 gl-m-0">{{ selectedItem.name }}</h4>
+ </template>
+ <template #default>
+ <workload-details :item="selectedItem" />
+ </template>
+ </gl-drawer>
+ </div>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue
new file mode 100644
index 00000000000..31b931e1855
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_stats.vue
@@ -0,0 +1,27 @@
+<script>
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
+
+export default {
+ components: {
+ GlSingleStat,
+ },
+ props: {
+ stats: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-flex-wrap gl-sm-flex-nowrap gl-mx-n3 gl-mt-n3">
+ <gl-single-stat
+ v-for="(stat, index) in stats"
+ :key="index"
+ class="gl-w-full gl-flex-direction-column gl-align-items-center gl-justify-content-center gl-bg-white gl-border gl-border-gray-a-08 gl-mx-3 gl-p-3 gl-mt-3"
+ :value="stat.value"
+ :title="stat.title"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue
new file mode 100644
index 00000000000..d3704863538
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue
@@ -0,0 +1,85 @@
+<script>
+import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
+import {
+ WORKLOAD_STATUS_BADGE_VARIANTS,
+ PAGE_SIZE,
+ TABLE_HEADING_CLASSES,
+ DEFAULT_WORKLOAD_TABLE_FIELDS,
+} from '../constants';
+
+export default {
+ components: {
+ GlTable,
+ GlBadge,
+ GlPagination,
+ },
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ fields: {
+ type: Array,
+ default: () => DEFAULT_WORKLOAD_TABLE_FIELDS,
+ required: false,
+ },
+ },
+ data() {
+ return {
+ currentPage: 1,
+ };
+ },
+ computed: {
+ tableFields() {
+ return this.fields.map((field) => {
+ return {
+ ...field,
+ thClass: TABLE_HEADING_CLASSES,
+ sortable: true,
+ };
+ });
+ },
+ },
+ methods: {
+ selectItem(item) {
+ this.$emit('select-item', item);
+ },
+ },
+ PAGE_SIZE,
+ WORKLOAD_STATUS_BADGE_VARIANTS,
+ TABLE_CELL_CLASSES: 'gl-p-2',
+};
+</script>
+
+<template>
+ <div class="gl-mt-8">
+ <gl-table
+ :items="items"
+ :fields="tableFields"
+ :per-page="$options.PAGE_SIZE"
+ :current-page="currentPage"
+ tbody-tr-class="gl-hover-cursor-pointer"
+ stacked="md"
+ bordered
+ hover
+ @row-clicked="selectItem"
+ >
+ <template #cell(status)="{ item: { status } }">
+ <gl-badge
+ :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[status]"
+ size="sm"
+ class="gl-ml-2"
+ >{{ status }}</gl-badge
+ >
+ </template>
+ </gl-table>
+
+ <gl-pagination
+ v-model="currentPage"
+ :per-page="$options.PAGE_SIZE"
+ :total-items="items.length"
+ align="center"
+ class="gl-mt-6"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js
new file mode 100644
index 00000000000..b93740aec90
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/constants.js
@@ -0,0 +1,49 @@
+import { s__ } from '~/locale';
+
+export const STATUS_RUNNING = 'Running';
+export const STATUS_PENDING = 'Pending';
+export const STATUS_SUCCEEDED = 'Succeeded';
+export const STATUS_FAILED = 'Failed';
+export const STATUS_READY = 'Ready';
+
+export const STATUS_LABELS = {
+ [STATUS_RUNNING]: s__('KubernetesDashboard|Running'),
+ [STATUS_PENDING]: s__('KubernetesDashboard|Pending'),
+ [STATUS_SUCCEEDED]: s__('KubernetesDashboard|Succeeded'),
+ [STATUS_FAILED]: s__('KubernetesDashboard|Failed'),
+ [STATUS_READY]: s__('KubernetesDashboard|Ready'),
+};
+
+export const WORKLOAD_STATUS_BADGE_VARIANTS = {
+ [STATUS_RUNNING]: 'info',
+ [STATUS_PENDING]: 'warning',
+ [STATUS_SUCCEEDED]: 'success',
+ [STATUS_FAILED]: 'danger',
+ [STATUS_READY]: 'success',
+};
+
+export const PAGE_SIZE = 20;
+
+export const TABLE_HEADING_CLASSES = 'gl-bg-gray-50! gl-font-weight-bold gl-white-space-nowrap';
+
+export const DEFAULT_WORKLOAD_TABLE_FIELDS = [
+ {
+ key: 'name',
+ label: s__('KubernetesDashboard|Name'),
+ },
+ {
+ key: 'status',
+ label: s__('KubernetesDashboard|Status'),
+ },
+ {
+ key: 'namespace',
+ label: s__('KubernetesDashboard|Namespace'),
+ },
+ {
+ key: 'age',
+ label: s__('KubernetesDashboard|Age'),
+ },
+];
+
+export const STATUS_TRUE = 'True';
+export const STATUS_FALSE = 'False';
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
new file mode 100644
index 00000000000..5894472d83b
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
@@ -0,0 +1,108 @@
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
+import typeDefs from '~/environments/graphql/typedefs.graphql';
+import k8sPodsQuery from './queries/k8s_dashboard_pods.query.graphql';
+import k8sDeploymentsQuery from './queries/k8s_dashboard_deployments.query.graphql';
+import k8sStatefulSetsQuery from './queries/k8s_dashboard_stateful_sets.query.graphql';
+import k8sReplicaSetsQuery from './queries/k8s_dashboard_replica_sets.query.graphql';
+import k8sDaemonSetsQuery from './queries/k8s_dashboard_daemon_sets.query.graphql';
+import { resolvers } from './resolvers';
+
+export const apolloProvider = () => {
+ const defaultClient = createDefaultClient(resolvers, {
+ typeDefs,
+ });
+ const { cache } = defaultClient;
+
+ cache.writeQuery({
+ query: k8sPodsQuery,
+ data: {
+ metadata: {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ },
+ status: {
+ phase: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sDeploymentsQuery,
+ data: {
+ metadata: {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ },
+ status: {
+ conditions: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sStatefulSetsQuery,
+ data: {
+ metadata: {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ },
+ status: {
+ readyReplicas: null,
+ },
+ spec: {
+ replicas: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sReplicaSetsQuery,
+ data: {
+ metadata: {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ },
+ status: {
+ readyReplicas: null,
+ },
+ spec: {
+ replicas: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sDaemonSetsQuery,
+ data: {
+ metadata: {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ },
+ status: {
+ numberMisscheduled: null,
+ numberReady: null,
+ desiredNumberScheduled: null,
+ },
+ },
+ });
+
+ return new VueApollo({
+ defaultClient,
+ });
+};
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
new file mode 100644
index 00000000000..47c2f543357
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
@@ -0,0 +1,116 @@
+import { CoreV1Api, Configuration, WatchApi, EVENT_DATA } from '@gitlab/cluster-client';
+
+export const handleClusterError = async (err) => {
+ if (!err.response) {
+ throw err;
+ }
+
+ const errorData = await err.response.json();
+ throw errorData;
+};
+
+export const buildWatchPath = ({ resource, api = 'api/v1', namespace = '' }) => {
+ return namespace ? `/${api}/namespaces/${namespace}/${resource}` : `/${api}/${resource}`;
+};
+
+export const mapWorkloadItem = (item) => {
+ if (item.metadata) {
+ const metadata = {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ };
+ return { status: item.status, metadata };
+ }
+ return { status: item.status };
+};
+
+export const mapSetItem = (item) => {
+ const status = {
+ ...item.status,
+ readyReplicas: item.status?.readyReplicas || null,
+ };
+
+ const metadata =
+ {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ } || null;
+
+ const spec = item.spec || null;
+
+ return { status, metadata, spec };
+};
+
+export const watchWorkloadItems = ({
+ client,
+ query,
+ configuration,
+ namespace,
+ watchPath,
+ queryField,
+ mapFn = mapWorkloadItem,
+}) => {
+ const config = new Configuration(configuration);
+ const watcherApi = new WatchApi(config);
+
+ watcherApi
+ .subscribeToStream(watchPath, { watch: true })
+ .then((watcher) => {
+ let result = [];
+
+ watcher.on(EVENT_DATA, (data) => {
+ result = data.map(mapFn);
+
+ client.writeQuery({
+ query,
+ variables: { configuration, namespace },
+ data: { [queryField]: result },
+ });
+ });
+ })
+ .catch((err) => {
+ handleClusterError(err);
+ });
+};
+
+export const getK8sPods = ({
+ client,
+ query,
+ configuration,
+ namespace = '',
+ enableWatch = false,
+}) => {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
+ const podsApi = namespace
+ ? coreV1Api.listCoreV1NamespacedPod({ namespace })
+ : coreV1Api.listCoreV1PodForAllNamespaces();
+
+ return podsApi
+ .then((res) => {
+ if (enableWatch) {
+ const watchPath = buildWatchPath({ resource: 'pods', namespace });
+ watchWorkloadItems({
+ client,
+ query,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sPods',
+ });
+ }
+
+ const data = res?.items || [];
+ return data.map(mapWorkloadItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+};
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql
new file mode 100644
index 00000000000..4469c7a161a
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_daemon_sets.query.graphql
@@ -0,0 +1,16 @@
+query getK8sDashboardDaemonSets($configuration: LocalConfiguration) {
+ k8sDaemonSets(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ numberMisscheduled
+ numberReady
+ desiredNumberScheduled
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql
new file mode 100644
index 00000000000..21172bbbeb0
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_deployments.query.graphql
@@ -0,0 +1,14 @@
+query getK8sDashboardDeployments($configuration: LocalConfiguration) {
+ k8sDeployments(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ conditions
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql
new file mode 100644
index 00000000000..3f8eabca03e
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_pods.query.graphql
@@ -0,0 +1,14 @@
+query getK8sDashboardPods($configuration: LocalConfiguration) {
+ k8sPods(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ phase
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql
new file mode 100644
index 00000000000..38aaa79853c
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_replica_sets.query.graphql
@@ -0,0 +1,17 @@
+query getK8sDashboardReplicaSets($configuration: LocalConfiguration) {
+ k8sReplicaSets(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ readyReplicas
+ }
+ spec {
+ replicas
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql
new file mode 100644
index 00000000000..ab1b9e1e472
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_stateful_sets.query.graphql
@@ -0,0 +1,17 @@
+query getK8sDashboardStatefulSets($configuration: LocalConfiguration) {
+ k8sStatefulSets(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ readyReplicas
+ }
+ spec {
+ replicas
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js
new file mode 100644
index 00000000000..b99ffff5bd1
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers.js
@@ -0,0 +1,7 @@
+import kubernetesQueries from './resolvers/kubernetes';
+
+export const resolvers = {
+ Query: {
+ ...kubernetesQueries,
+ },
+};
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
new file mode 100644
index 00000000000..e59bed5581b
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
@@ -0,0 +1,169 @@
+import { Configuration, AppsV1Api } from '@gitlab/cluster-client';
+
+import {
+ getK8sPods,
+ handleClusterError,
+ mapWorkloadItem,
+ mapSetItem,
+ buildWatchPath,
+ watchWorkloadItems,
+} from '../helpers/resolver_helpers';
+import k8sDashboardPodsQuery from '../queries/k8s_dashboard_pods.query.graphql';
+import k8sDashboardDeploymentsQuery from '../queries/k8s_dashboard_deployments.query.graphql';
+import k8sDashboardStatefulSetsQuery from '../queries/k8s_dashboard_stateful_sets.query.graphql';
+import k8sDashboardReplicaSetsQuery from '../queries/k8s_dashboard_replica_sets.query.graphql';
+import k8sDaemonSetsQuery from '../queries/k8s_dashboard_daemon_sets.query.graphql';
+
+export default {
+ k8sPods(_, { configuration }, { client }) {
+ const query = k8sDashboardPodsQuery;
+ const enableWatch = true;
+ return getK8sPods({ client, query, configuration, enableWatch });
+ },
+
+ k8sDeployments(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const appsV1api = new AppsV1Api(config);
+ const deploymentsApi = namespace
+ ? appsV1api.listAppsV1NamespacedDeployment({ namespace })
+ : appsV1api.listAppsV1DeploymentForAllNamespaces();
+ return deploymentsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'deployments',
+ api: 'apis/apps/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sDashboardDeploymentsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sDeployments',
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapWorkloadItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+
+ k8sStatefulSets(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const appsV1api = new AppsV1Api(config);
+ const deploymentsApi = namespace
+ ? appsV1api.listAppsV1NamespacedStatefulSet({ namespace })
+ : appsV1api.listAppsV1StatefulSetForAllNamespaces();
+ return deploymentsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'statefulsets',
+ api: 'apis/apps/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sDashboardStatefulSetsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sStatefulSets',
+ mapFn: mapSetItem,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapSetItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+
+ k8sReplicaSets(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const appsV1api = new AppsV1Api(config);
+ const deploymentsApi = namespace
+ ? appsV1api.listAppsV1NamespacedReplicaSet({ namespace })
+ : appsV1api.listAppsV1ReplicaSetForAllNamespaces();
+ return deploymentsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'replicasets',
+ api: 'apis/apps/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sDashboardReplicaSetsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sReplicaSets',
+ mapFn: mapSetItem,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapSetItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+
+ k8sDaemonSets(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const appsV1api = new AppsV1Api(config);
+ const deploymentsApi = namespace
+ ? appsV1api.listAppsV1NamespacedDaemonSet({ namespace })
+ : appsV1api.listAppsV1DaemonSetForAllNamespaces();
+ return deploymentsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'daemonsets',
+ api: 'apis/apps/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sDaemonSetsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sDaemonSets',
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapWorkloadItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+};
diff --git a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
new file mode 100644
index 00000000000..24f43e21506
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
@@ -0,0 +1,60 @@
+import { differenceInSeconds } from '~/lib/utils/datetime_utility';
+import {
+ STATUS_TRUE,
+ STATUS_FALSE,
+ STATUS_PENDING,
+ STATUS_READY,
+ STATUS_FAILED,
+} from '../constants';
+
+export function getAge(creationTimestamp) {
+ if (!creationTimestamp) return '';
+
+ const timeDifference = differenceInSeconds(new Date(creationTimestamp), new Date());
+
+ const seconds = Math.floor(timeDifference);
+ const minutes = Math.floor(seconds / 60) % 60;
+ const hours = Math.floor(seconds / 60 / 60) % 24;
+ const days = Math.floor(seconds / 60 / 60 / 24);
+
+ let ageString;
+ if (days > 0) {
+ ageString = `${days}d`;
+ } else if (hours > 0) {
+ ageString = `${hours}h`;
+ } else if (minutes > 0) {
+ ageString = `${minutes}m`;
+ } else {
+ ageString = `${seconds}s`;
+ }
+
+ return ageString;
+}
+
+export function calculateDeploymentStatus(item) {
+ const [available, progressing] = item.status?.conditions ?? [];
+ if (available?.status === STATUS_TRUE) {
+ return STATUS_READY;
+ }
+ if (available?.status === STATUS_FALSE && progressing?.status !== STATUS_TRUE) {
+ return STATUS_FAILED;
+ }
+ return STATUS_PENDING;
+}
+
+export function calculateStatefulSetStatus(item) {
+ if (item.status?.readyReplicas === item.spec?.replicas) {
+ return STATUS_READY;
+ }
+ return STATUS_FAILED;
+}
+
+export function calculateDaemonSetStatus(item) {
+ if (
+ item.status?.numberReady === item.status?.desiredNumberScheduled &&
+ !item.status?.numberMisscheduled
+ ) {
+ return STATUS_READY;
+ }
+ return STATUS_FAILED;
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js b/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js
new file mode 100644
index 00000000000..6c8e8a2eb31
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/init_kubernetes_dashboard.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility';
+import csrf from '~/lib/utils/csrf';
+import { apolloProvider as createApolloProvider } from './graphql/client';
+import App from './pages/app.vue';
+import createRouter from './router/index';
+
+Vue.use(VueApollo);
+
+const initKubernetesDashboard = () => {
+ const el = document.querySelector('.js-kubernetes-app');
+
+ if (!el) {
+ return null;
+ }
+
+ const { basePath, agent, kasTunnelUrl } = el.dataset;
+ const agentObject = JSON.parse(agent);
+
+ const configuration = {
+ basePath: removeLastSlashInUrlPath(kasTunnelUrl),
+ headers: {
+ 'GitLab-Agent-Id': agentObject.id,
+ 'Content-Type': 'application/json',
+ ...csrf.headers,
+ },
+ credentials: 'include',
+ };
+
+ const router = createRouter({
+ base: basePath,
+ });
+
+ return new Vue({
+ el,
+ name: 'KubernetesDashboardRoot',
+ router,
+ apolloProvider: createApolloProvider(),
+ provide: {
+ agent: agentObject,
+ configuration,
+ },
+ render: (createElement) => createElement(App),
+ });
+};
+
+export { initKubernetesDashboard };
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/app.vue b/app/assets/javascripts/kubernetes_dashboard/pages/app.vue
new file mode 100644
index 00000000000..e135afed9fe
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/app.vue
@@ -0,0 +1,13 @@
+<script>
+import PageTitle from '../components/page_title.vue';
+
+export default {
+ components: { PageTitle },
+};
+</script>
+<template>
+ <div class="gl-mt-5">
+ <page-title> {{ $route.meta.title }} </page-title>
+ <router-view />
+ </div>
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/daemon_sets_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/daemon_sets_page.vue
new file mode 100644
index 00000000000..bdde4e89f34
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/daemon_sets_page.vue
@@ -0,0 +1,80 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateDaemonSetStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sDaemonSetsQuery from '../graphql/queries/k8s_dashboard_daemon_sets.query.graphql';
+import { STATUS_FAILED, STATUS_READY, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sDaemonSets: {
+ query: k8sDaemonSetsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sDaemonSets?.map((daemonSet) => {
+ return {
+ name: daemonSet.metadata?.name,
+ namespace: daemonSet.metadata?.namespace,
+ status: calculateDaemonSetStatus(daemonSet),
+ age: getAge(daemonSet.metadata?.creationTimestamp),
+ labels: daemonSet.metadata?.labels,
+ annotations: daemonSet.metadata?.annotations,
+ kind: s__('KubernetesDashboard|DaemonSet'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sDaemonSets: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ daemonSetsStats() {
+ return [
+ {
+ value: this.countDaemonSetsByStatus(STATUS_READY),
+ title: STATUS_LABELS[STATUS_READY],
+ },
+ {
+ value: this.countDaemonSetsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sDaemonSets.loading;
+ },
+ },
+ methods: {
+ countDaemonSetsByStatus(status) {
+ const filteredDaemonSets = this.k8sDaemonSets.filter((item) => item.status === status) || [];
+
+ return filteredDaemonSets.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="daemonSetsStats"
+ :items="k8sDaemonSets"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/deployments_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/deployments_page.vue
new file mode 100644
index 00000000000..c5472966539
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/deployments_page.vue
@@ -0,0 +1,84 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateDeploymentStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sDeploymentsQuery from '../graphql/queries/k8s_dashboard_deployments.query.graphql';
+import { STATUS_FAILED, STATUS_READY, STATUS_PENDING, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sDeployments: {
+ query: k8sDeploymentsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sDeployments?.map((deployment) => {
+ return {
+ name: deployment.metadata?.name,
+ namespace: deployment.metadata?.namespace,
+ status: calculateDeploymentStatus(deployment),
+ age: getAge(deployment.metadata?.creationTimestamp),
+ labels: deployment.metadata?.labels,
+ annotations: deployment.metadata?.annotations,
+ kind: s__('KubernetesDashboard|Deployment'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sDeployments: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ deploymentsStats() {
+ return [
+ {
+ value: this.countDeploymentsByStatus(STATUS_READY),
+ title: STATUS_LABELS[STATUS_READY],
+ },
+ {
+ value: this.countDeploymentsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ {
+ value: this.countDeploymentsByStatus(STATUS_PENDING),
+ title: STATUS_LABELS[STATUS_PENDING],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sDeployments.loading;
+ },
+ },
+ methods: {
+ countDeploymentsByStatus(phase) {
+ const filteredDeployments = this.k8sDeployments.filter((item) => item.status === phase) || [];
+
+ return filteredDeployments.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="deploymentsStats"
+ :items="k8sDeployments"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue
new file mode 100644
index 00000000000..4be40fdde62
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/pods_page.vue
@@ -0,0 +1,94 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sPodsQuery from '../graphql/queries/k8s_dashboard_pods.query.graphql';
+import {
+ STATUS_RUNNING,
+ STATUS_PENDING,
+ STATUS_SUCCEEDED,
+ STATUS_FAILED,
+ STATUS_LABELS,
+} from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sPods: {
+ query: k8sPodsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sPods?.map((pod) => {
+ return {
+ name: pod.metadata?.name,
+ namespace: pod.metadata?.namespace,
+ status: pod.status.phase,
+ age: getAge(pod.metadata?.creationTimestamp),
+ labels: pod.metadata?.labels,
+ annotations: pod.metadata?.annotations,
+ kind: s__('KubernetesDashboard|Pod'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sPods: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ podStats() {
+ return [
+ {
+ value: this.countPodsByPhase(STATUS_RUNNING),
+ title: STATUS_LABELS[STATUS_RUNNING],
+ },
+ {
+ value: this.countPodsByPhase(STATUS_PENDING),
+ title: STATUS_LABELS[STATUS_PENDING],
+ },
+ {
+ value: this.countPodsByPhase(STATUS_SUCCEEDED),
+ title: STATUS_LABELS[STATUS_SUCCEEDED],
+ },
+ {
+ value: this.countPodsByPhase(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo?.queries?.k8sPods?.loading;
+ },
+ },
+ methods: {
+ countPodsByPhase(phase) {
+ const filteredPods = this.k8sPods?.filter((item) => item.status === phase) || [];
+
+ return filteredPods.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="podStats"
+ :items="k8sPods"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/replica_sets_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/replica_sets_page.vue
new file mode 100644
index 00000000000..212cc0dbaf7
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/replica_sets_page.vue
@@ -0,0 +1,80 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateStatefulSetStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sReplicaSetsQuery from '../graphql/queries/k8s_dashboard_replica_sets.query.graphql';
+import { STATUS_FAILED, STATUS_READY, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sReplicaSets: {
+ query: k8sReplicaSetsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sReplicaSets?.map((replicaSet) => {
+ return {
+ name: replicaSet.metadata?.name,
+ namespace: replicaSet.metadata?.namespace,
+ status: calculateStatefulSetStatus(replicaSet),
+ age: getAge(replicaSet.metadata?.creationTimestamp),
+ labels: replicaSet.metadata?.labels,
+ annotations: replicaSet.metadata?.annotations,
+ kind: s__('KubernetesDashboard|ReplicaSet'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sReplicaSets: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ replicaSetsStats() {
+ return [
+ {
+ value: this.countReplicaSetsByStatus(STATUS_READY),
+ title: STATUS_LABELS[STATUS_READY],
+ },
+ {
+ value: this.countReplicaSetsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sReplicaSets.loading;
+ },
+ },
+ methods: {
+ countReplicaSetsByStatus(phase) {
+ const filteredReplicaSets = this.k8sReplicaSets.filter((item) => item.status === phase) || [];
+
+ return filteredReplicaSets.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="replicaSetsStats"
+ :items="k8sReplicaSets"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/stateful_sets_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/stateful_sets_page.vue
new file mode 100644
index 00000000000..bcdce41b433
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/stateful_sets_page.vue
@@ -0,0 +1,81 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateStatefulSetStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sStatefulSetsQuery from '../graphql/queries/k8s_dashboard_stateful_sets.query.graphql';
+import { STATUS_FAILED, STATUS_READY, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sStatefulSets: {
+ query: k8sStatefulSetsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sStatefulSets?.map((statefulSet) => {
+ return {
+ name: statefulSet.metadata?.name,
+ namespace: statefulSet.metadata?.namespace,
+ status: calculateStatefulSetStatus(statefulSet),
+ age: getAge(statefulSet.metadata?.creationTimestamp),
+ labels: statefulSet.metadata?.labels,
+ annotations: statefulSet.metadata?.annotations,
+ kind: s__('KubernetesDashboard|StatefulSet'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sStatefulSets: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ statefulSetsStats() {
+ return [
+ {
+ value: this.countStatefulSetsByStatus(STATUS_READY),
+ title: STATUS_LABELS[STATUS_READY],
+ },
+ {
+ value: this.countStatefulSetsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sStatefulSets.loading;
+ },
+ },
+ methods: {
+ countStatefulSetsByStatus(phase) {
+ const filteredStatefulSets =
+ this.k8sStatefulSets.filter((item) => item.status === phase) || [];
+
+ return filteredStatefulSets.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="statefulSetsStats"
+ :items="k8sStatefulSets"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/constants.js b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
new file mode 100644
index 00000000000..700f501ade4
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
@@ -0,0 +1,11 @@
+export const PODS_ROUTE_NAME = 'pods';
+export const DEPLOYMENTS_ROUTE_NAME = 'deployments';
+export const STATEFUL_SETS_ROUTE_NAME = 'statefulSets';
+export const REPLICA_SETS_ROUTE_NAME = 'replicaSets';
+export const DAEMON_SETS_ROUTE_NAME = 'daemonSets';
+
+export const PODS_ROUTE_PATH = '/pods';
+export const DEPLOYMENTS_ROUTE_PATH = '/deployments';
+export const STATEFUL_SETS_ROUTE_PATH = '/statefulsets';
+export const REPLICA_SETS_ROUTE_PATH = '/replicasets';
+export const DAEMON_SETS_ROUTE_PATH = '/daemonsets';
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/index.js b/app/assets/javascripts/kubernetes_dashboard/router/index.js
new file mode 100644
index 00000000000..7f59f850f3f
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/router/index.js
@@ -0,0 +1,15 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import routes from './routes';
+
+Vue.use(VueRouter);
+
+export default function createRouter({ base }) {
+ const router = new VueRouter({
+ base,
+ mode: 'history',
+ routes,
+ });
+
+ return router;
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/routes.js b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
new file mode 100644
index 00000000000..a1684a62ca4
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
@@ -0,0 +1,61 @@
+import { s__ } from '~/locale';
+import PodsPage from '../pages/pods_page.vue';
+import DeploymentsPage from '../pages/deployments_page.vue';
+import StatefulSetsPage from '../pages/stateful_sets_page.vue';
+import ReplicaSetsPage from '../pages/replica_sets_page.vue';
+import DaemonSetsPage from '../pages/daemon_sets_page.vue';
+import {
+ PODS_ROUTE_NAME,
+ PODS_ROUTE_PATH,
+ DEPLOYMENTS_ROUTE_NAME,
+ DEPLOYMENTS_ROUTE_PATH,
+ STATEFUL_SETS_ROUTE_NAME,
+ STATEFUL_SETS_ROUTE_PATH,
+ REPLICA_SETS_ROUTE_NAME,
+ REPLICA_SETS_ROUTE_PATH,
+ DAEMON_SETS_ROUTE_NAME,
+ DAEMON_SETS_ROUTE_PATH,
+} from './constants';
+
+export default [
+ {
+ name: PODS_ROUTE_NAME,
+ path: PODS_ROUTE_PATH,
+ component: PodsPage,
+ meta: {
+ title: s__('KubernetesDashboard|Pods'),
+ },
+ },
+ {
+ name: DEPLOYMENTS_ROUTE_NAME,
+ path: DEPLOYMENTS_ROUTE_PATH,
+ component: DeploymentsPage,
+ meta: {
+ title: s__('KubernetesDashboard|Deployments'),
+ },
+ },
+ {
+ name: STATEFUL_SETS_ROUTE_NAME,
+ path: STATEFUL_SETS_ROUTE_PATH,
+ component: StatefulSetsPage,
+ meta: {
+ title: s__('KubernetesDashboard|StatefulSets'),
+ },
+ },
+ {
+ name: REPLICA_SETS_ROUTE_NAME,
+ path: REPLICA_SETS_ROUTE_PATH,
+ component: ReplicaSetsPage,
+ meta: {
+ title: s__('KubernetesDashboard|ReplicaSets'),
+ },
+ },
+ {
+ name: DAEMON_SETS_ROUTE_NAME,
+ path: DAEMON_SETS_ROUTE_PATH,
+ component: DaemonSetsPage,
+ meta: {
+ title: s__('KubernetesDashboard|DaemonSets'),
+ },
+ },
+];
diff --git a/app/assets/javascripts/language_switcher/components/app.vue b/app/assets/javascripts/language_switcher/components/app.vue
index a2012f95fd6..f00290d0e46 100644
--- a/app/assets/javascripts/language_switcher/components/app.vue
+++ b/app/assets/javascripts/language_switcher/components/app.vue
@@ -52,10 +52,7 @@ export default {
@select="onLanguageSelected"
>
<template #list-item="{ item: locale }">
- <span
- :data-testid="itemTestSelector(locale.value)"
- :data-qa-selector="itemTestSelector(locale.value)"
- >
+ <span :data-testid="itemTestSelector(locale.value)">
{{ locale.text }}
</span>
</template>
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 670170ec9b9..1f58065a505 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,7 +1,4 @@
import $ from 'jquery';
-import ContextualSidebar from './contextual_sidebar';
-import initFlyOutNav from './fly_out_nav';
-import { setNotification } from './whats_new/utils/notification';
function hideEndFade($scrollingTabs) {
$scrollingTabs.each(function scrollTabsLoop() {
@@ -88,36 +85,11 @@ function initInviteMembers() {
.catch(() => {});
}
-function initWhatsNewComponent() {
- const appEl = document.getElementById('whats-new-app');
- if (!appEl) return;
-
- setNotification(appEl);
-
- const triggerEl = document.querySelector('.js-whats-new-trigger');
- if (!triggerEl) return;
-
- triggerEl.addEventListener('click', () => {
- import(/* webpackChunkName: 'whatsNewApp' */ '~/whats_new')
- .then(({ default: initWhatsNew }) => {
- initWhatsNew(appEl);
- })
- .catch(() => {});
- });
-}
-
function initDeferred() {
initScrollingTabs();
- initWhatsNewComponent();
initInviteMembers();
}
export default function initLayoutNav() {
- if (!gon.use_new_navigation) {
- const contextualSidebar = new ContextualSidebar();
- contextualSidebar.bindEvents();
- initFlyOutNav();
- }
-
requestIdleCallback(initDeferred);
}
diff --git a/app/assets/javascripts/lib/graphql.js b/app/assets/javascripts/lib/graphql.js
index 5285fa363a5..7ae78eb72c9 100644
--- a/app/assets/javascripts/lib/graphql.js
+++ b/app/assets/javascripts/lib/graphql.js
@@ -1,4 +1,5 @@
import { ApolloClient, InMemoryCache, ApolloLink, HttpLink } from '@apollo/client/core';
+import { BatchHttpLink } from '@apollo/client/link/batch-http';
import { createUploadLink } from 'apollo-upload-client';
import { persistCache } from 'apollo3-cache-persist';
import ActionCableLink from '~/actioncable_link';
@@ -64,8 +65,7 @@ export const typePolicies = {
};
export const stripWhitespaceFromQuery = (url, path) => {
- /* eslint-disable-next-line no-unused-vars */
- const [_, params] = url.split(path);
+ const [, params] = url.split(path);
if (!params) {
return url;
@@ -159,7 +159,15 @@ function createApolloClient(resolvers = {}, config = {}) {
return fetch(stripWhitespaceFromQuery(url, uri), options);
};
- const requestLink = new HttpLink({ ...httpOptions, fetch: fetchIntervention });
+ const requestLink = ApolloLink.split(
+ (operation) => operation.getContext().batchKey,
+ new BatchHttpLink({
+ ...httpOptions,
+ batchKey: (operation) => operation.getContext().batchKey,
+ fetch: fetchIntervention,
+ }),
+ new HttpLink({ ...httpOptions, fetch: fetchIntervention }),
+ );
const uploadsLink = ApolloLink.split(
(operation) => operation.getContext().hasUpload,
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 27da2ac6ce1..674a901aebc 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -82,7 +82,7 @@ export const handleLocationHash = () => {
const fixedTabs = document.querySelector('.js-tabs-affix');
const fixedDiffStats = document.querySelector('.js-diff-files-changed');
- const fixedNav = document.querySelector('.navbar-gitlab');
+ const headerLoggedOut = document.querySelector('.header-logged-out');
const fixedTopBar = document.querySelector('.top-bar-fixed');
const performanceBar = document.querySelector('#js-peek');
const topPadding = 8;
@@ -91,7 +91,7 @@ export const handleLocationHash = () => {
let adjustment = 0;
- adjustment -= getElementOffsetHeight(fixedNav);
+ adjustment -= getElementOffsetHeight(headerLoggedOut);
adjustment -= getElementOffsetHeight(fixedTabs);
adjustment -= getElementOffsetHeight(fixedDiffStats);
adjustment -= getElementOffsetHeight(fixedTopBar);
@@ -153,7 +153,7 @@ export const contentTop = () => {
const isDesktop = breakpointInstance.isDesktop();
const heightCalculators = [
() => getOuterHeight('#js-peek'),
- () => getOuterHeight('.navbar-gitlab'),
+ () => getOuterHeight('.header-logged-out'),
() => getOuterHeight('.top-bar-fixed'),
({ desktop }) => {
const mrStickyHeader = document.querySelector('.merge-request-sticky-header');
@@ -176,25 +176,13 @@ export const contentTop = () => {
() => getOuterHeight('.js-diff-files-changed'),
({ desktop }) => {
const diffsTabIsActive = window.mrTabs?.currentAction === 'diffs';
- let size;
-
- if (desktop && diffsTabIsActive) {
- size = getOuterHeight(
- '.diffs .diff-file .file-title-flex-parent:not([style="display:none"])',
- );
- }
-
- return size;
- },
- ({ desktop }) => {
- let size;
-
- if (desktop) {
- size = getOuterHeight('.mr-version-controls');
- }
-
- return size;
+ const isDiscussionScroll =
+ desktop && diffsTabIsActive && window.location.hash.startsWith('#note');
+ return isDiscussionScroll
+ ? getOuterHeight('.diffs .diff-file .file-title-flex-parent:not([style="display:none"])')
+ : 0;
},
+ ({ desktop }) => (desktop ? getOuterHeight('.mr-version-controls') : 0),
];
return heightCalculators.reduce((totalHeight, calculator) => {
@@ -385,8 +373,8 @@ export const buildUrlWithCurrentLocation = (param) => {
*
* @param {String} param
*/
-export const historyPushState = (newUrl) => {
- window.history.pushState({}, document.title, newUrl);
+export const historyPushState = (newUrl, state = {}) => {
+ window.history.pushState(state, document.title, newUrl);
};
/**
@@ -752,3 +740,12 @@ export const isDefaultCiConfig = (path) => {
export const hasCiConfigExtension = (path) => {
return CI_CONFIG_PATH_EXTENSION.test(path);
};
+
+/**
+ * Checks if an element with position:sticky is stuck
+ *
+ * @param el
+ * @returns {boolean}
+ */
+export const isElementStuck = (el) =>
+ el.getBoundingClientRect().top <= parseInt(getComputedStyle(el).top, 10);
diff --git a/app/assets/javascripts/lib/utils/datetime/constants.js b/app/assets/javascripts/lib/utils/datetime/constants.js
deleted file mode 100644
index 869ade45ebd..00000000000
--- a/app/assets/javascripts/lib/utils/datetime/constants.js
+++ /dev/null
@@ -1,7 +0,0 @@
-// Keys for the memoized Intl dateTime formatters
-export const DATE_WITH_TIME_FORMAT = 'DATE_WITH_TIME_FORMAT';
-export const DATE_ONLY_FORMAT = 'DATE_ONLY_FORMAT';
-
-export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT;
-
-export const DATE_TIME_FORMATS = [DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT];
diff --git a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
index 4e0d19f2c2a..6484fcff769 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_calculation_utility.js
@@ -413,6 +413,15 @@ export const nYearsAfter = (date, numberOfYears) => {
};
/**
+ * Returns the date `n` years before the date provided.
+ *
+ * @param {Date} date the initial date
+ * @param {Number} numberOfYears number of years before
+ * @return {Date} A `Date` object `n` years before the provided `Date`
+ */
+export const nYearsBefore = (date, numberOfYears) => nYearsAfter(date, -numberOfYears);
+
+/**
* Returns the date after the date provided
*
* @param {Date} date the initial date
diff --git a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
index c4b8f95e99f..7eb9c0f4518 100644
--- a/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/date_format_utility.js
@@ -377,28 +377,50 @@ export const dateToTimeInputValue = (date) => {
export const formatTimeAsSummary = ({ seconds, hours, days, minutes, weeks, months }) => {
if (months) {
- return sprintf(s__('ValueStreamAnalytics|%{value}M'), {
- value: roundToNearestHalf(months),
- });
+ const value = roundToNearestHalf(months);
+ return sprintf(
+ n__('ValueStreamAnalytics|%{value} month', 'ValueStreamAnalytics|%{value} months', value),
+ {
+ value,
+ },
+ );
}
if (weeks) {
- return sprintf(s__('ValueStreamAnalytics|%{value}w'), {
- value: roundToNearestHalf(weeks),
- });
+ const value = roundToNearestHalf(weeks);
+ return sprintf(
+ n__('ValueStreamAnalytics|%{value} week', 'ValueStreamAnalytics|%{value} weeks', value),
+ {
+ value,
+ },
+ );
}
if (days) {
- return sprintf(s__('ValueStreamAnalytics|%{value}d'), {
- value: roundToNearestHalf(days),
- });
+ const value = roundToNearestHalf(days);
+ return sprintf(
+ n__('ValueStreamAnalytics|%{value} day', 'ValueStreamAnalytics|%{value} days', value),
+ {
+ value,
+ },
+ );
}
if (hours) {
- return sprintf(s__('ValueStreamAnalytics|%{value}h'), { value: hours });
+ return sprintf(
+ n__('ValueStreamAnalytics|%{value} hour', 'ValueStreamAnalytics|%{value} hours', hours),
+ {
+ value: hours,
+ },
+ );
}
if (minutes) {
- return sprintf(s__('ValueStreamAnalytics|%{value}m'), { value: minutes });
+ return sprintf(
+ n__('ValueStreamAnalytics|%{value} minute', 'ValueStreamAnalytics|%{value} minutes', minutes),
+ {
+ value: minutes,
+ },
+ );
}
if (seconds) {
- return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1m'), { ALLOWED_TAGS: [] }));
+ return unescape(sanitize(s__('ValueStreamAnalytics|&lt;1 minute'), { ALLOWED_TAGS: [] }));
}
return '-';
};
diff --git a/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
new file mode 100644
index 00000000000..a4d911a6699
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/datetime/locale_dateformat.js
@@ -0,0 +1,273 @@
+import { createDateTimeFormat } from '~/locale';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asDateTime}
+ *
+ * Note: In case you can use localeDateFormat.asDateTime directly, please do that.
+ *
+ * @example
+ * localeDateFormat[DATE_WITH_TIME_FORMAT].format(date) // returns 'Jul 6, 2020, 2:43 PM'
+ * localeDateFormat[DATE_WITH_TIME_FORMAT].formatRange(date, date) // returns 'Jul 6, 2020, 2:45PM – 8:43 PM'
+ */
+export const DATE_WITH_TIME_FORMAT = 'asDateTime';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asDateTimeFull}
+ *
+ * Note: In case you can use localeDateFormat.asDateTimeFull directly, please do that.
+ *
+ * @example
+ * localeDateFormat[DATE_TIME_FULL_FORMAT].format(date) // returns 'July 6, 2020 at 2:43:12 PM GMT'
+ */
+export const DATE_TIME_FULL_FORMAT = 'asDateTimeFull';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asDate}
+ *
+ * Note: In case you can use localeDateFormat.asDate directly, please do that.
+ *
+ * @example
+ * localeDateFormat[DATE_ONLY_FORMAT].format(date) // returns 'Jul 05, 2023'
+ * localeDateFormat[DATE_ONLY_FORMAT].formatRange(date, date) // returns 'Jul 05 - Jul 07, 2023'
+ */
+export const DATE_ONLY_FORMAT = 'asDate';
+
+/**
+ * Format a Date with the help of {@link DateTimeFormat.asTime}
+ *
+ * Note: In case you can use localeDateFormat.asTime directly, please do that.
+ *
+ * @example
+ * localeDateFormat[TIME_ONLY_FORMAT].format(date) // returns '2:43'
+ * localeDateFormat[TIME_ONLY_FORMAT].formatRange(date, date) // returns '2:43 - 6:27 PM'
+ */
+export const TIME_ONLY_FORMAT = 'asTime';
+export const DEFAULT_DATE_TIME_FORMAT = DATE_WITH_TIME_FORMAT;
+export const DATE_TIME_FORMATS = [
+ DATE_WITH_TIME_FORMAT,
+ DATE_TIME_FULL_FORMAT,
+ DATE_ONLY_FORMAT,
+ TIME_ONLY_FORMAT,
+];
+
+/**
+ * The DateTimeFormat utilities support formatting a number of types,
+ * essentially anything you might use in the `Date` constructor.
+ *
+ * The reason for this is mostly backwards compatibility, as dateformat did the same
+ * https://github.com/felixge/node-dateformat/blob/c53e475891130a1fecd3b0d9bc5ebf3820b31b44/src/dateformat.js#L37-L41
+ *
+ * @typedef {Date|number|string|null} Dateish
+ *
+ */
+/**
+ * @typedef {Object} DateTimeFormatter
+ * @property {function(Dateish): string} format
+ * Formats a single {@link Dateish}
+ * with {@link Intl.DateTimeFormat.format}
+ * @property {function(Dateish, Dateish): string} formatRange
+ * Formats two {@link Dateish} as a range
+ * with {@link Intl.DateTimeFormat.formatRange}
+ */
+
+class DateTimeFormat {
+ #formatters = {};
+
+ /**
+ * Locale aware formatter to display date _and_ time.
+ *
+ * Use this formatter when in doubt.
+ *
+ * @example
+ * // en-US: returns something like Jul 6, 2020, 2:43 PM
+ * // en-GB: returns something like 6 Jul 2020, 14:43
+ * localeDateFormat.asDateTime.format(date)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asDateTime() {
+ return (
+ this.#formatters[DATE_WITH_TIME_FORMAT] ||
+ this.#createFormatter(DATE_WITH_TIME_FORMAT, {
+ dateStyle: 'medium',
+ timeStyle: 'short',
+ hourCycle: DateTimeFormat.#hourCycle,
+ })
+ );
+ }
+ /**
+ * Locale aware formatter to a complete date time.
+ *
+ * This is needed if you need to convey a full timestamp including timezone and seconds.
+ *
+ * This is mainly used in tooltips. Use {@link DateTimeFormat.asDateTime}
+ * if you don't need to show all the information.
+ *
+ *
+ * @example
+ * // en-US: returns something like July 6, 2020 at 2:43:12 PM GMT
+ * // en-GB: returns something like 6 July 2020 at 14:43:12 GMT
+ * localeDateFormat.asDateTimeFull.format(date)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asDateTimeFull() {
+ return (
+ this.#formatters[DATE_TIME_FULL_FORMAT] ||
+ this.#createFormatter(DATE_TIME_FULL_FORMAT, {
+ dateStyle: 'long',
+ timeStyle: 'long',
+ hourCycle: DateTimeFormat.#hourCycle,
+ })
+ );
+ }
+
+ /**
+ * Locale aware formatter to display a only the date.
+ *
+ * Use {@link DateTimeFormat.asDateTime} if you also need to display the time.
+ *
+ * @example
+ * // en-US: returns something like Jul 6, 2020
+ * // en-GB: returns something like 6 Jul 2020
+ * localeDateFormat.asDate.format(date)
+ *
+ * @example
+ * // en-US: returns something like Jul 6 – 7, 2020
+ * // en-GB: returns something like 6-7 Jul 2020
+ * localeDateFormat.asDate.formatRange(date, date2)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asDate() {
+ return (
+ this.#formatters[DATE_ONLY_FORMAT] ||
+ this.#createFormatter(DATE_ONLY_FORMAT, {
+ dateStyle: 'medium',
+ })
+ );
+ }
+
+ /**
+ * Locale aware formatter to display only the time.
+ *
+ * Use {@link DateTimeFormat.asDateTime} if you also need to display the date.
+ *
+ *
+ * @example
+ * // en-US: returns something like 2:43 PM
+ * // en-GB: returns something like 14:43
+ * localeDateFormat.asTime.format(date)
+ *
+ * Note: If formatting a _range_ and the dates are not on the same day,
+ * the formatter will do something sensible like:
+ * 7/9/1983, 2:43 PM – 7/12/1983, 12:36 PM
+ *
+ * @example
+ * // en-US: returns something like 2:43 – 6:27 PM
+ * // en-GB: returns something like 14:43 – 18:27
+ * localeDateFormat.asTime.formatRange(date, date2)
+ *
+ * @returns {DateTimeFormatter}
+ */
+ get asTime() {
+ return (
+ this.#formatters[TIME_ONLY_FORMAT] ||
+ this.#createFormatter(TIME_ONLY_FORMAT, {
+ timeStyle: 'short',
+ hourCycle: DateTimeFormat.#hourCycle,
+ })
+ );
+ }
+
+ /**
+ * Resets the memoized formatters
+ *
+ * While this method only seems to be useful for testing right now,
+ * it could also be used in the future to live-preview the formatting
+ * to the user on their settings page.
+ */
+ reset() {
+ this.#formatters = {};
+ }
+
+ /**
+ * This helper function creates formatters in a memoized fashion.
+ *
+ * The first time a getter is called, it will use this helper
+ * to create an {@link Intl.DateTimeFormat} which is used internally.
+ *
+ * We memoize the creation of the formatter, because using one of them
+ * is about 300 faster than creating them.
+ *
+ * @param {string} name (one of {@link DATE_TIME_FORMATS})
+ * @param {Intl.DateTimeFormatOptions} format
+ * @returns {DateTimeFormatter}
+ */
+ #createFormatter(name, format) {
+ const intlFormatter = createDateTimeFormat(format);
+
+ this.#formatters[name] = {
+ format: (date) => intlFormatter.format(DateTimeFormat.castToDate(date)),
+ formatRange: (date1, date2) => {
+ return intlFormatter.formatRange(
+ DateTimeFormat.castToDate(date1),
+ DateTimeFormat.castToDate(date2),
+ );
+ },
+ };
+
+ return this.#formatters[name];
+ }
+
+ /**
+ * Casts a Dateish to a Date.
+ * @param dateish {Dateish}
+ * @returns {Date}
+ */
+ static castToDate(dateish) {
+ const date = dateish instanceof Date ? dateish : new Date(dateish);
+ if (Number.isNaN(date)) {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ throw new Error('Invalid date provided');
+ }
+ return date;
+ }
+
+ /**
+ * Internal method to determine the {@link Intl.Locale.hourCycle} a user prefers.
+ *
+ * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
+ * @returns {undefined|'h12'|'h23'}
+ */
+ static get #hourCycle() {
+ switch (window.gon?.time_display_format) {
+ case 1:
+ return 'h12';
+ case 2:
+ return 'h23';
+ default:
+ return undefined;
+ }
+ }
+}
+
+/**
+ * A singleton instance of {@link DateTimeFormat}.
+ * This formatting helper respects the user preferences (locale and 12h/24h preference)
+ * and gives an efficient way to format dates and times.
+ *
+ * Each of the supported formatters has support to format a simple date, but also a range.
+ *
+ *
+ * DateTime (showing both date and times):
+ * - {@link DateTimeFormat.asDateTime localeDateFormat.asDateTime} - the default format for date times
+ * - {@link DateTimeFormat.asDateTimeFull localeDateFormat.asDateTimeFull} - full format, including timezone and seconds
+ *
+ * Date (showing date only):
+ * - {@link DateTimeFormat.asDate localeDateFormat.asDate} - the default format for a date
+ *
+ * Time (showing time only):
+ * - {@link DateTimeFormat.asTime localeDateFormat.asTime} - the default format for a time
+ */
+export const localeDateFormat = new DateTimeFormat();
diff --git a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
index 89170ecc55d..3a94b26ee35 100644
--- a/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime/timeago_utility.js
@@ -1,7 +1,6 @@
import * as timeago from 'timeago.js';
-import { languageCode, s__, createDateTimeFormat } from '~/locale';
-import { formatDate } from './date_format_utility';
-import { DATE_WITH_TIME_FORMAT, DATE_ONLY_FORMAT, DEFAULT_DATE_TIME_FORMAT } from './constants';
+import { languageCode, s__ } from '~/locale';
+import { DEFAULT_DATE_TIME_FORMAT, localeDateFormat } from '~/lib/utils/datetime/locale_dateformat';
/**
* Timeago uses underscores instead of dashes to separate language from country code.
@@ -107,51 +106,10 @@ timeago.register(timeagoLanguageCode, memoizedLocale());
timeago.register(`${timeagoLanguageCode}-remaining`, memoizedLocaleRemaining());
timeago.register(`${timeagoLanguageCode}-duration`, memoizedLocaleDuration());
-const setupAbsoluteFormatters = () => {
- let cache = {};
-
- // Intl.DateTimeFormat options (https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat#using_options)
- // For hourCycle please check https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Locale/hourCycle
- const hourCycle = [undefined, 'h12', 'h23'];
- const formats = {
- [DATE_WITH_TIME_FORMAT]: () => ({
- dateStyle: 'medium',
- timeStyle: 'short',
- hourCycle: hourCycle[window.gon?.time_display_format || 0],
- }),
- [DATE_ONLY_FORMAT]: () => ({ dateStyle: 'medium' }),
- };
-
- return (formatName = DEFAULT_DATE_TIME_FORMAT) => {
- if (cache.time_display_format !== window.gon?.time_display_format) {
- cache = {
- time_display_format: window.gon?.time_display_format,
- };
- }
-
- if (cache[formatName]) {
- return cache[formatName];
- }
-
- let format = formats[formatName] && formats[formatName]();
- if (!format) {
- format = formats[DEFAULT_DATE_TIME_FORMAT]();
- }
-
- const formatter = createDateTimeFormat(format);
-
- cache[formatName] = {
- format(date) {
- return formatter.format(date instanceof Date ? date : new Date(date));
- },
- };
- return cache[formatName];
- };
-};
-const memoizedFormatters = setupAbsoluteFormatters();
-
export const getTimeago = (formatName) =>
- window.gon?.time_display_relative === false ? memoizedFormatters(formatName) : timeago;
+ window.gon?.time_display_relative === false
+ ? localeDateFormat[formatName] ?? localeDateFormat[DEFAULT_DATE_TIME_FORMAT]
+ : timeago;
/**
* For the given elements, sets a tooltip with a formatted date.
@@ -171,7 +129,7 @@ export const localTimeAgo = (elements, updateTooltip = true) => {
function addTimeAgoTooltip() {
elements.forEach((el) => {
// Recreate with custom template
- el.setAttribute('title', formatDate(el.dateTime));
+ el.setAttribute('title', localeDateFormat.asDateTimeFull.format(el.dateTime));
});
}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index a6331bc6551..061ce96407e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,6 +1,6 @@
-export * from './datetime/constants';
export * from './datetime/timeago_utility';
export * from './datetime/date_format_utility';
export * from './datetime/date_calculation_utility';
export * from './datetime/pikaday_utility';
export * from './datetime/time_spent_utility';
+export * from './datetime/locale_dateformat';
diff --git a/app/assets/javascripts/lib/utils/regexp.js b/app/assets/javascripts/lib/utils/regexp.js
index f212bf80bd7..8f3c3fccc97 100644
--- a/app/assets/javascripts/lib/utils/regexp.js
+++ b/app/assets/javascripts/lib/utils/regexp.js
@@ -4,12 +4,5 @@
// Inspired by https://github.com/mishoo/UglifyJS/blob/2bc1d02363db3798d5df41fb5059a19edca9b7eb/lib/parse-js.js#L203
// Unicode 6.1
-const unicodeLetters =
+export const unicodeLetters =
'\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376\\u0377\\u037A-\\u037D\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u0527\\u0531-\\u0556\\u0559\\u0561-\\u0587\\u05D0-\\u05EA\\u05F0-\\u05F2\\u0620-\\u064A\\u066E\\u066F\\u0671-\\u06D3\\u06D5\\u06E5\\u06E6\\u06EE\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u08A0\\u08A2-\\u08AC\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0977\\u0979-\\u097F\\u0985-\\u098C\\u098F\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC\\u09DD\\u09DF-\\u09E1\\u09F0\\u09F1\\u0A05-\\u0A0A\\u0A0F\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32\\u0A33\\u0A35\\u0A36\\u0A38\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0\\u0AE1\\u0B05-\\u0B0C\\u0B0F\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99\\u0B9A\\u0B9C\\u0B9E\\u0B9F\\u0BA3\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C33\\u0C35-\\u0C39\\u0C3D\\u0C58\\u0C59\\u0C60\\u0C61\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDE\\u0CE0\\u0CE1\\u0CF1\\u0CF2\\u0D05-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D60\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32\\u0E33\\u0E40-\\u0E46\\u0E81\\u0E82\\u0E84\\u0E87\\u0E88\\u0E8A\\u0E8D\\u0E94-\\u0E97\\u0E99-\\u0E9F\\u0EA1-\\u0EA3\\u0EA5\\u0EA7\\u0EAA\\u0EAB\\u0EAD-\\u0EB0\\u0EB2\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F4\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16EE-\\u16F0\\u1700-\\u170C\\u170E-\\u1711\\u1720-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1877\\u1880-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191C\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19C1-\\u19C7\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4B\\u1B83-\\u1BA0\\u1BAE\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1CE9-\\u1CEC\\u1CEE-\\u1CF1\\u1CF5\\u1CF6\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2160-\\u2188\\u2C00-\\u2C2E\\u2C30-\\u2C5E\\u2C60-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3007\\u3021-\\u3029\\u3031-\\u3035\\u3038-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312D\\u3131-\\u318E\\u31A0-\\u31BA\\u31F0-\\u31FF\\u3400-\\u4DB5\\u4E00-\\u9FCC\\uA000-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A\\uA62B\\uA640-\\uA66E\\uA67F-\\uA697\\uA6A0-\\uA6EF\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA78E\\uA790-\\uA793\\uA7A0-\\uA7AA\\uA7F8-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA80-\\uAAAF\\uAAB1\\uAAB5\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uABC0-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40\\uFB41\\uFB43\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC';
-
-/**
- * A regex that matches all single quotes in a string
- */
-export const allSingleQuotes = /'/g;
-
-export default { unicodeLetters, allSingleQuotes };
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
index 49de7b3a081..4d8612aeeff 100644
--- a/app/assets/javascripts/lib/utils/secret_detection.js
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -28,6 +28,10 @@ export const containsSensitiveToken = (message) => {
name: 'GitLab OAuth Application Secret',
regex: `gloas-[0-9a-zA-Z_-]{64}`,
},
+ {
+ name: 'GitLab Deploy Token',
+ regex: `gldt-[0-9a-zA-Z_-]{20}`,
+ },
];
for (const rule of sensitiveDataPatterns) {
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 638ee1f7e5a..6c30294cbbb 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -5,7 +5,6 @@ import {
TRUNCATE_WIDTH_DEFAULT_WIDTH,
TRUNCATE_WIDTH_DEFAULT_FONT_SIZE,
} from '~/lib/utils/constants';
-import { allSingleQuotes } from '~/lib/utils/regexp';
export const COLON = ':';
export const HYPHEN = '-';
@@ -446,6 +445,11 @@ export const markdownConfig = {
};
/**
+ * A regex that matches all single quotes in a string
+ */
+const allSingleQuotes = /'/g;
+
+/**
* Escapes a string into a shell string, for example
* when you want to give a user the command to checkout
* a branch.
diff --git a/app/assets/javascripts/lib/utils/vuex_module_mappers.js b/app/assets/javascripts/lib/utils/vuex_module_mappers.js
deleted file mode 100644
index 5298eb67c2b..00000000000
--- a/app/assets/javascripts/lib/utils/vuex_module_mappers.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import { mapValues, isString } from 'lodash';
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapActions } from 'vuex';
-
-export const REQUIRE_STRING_ERROR_MESSAGE =
- '`vuex_module_mappers` can only be used with an array of strings, or an object with string values. Consider using the regular `vuex` map helpers instead.';
-
-const normalizeFieldsToObject = (fields) => {
- return Array.isArray(fields)
- ? fields.reduce((acc, key) => Object.assign(acc, { [key]: key }), {})
- : fields;
-};
-
-const mapVuexModuleFields = ({ namespaceSelector, fields, vuexHelper, selector } = {}) => {
- // The `vuexHelper` needs an object which maps keys to field selector functions.
- const map = mapValues(normalizeFieldsToObject(fields), (value) => {
- if (!isString(value)) {
- throw new Error(REQUIRE_STRING_ERROR_MESSAGE);
- }
-
- // We need to use a good ol' function to capture the right "this".
- return function mappedFieldSelector(...args) {
- const namespace = namespaceSelector(this);
-
- return selector(namespace, value, ...args);
- };
- });
-
- return vuexHelper(map);
-};
-
-/**
- * Like `mapState`, but takes a function in the first param for selecting a namespace.
- *
- * ```
- * computed: {
- * ...mapVuexModuleState(vm => vm.vuexModule, ['foo']),
- * }
- * ```
- *
- * @param {Function} namespaceSelector
- * @param {Array|Object} fields
- */
-export const mapVuexModuleState = (namespaceSelector, fields) =>
- mapVuexModuleFields({
- namespaceSelector,
- fields,
- vuexHelper: mapState,
- selector: (namespace, value, state) => state[namespace][value],
- });
-
-/**
- * Like `mapActions`, but takes a function in the first param for selecting a namespace.
- *
- * ```
- * methods: {
- * ...mapVuexModuleActions(vm => vm.vuexModule, ['fetchFoos']),
- * }
- * ```
- *
- * @param {Function} namespaceSelector
- * @param {Array|Object} fields
- */
-export const mapVuexModuleActions = (namespaceSelector, fields) =>
- mapVuexModuleFields({
- namespaceSelector,
- fields,
- vuexHelper: mapActions,
- selector: (namespace, value, dispatch, ...args) => dispatch(`${namespace}/${value}`, ...args),
- });
-
-/**
- * Like `mapGetters`, but takes a function in the first param for selecting a namespace.
- *
- * ```
- * computed: {
- * ...mapGetters(vm => vm.vuexModule, ['hasSearchInfo']),
- * }
- * ```
- *
- * @param {Function} namespaceSelector
- * @param {Array|Object} fields
- */
-export const mapVuexModuleGetters = (namespaceSelector, fields) =>
- mapVuexModuleFields({
- namespaceSelector,
- fields,
- // `mapGetters` does not let us pass an object which maps to functions. Thankfully `mapState` does
- // and gives us access to the getters.
- vuexHelper: mapState,
- selector: (namespace, value, state, getters) => getters[`${namespace}/${value}`],
- });
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index ca3f1caec67..c76e44a196d 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -3,3 +3,20 @@ export default function initLogoAnimation() {
document.querySelector('.tanuki-logo')?.classList.add('animate');
});
}
+
+export function initPortraitLogoDetection() {
+ const image = document.querySelector('.js-portrait-logo-detection');
+
+ image?.addEventListener(
+ 'load',
+ ({ currentTarget: img }) => {
+ const isPortrait = img.height > img.width;
+ if (isPortrait) {
+ // Limit the width when the logo has portrait format
+ img.classList.replace('gl-h-9', 'gl-w-10');
+ }
+ img.classList.remove('gl-visibility-hidden');
+ },
+ { once: true },
+ );
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 29189e3ac2f..c3914391a49 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -15,26 +15,21 @@ import * as tooltips from '~/tooltips';
import { initPrefetchLinks } from '~/lib/utils/navigation_utility';
import { logHelloDeferred } from 'jh_else_ce/lib/logger/hello_deferred';
import initAlertHandler from './alert_handler';
-import initTodoToggle from './header';
import initLayoutNav from './layout_nav';
import { handleLocationHash, addSelectOnFocusBehaviour } from './lib/utils/common_utils';
import { localTimeAgo } from './lib/utils/datetime/timeago_utility';
import { getLocationHash, visitUrl, mergeUrlParams } from './lib/utils/url_utility';
// everything else
-import initFeatureHighlight from './feature_highlight';
import LazyLoader from './lazy_loader';
-import initLogoAnimation from './logo';
+import initLogoAnimation, { initPortraitLogoDetection } from './logo';
import initBreadcrumbs from './breadcrumb';
import initPersistentUserCallouts from './persistent_user_callouts';
import { initUserTracking, initDefaultTrackers } from './tracking';
-import { initSidebarTracking } from './pages/shared/nav/sidebar_tracking';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
import initBroadcastNotifications from './broadcast_notification';
-import { initTopNav } from './nav';
import { initCopyCodeButton } from './behaviors/copy_code';
-import initHeaderSearch from './header_search/init';
import initGitlabVersionCheck from './gitlab_version_check';
import 'ee_else_ce/main_ee';
@@ -85,19 +80,14 @@ initRails();
function deferredInitialisation() {
const $body = $('body');
- if (!gon.use_new_navigation) {
- initTopNav();
- initTodoToggle();
- }
initBreadcrumbs();
initPrefetchLinks('.js-prefetch-document');
initLogoAnimation();
+ initPortraitLogoDetection();
initUserPopovers();
initBroadcastNotifications();
initPersistentUserCallouts();
initDefaultTrackers();
- initSidebarTracking();
- initFeatureHighlight();
initCopyCodeButton();
initGitlabVersionCheck();
@@ -121,11 +111,6 @@ function deferredInitialisation() {
setTimeout(() => $body.addClass('page-initialised'), 1000);
}
-// header search vue component bootstrap
-// loading this inside requestIdleCallback is causing issues
-// see https://gitlab.com/gitlab-org/gitlab/-/issues/365746
-initHeaderSearch();
-
const $body = $('body');
const $document = $(document);
const bootstrapBreakpoint = bp.getBreakpointSize();
@@ -198,10 +183,6 @@ $body.on('ajax:complete, ajax:beforeSend, submit', 'form', function ajaxComplete
}
});
-$('.navbar-toggler').on('click', () => {
- document.body.classList.toggle('top-nav-responsive-open');
-});
-
/**
* Show suppressed commit diff
*
diff --git a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
index 2f10a333bf4..c76b928ad3d 100644
--- a/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
+++ b/app/assets/javascripts/members/components/action_buttons/remove_group_link_button.vue
@@ -36,7 +36,7 @@ export default {
:title="$options.i18n.buttonTitle"
:aria-label="$options.i18n.buttonTitle"
icon="remove"
- data-qa-selector="remove_group_link_button"
+ data-testid="remove-group-link-button"
@click="showRemoveGroupLinkModal(groupLink)"
/>
</template>
diff --git a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
index 18db8fe9cfb..55f75bc819c 100644
--- a/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_group_link_modal.vue
@@ -15,7 +15,7 @@ export default {
text: s__('Members|Remove group'),
attributes: {
variant: 'danger',
- 'data-qa-selector': 'remove_group_button',
+ 'data-testid': 'remove-group-button',
},
},
csrf,
@@ -69,7 +69,7 @@ export default {
:action-primary="$options.actionPrimary"
:action-cancel="$options.actionCancel"
size="sm"
- data-qa-selector="remove_group_link_modal_content"
+ data-testid="remove-group-link-modal-content"
@primary="handlePrimary"
@hide="hideRemoveGroupLinkModal"
>
diff --git a/app/assets/javascripts/members/components/modals/remove_member_modal.vue b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
index ecc769174f4..d3079dc7d0a 100644
--- a/app/assets/javascripts/members/components/modals/remove_member_modal.vue
+++ b/app/assets/javascripts/members/components/modals/remove_member_modal.vue
@@ -72,7 +72,7 @@ export default {
text: this.actionText,
attributes: {
variant: 'danger',
- 'data-qa-selector': 'remove_member_button',
+ 'data-testid': 'remove-member-button',
},
};
},
@@ -104,7 +104,7 @@ export default {
:action-primary="actionPrimary"
:title="actionText"
:visible="removeMemberModalVisible"
- data-qa-selector="remove_member_modal"
+ data-testid="remove-member-modal"
@primary="submitForm"
@hide="hideRemoveMemberModal"
>
diff --git a/app/assets/javascripts/members/components/table/max_role.vue b/app/assets/javascripts/members/components/table/max_role.vue
new file mode 100644
index 00000000000..89780108518
--- /dev/null
+++ b/app/assets/javascripts/members/components/table/max_role.vue
@@ -0,0 +1,134 @@
+<script>
+import { GlBadge, GlCollapsibleListbox } from '@gitlab/ui';
+import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
+// eslint-disable-next-line no-restricted-imports
+import { mapActions } from 'vuex';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
+import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils';
+import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ GlBadge,
+ LdapDropdownFooter: () =>
+ import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'),
+ CustomPermissions: () => import('ee_component/members/components/table/custom_permissions.vue'),
+ },
+ inject: ['namespace', 'group'],
+ props: {
+ member: {
+ type: Object,
+ required: true,
+ },
+ permissions: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ const accessLevelOptions = roleDropdownItems(this.member);
+ return {
+ accessLevelOptions,
+ busy: false,
+ customPermissions: this.member.customPermissions ?? [],
+ isDesktop: false,
+ memberRoleId: this.member.accessLevel.memberRoleId ?? null,
+ selectedRole: initialSelectedRole(accessLevelOptions.flatten, this.member),
+ };
+ },
+ computed: {
+ disabled() {
+ return this.permissions.canOverride && !this.member.isOverridden;
+ },
+ },
+ mounted() {
+ this.isDesktop = bp.isDesktop();
+ },
+ methods: {
+ ...mapActions({
+ updateMemberRole(dispatch, { memberId, accessLevel, memberRoleId }) {
+ return dispatch(`${this.namespace}/updateMemberRole`, {
+ memberId,
+ accessLevel,
+ memberRoleId,
+ });
+ },
+ }),
+ async handleSelect(value) {
+ this.busy = true;
+
+ const newRole = this.accessLevelOptions.flatten.find((item) => item.value === value);
+ const previousRole = this.selectedRole;
+ const previousMemberRoleId = this.memberRoleId;
+
+ try {
+ const confirmed = await guestOverageConfirmAction({
+ oldAccessLevel: this.member.accessLevel.integerValue,
+ newRoleName: ACCESS_LEVEL_LABELS[newRole.accessLevel],
+ newMemberRoleId: newRole.memberRoleId,
+ group: this.group,
+ memberId: this.member.id,
+ memberType: this.namespace,
+ });
+ if (!confirmed) {
+ return;
+ }
+
+ this.selectedRole = value;
+ this.memberRoleId = newRole.memberRoleId;
+
+ await this.updateMemberRole({
+ memberId: this.member.id,
+ accessLevel: newRole.accessLevel,
+ memberRoleId: newRole.memberRoleId,
+ });
+
+ this.$toast.show(s__('Members|Role updated successfully.'));
+ } catch (error) {
+ this.selectedRole = previousRole;
+ this.memberRoleId = previousMemberRoleId;
+ Sentry.captureException(error);
+ } finally {
+ this.busy = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-collapsible-listbox
+ v-if="permissions.canUpdate"
+ :placement="isDesktop ? 'left' : 'right'"
+ :header-text="__('Change role')"
+ :disabled="disabled"
+ :loading="busy"
+ data-testid="access-level-dropdown"
+ :items="accessLevelOptions.formatted"
+ :selected="selectedRole"
+ @select="handleSelect"
+ >
+ <template #list-item="{ item }">
+ <span data-testid="access-level-link">{{ item.text }}</span>
+ </template>
+ <template #footer>
+ <ldap-dropdown-footer
+ v-if="permissions.canOverride && member.isOverridden"
+ :member-id="member.id"
+ />
+ </template>
+ </gl-collapsible-listbox>
+
+ <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
+
+ <custom-permissions
+ v-if="memberRoleId !== null"
+ :member-role-id="memberRoleId"
+ :custom-permissions="customPermissions"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/members/components/table/members_table.vue b/app/assets/javascripts/members/components/table/members_table.vue
index 2b3294c1c79..1bccb8a0c4b 100644
--- a/app/assets/javascripts/members/components/table/members_table.vue
+++ b/app/assets/javascripts/members/components/table/members_table.vue
@@ -31,7 +31,7 @@ import MemberActions from './member_actions.vue';
import MemberAvatar from './member_avatar.vue';
import MemberSource from './member_source.vue';
import MemberActivity from './member_activity.vue';
-import RoleDropdown from './role_dropdown.vue';
+import MaxRole from './max_role.vue';
export default {
name: 'MembersTable',
@@ -44,7 +44,7 @@ export default {
MembersTableCell,
MemberSource,
MemberActions,
- RoleDropdown,
+ MaxRole,
RemoveGroupLinkModal,
RemoveMemberModal,
ExpirationDatepicker,
@@ -143,7 +143,6 @@ export default {
...this.tableAttrs.tr,
...(member?.id && {
'data-testid': `members-table-row-${member.id}`,
- 'data-qa-selector': 'member_row',
}),
};
},
@@ -292,8 +291,7 @@ export default {
<template #cell(maxRole)="{ item: member }">
<members-table-cell #default="{ permissions }" :member="member">
- <role-dropdown v-if="permissions.canUpdate" :permissions="permissions" :member="member" />
- <gl-badge v-else>{{ member.accessLevel.stringValue }}</gl-badge>
+ <max-role :permissions="permissions" :member="member" />
</members-table-cell>
</template>
diff --git a/app/assets/javascripts/members/components/table/role_dropdown.vue b/app/assets/javascripts/members/components/table/role_dropdown.vue
deleted file mode 100644
index 2b72a3fe6e8..00000000000
--- a/app/assets/javascripts/members/components/table/role_dropdown.vue
+++ /dev/null
@@ -1,120 +0,0 @@
-<script>
-import { GlCollapsibleListbox } from '@gitlab/ui';
-import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions } from 'vuex';
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
-import { roleDropdownItems, initialSelectedRole } from 'ee_else_ce/members/utils';
-import { s__ } from '~/locale';
-
-export default {
- name: 'RoleDropdown',
- components: {
- GlCollapsibleListbox,
- LdapDropdownFooter: () =>
- import('ee_component/members/components/action_dropdowns/ldap_dropdown_footer.vue'),
- },
- inject: ['namespace', 'group'],
- props: {
- member: {
- type: Object,
- required: true,
- },
- permissions: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- isDesktop: false,
- busy: false,
- selectedRole: null,
- };
- },
- computed: {
- disabled() {
- return this.permissions.canOverride && !this.member.isOverridden;
- },
- dropdownItems() {
- return roleDropdownItems(this.member);
- },
- },
- created() {
- this.selectedRole = initialSelectedRole(this.dropdownItems.flatten, this.member);
- },
- mounted() {
- this.isDesktop = bp.isDesktop();
- },
- methods: {
- ...mapActions({
- updateMemberRole(dispatch, payload) {
- return dispatch(`${this.namespace}/updateMemberRole`, payload);
- },
- }),
- async handleSelect(value) {
- this.busy = true;
-
- const newRole = this.dropdownItems.flatten.find((item) => item.value === value);
- const previousRole = this.selectedRole;
-
- try {
- const confirmed = await guestOverageConfirmAction({
- currentRoleValue: this.member.accessLevel.integerValue,
- newRoleValue: newRole.accessLevel,
- newRoleName: newRole.text,
- newMemberRoleId: newRole.memberRoleId,
- group: this.group,
- memberId: this.member.id,
- memberType: this.namespace,
- });
- if (!confirmed) {
- return;
- }
-
- this.selectedRole = value;
-
- await this.updateMemberRole({
- memberId: this.member.id,
- accessLevel: {
- integerValue: newRole.accessLevel,
- memberRoleId: newRole.memberRoleId,
- },
- });
-
- this.$toast.show(s__('Members|Role updated successfully.'));
- } catch (error) {
- this.selectedRole = previousRole;
- Sentry.captureException(error);
- } finally {
- this.busy = false;
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-collapsible-listbox
- :placement="isDesktop ? 'left' : 'right'"
- :toggle-text="member.accessLevel.stringValue"
- :header-text="__('Change role')"
- :disabled="disabled"
- :loading="busy"
- data-qa-selector="access_level_dropdown"
- :items="dropdownItems.formatted"
- :selected="selectedRole"
- @select="handleSelect"
- >
- <template #list-item="{ item }">
- <span data-qa-selector="access_level_link">{{ item.text }}</span>
- </template>
- <template #footer>
- <ldap-dropdown-footer
- v-if="permissions.canOverride && member.isOverridden"
- :member-id="member.id"
- />
- </template>
- </gl-collapsible-listbox>
-</template>
diff --git a/app/assets/javascripts/members/index.js b/app/assets/javascripts/members/index.js
index 87ae670c146..ad477d8b4b6 100644
--- a/app/assets/javascripts/members/index.js
+++ b/app/assets/javascripts/members/index.js
@@ -2,6 +2,8 @@ import { GlToast } from '@gitlab/ui';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createDefaultClient from '~/lib/graphql';
import { parseDataAttributes } from '~/members/utils';
import { MEMBER_TYPES } from 'ee_else_ce/members/constants';
import MembersTabs from './components/members_tabs.vue';
@@ -13,6 +15,7 @@ export const initMembersApp = (el, options) => {
}
Vue.use(Vuex);
+ Vue.use(VueApollo);
Vue.use(GlToast);
const {
@@ -61,6 +64,7 @@ export const initMembersApp = (el, options) => {
el,
components: { MembersTabs },
store,
+ apolloProvider: new VueApollo({ defaultClient: createDefaultClient() }),
provide: {
currentUserId: gon.current_user_id || null,
sourceId,
diff --git a/app/assets/javascripts/members/store/actions.js b/app/assets/javascripts/members/store/actions.js
index d696f618a3c..54a53e7c0a9 100644
--- a/app/assets/javascripts/members/store/actions.js
+++ b/app/assets/javascripts/members/store/actions.js
@@ -2,17 +2,15 @@ import axios from '~/lib/utils/axios_utils';
import { formatDate } from '~/lib/utils/datetime_utility';
import * as types from './mutation_types';
-export const updateMemberRole = async ({ state, commit }, { memberId, accessLevel }) => {
+export const updateMemberRole = async (
+ { state, commit },
+ { memberId, accessLevel, memberRoleId },
+) => {
try {
await axios.put(
state.memberPath.replace(/:id$/, memberId),
- state.requestFormatter({
- accessLevel: accessLevel.integerValue,
- memberRoleId: accessLevel.memberRoleId,
- }),
+ state.requestFormatter({ accessLevel, memberRoleId }),
);
-
- commit(types.RECEIVE_MEMBER_ROLE_SUCCESS, { memberId, accessLevel });
} catch (error) {
commit(types.RECEIVE_MEMBER_ROLE_ERROR, { error });
diff --git a/app/assets/javascripts/members/store/mutation_types.js b/app/assets/javascripts/members/store/mutation_types.js
index 5fa75725552..c1cdbf6146f 100644
--- a/app/assets/javascripts/members/store/mutation_types.js
+++ b/app/assets/javascripts/members/store/mutation_types.js
@@ -1,4 +1,3 @@
-export const RECEIVE_MEMBER_ROLE_SUCCESS = 'RECEIVE_MEMBER_ROLE_SUCCESS';
export const RECEIVE_MEMBER_ROLE_ERROR = 'RECEIVE_MEMBER_ROLE_ERROR';
export const RECEIVE_MEMBER_EXPIRATION_SUCCESS = 'RECEIVE_MEMBER_EXPIRATION_SUCCESS';
diff --git a/app/assets/javascripts/members/store/mutations.js b/app/assets/javascripts/members/store/mutations.js
index b4cf9f3480f..edc400aef7d 100644
--- a/app/assets/javascripts/members/store/mutations.js
+++ b/app/assets/javascripts/members/store/mutations.js
@@ -4,15 +4,6 @@ import * as types from './mutation_types';
import { findMember } from './utils';
export default {
- [types.RECEIVE_MEMBER_ROLE_SUCCESS](state, { memberId, accessLevel }) {
- const member = findMember(state, memberId);
-
- if (!member) {
- return;
- }
-
- Vue.set(member, 'accessLevel', accessLevel);
- },
[types.RECEIVE_MEMBER_ROLE_ERROR](state, { error }) {
state.errorMessage =
error.response?.data?.message ||
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
index d80517c1c1f..5f8f0e2b96c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_resolver_app.vue
@@ -2,6 +2,7 @@
import { GlSprintf, GlButton, GlButtonGroup, GlLoadingIcon } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState, mapActions } from 'vuex';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import { __ } from '~/locale';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import DiffFileEditor from './components/diff_file_editor.vue';
@@ -23,6 +24,7 @@ export default {
components: {
GlButton,
GlButtonGroup,
+ ClipboardButton,
GlSprintf,
GlLoadingIcon,
FileIcon,
@@ -122,6 +124,12 @@ export default {
<div class="file-header-content" data-testid="file-name">
<file-icon :file-name="file.filePath" :size="16" css-classes="gl-mr-2" />
<strong class="file-title-name">{{ file.filePath }}</strong>
+ <clipboard-button
+ :title="__('Copy file path')"
+ :text="file.filePath"
+ size="small"
+ category="tertiary"
+ />
</div>
<div class="file-actions d-flex align-items-center gl-ml-auto gl-align-self-start">
<gl-button-group v-if="file.type === 'text'" class="gl-mr-3">
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 8ea995b8b4e..1290a5a17b9 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -237,7 +237,8 @@ export default class MergeRequestTabs {
bindEvents() {
$('.merge-request-tabs a[data-toggle="tabvue"]').on('click', this.clickTab);
- window.addEventListener('popstate', () => {
+ window.addEventListener('popstate', (event) => {
+ if (event?.state?.skipScrolling) return;
const action = getActionFromHref(location.href);
this.tabShown(action, location.href);
@@ -603,11 +604,7 @@ export default class MergeRequestTabs {
if (!isInVueNoteablePage() || this.cachedPageLayoutClasses) return;
this.cachedPageLayoutClasses = this.pageLayout.className;
- this.pageLayout.classList.remove(
- 'right-sidebar-collapsed',
- 'right-sidebar-expanded',
- 'page-with-icon-sidebar',
- );
+ this.pageLayout.classList.remove('right-sidebar-collapsed', 'right-sidebar-expanded');
this.sidebar.style.width = '0px';
}
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
index 43d28e3d699..ea942012af3 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
+++ b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/ml_candidates_show.vue
@@ -1,45 +1,15 @@
<script>
-import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
-import { isEmpty, maxBy, range } from 'lodash';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
-import { __, sprintf } from '~/locale';
-import DetailRow from './components/candidate_detail_row.vue';
-
-import {
- TITLE_LABEL,
- INFO_LABEL,
- ID_LABEL,
- STATUS_LABEL,
- EXPERIMENT_LABEL,
- ARTIFACTS_LABEL,
- PARAMETERS_LABEL,
- METRICS_LABEL,
- METADATA_LABEL,
- DELETE_CANDIDATE_CONFIRMATION_MESSAGE,
- DELETE_CANDIDATE_PRIMARY_ACTION_LABEL,
- DELETE_CANDIDATE_MODAL_TITLE,
- MLFLOW_ID_LABEL,
- CI_SECTION_LABEL,
- JOB_LABEL,
- CI_USER_LABEL,
- CI_MR_LABEL,
- PERFORMANCE_LABEL,
- NO_PARAMETERS_MESSAGE,
- NO_METRICS_MESSAGE,
- NO_METADATA_MESSAGE,
- NO_CI_MESSAGE,
-} from './translations';
+import CandidateDetail from '~/ml/model_registry/components/candidate_detail.vue';
+import { s__ } from '~/locale';
export default {
name: 'MlCandidatesShow',
components: {
ModelExperimentsHeader,
DeleteButton,
- DetailRow,
- GlAvatarLabeled,
- GlLink,
- GlTableLite,
+ CandidateDetail,
},
props: {
candidate: {
@@ -47,70 +17,18 @@ export default {
required: true,
},
},
- i18n: {
- TITLE_LABEL,
- INFO_LABEL,
- ID_LABEL,
- STATUS_LABEL,
- EXPERIMENT_LABEL,
- ARTIFACTS_LABEL,
- DELETE_CANDIDATE_CONFIRMATION_MESSAGE,
- DELETE_CANDIDATE_PRIMARY_ACTION_LABEL,
- DELETE_CANDIDATE_MODAL_TITLE,
- MLFLOW_ID_LABEL,
- CI_SECTION_LABEL,
- JOB_LABEL,
- CI_USER_LABEL,
- CI_MR_LABEL,
- PARAMETERS_LABEL,
- METRICS_LABEL,
- METADATA_LABEL,
- PERFORMANCE_LABEL,
- NO_PARAMETERS_MESSAGE,
- NO_METRICS_MESSAGE,
- NO_METADATA_MESSAGE,
- NO_CI_MESSAGE,
- },
computed: {
info() {
return Object.freeze(this.candidate.info);
},
- ciJob() {
- return Object.freeze(this.info.ci_job);
- },
- hasMetadata() {
- return !isEmpty(this.candidate.metadata);
- },
- hasParameters() {
- return !isEmpty(this.candidate.params);
- },
- hasMetrics() {
- return !isEmpty(this.candidate.metrics);
- },
- metricsTableFields() {
- const maxStep = maxBy(this.candidate.metrics, 'step').step;
- const rowClass = 'gl-p-3!';
-
- const cssClasses = { thClass: rowClass, tdClass: rowClass };
-
- const fields = range(maxStep + 1).map((step) => ({
- key: step.toString(),
- label: sprintf(__('Step %{step}'), { step }),
- ...cssClasses,
- }));
-
- return [{ key: 'name', label: __('Metric'), ...cssClasses }, ...fields];
- },
- metricsTableItems() {
- const items = {};
- this.candidate.metrics.forEach((metric) => {
- const metricRow = items[metric.name] || { name: metric.name };
- metricRow[metric.step] = metric.value;
- items[metric.name] = metricRow;
- });
-
- return Object.values(items);
- },
+ },
+ i18n: {
+ TITLE_LABEL: s__('MlExperimentTracking|Model candidate details'),
+ DELETE_CANDIDATE_CONFIRMATION_MESSAGE: s__(
+ 'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.',
+ ),
+ DELETE_CANDIDATE_PRIMARY_ACTION_LABEL: s__('MlExperimentTracking|Delete candidate'),
+ DELETE_CANDIDATE_MODAL_TITLE: s__('MlExperimentTracking|Delete candidate?'),
},
};
</script>
@@ -126,106 +44,6 @@ export default {
/>
</model-experiments-header>
- <section class="gl-mb-6">
- <table class="candidate-details">
- <tbody>
- <detail-row :label="$options.i18n.ID_LABEL">
- {{ info.iid }}
- </detail-row>
-
- <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
-
- <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
-
- <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
- <gl-link :href="info.path_to_experiment">
- {{ info.experiment_name }}
- </gl-link>
- </detail-row>
-
- <detail-row v-if="info.path_to_artifact" :label="$options.i18n.ARTIFACTS_LABEL">
- <gl-link :href="info.path_to_artifact">
- {{ $options.i18n.ARTIFACTS_LABEL }}
- </gl-link>
- </detail-row>
- </tbody>
- </table>
- </section>
-
- <section class="gl-mb-6">
- <h4>{{ $options.i18n.CI_SECTION_LABEL }}</h4>
-
- <table v-if="ciJob" class="candidate-details">
- <tbody>
- <detail-row
- :label="$options.i18n.JOB_LABEL"
- :section-label="$options.i18n.CI_SECTION_LABEL"
- >
- <gl-link :href="ciJob.path">
- {{ ciJob.name }}
- </gl-link>
- </detail-row>
-
- <detail-row v-if="ciJob.user" :label="$options.i18n.CI_USER_LABEL">
- <gl-avatar-labeled label="" :size="24" :src="ciJob.user.avatar">
- <gl-link :href="ciJob.user.path">
- {{ ciJob.user.name }}
- </gl-link>
- </gl-avatar-labeled>
- </detail-row>
-
- <detail-row v-if="ciJob.merge_request" :label="$options.i18n.CI_MR_LABEL">
- <gl-link :href="ciJob.merge_request.path">
- !{{ ciJob.merge_request.iid }} {{ ciJob.merge_request.title }}
- </gl-link>
- </detail-row>
- </tbody>
- </table>
-
- <div v-else class="gl-text-secondary">{{ $options.i18n.NO_CI_MESSAGE }}</div>
- </section>
-
- <section class="gl-mb-6">
- <h4>{{ $options.i18n.PARAMETERS_LABEL }}</h4>
-
- <table v-if="hasParameters" class="candidate-details">
- <tbody>
- <detail-row v-for="item in candidate.params" :key="item.name" :label="item.name">
- {{ item.value }}
- </detail-row>
- </tbody>
- </table>
-
- <div v-else class="gl-text-secondary">{{ $options.i18n.NO_PARAMETERS_MESSAGE }}</div>
- </section>
-
- <section class="gl-mb-6">
- <h4>{{ $options.i18n.METADATA_LABEL }}</h4>
-
- <table v-if="hasMetadata" class="candidate-details">
- <tbody>
- <detail-row v-for="item in candidate.metadata" :key="item.name" :label="item.name">
- {{ item.value }}
- </detail-row>
- </tbody>
- </table>
-
- <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METADATA_MESSAGE }}</div>
- </section>
-
- <section class="gl-mb-6">
- <h4>{{ $options.i18n.PERFORMANCE_LABEL }}</h4>
-
- <div v-if="hasMetrics" class="gl-overflow-x-auto">
- <gl-table-lite
- :items="metricsTableItems"
- :fields="metricsTableFields"
- class="gl-w-auto"
- hover
- />
- </div>
-
- <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METRICS_MESSAGE }}</div>
- </section>
+ <candidate-detail :candidate="candidate" />
</div>
</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js b/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
deleted file mode 100644
index 98988e1db35..00000000000
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/translations.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import { __, s__ } from '~/locale';
-
-export const TITLE_LABEL = s__('MlExperimentTracking|Model candidate details');
-export const INFO_LABEL = s__('MlExperimentTracking|Info');
-export const ID_LABEL = s__('MlExperimentTracking|ID');
-export const MLFLOW_ID_LABEL = s__('MlExperimentTracking|MLflow run ID');
-export const STATUS_LABEL = s__('MlExperimentTracking|Status');
-export const EXPERIMENT_LABEL = s__('MlExperimentTracking|Experiment');
-export const ARTIFACTS_LABEL = s__('MlExperimentTracking|Artifacts');
-export const PARAMETERS_LABEL = s__('MlExperimentTracking|Parameters');
-export const METRICS_LABEL = s__('MlExperimentTracking|Metrics');
-export const PERFORMANCE_LABEL = s__('MlExperimentTracking|Model performance');
-export const METADATA_LABEL = s__('MlExperimentTracking|Metadata');
-export const NO_PARAMETERS_MESSAGE = s__('MlExperimentTracking|No logged parameters');
-export const NO_METRICS_MESSAGE = s__('MlExperimentTracking|No logged metrics');
-export const NO_METADATA_MESSAGE = s__('MlExperimentTracking|No logged metadata');
-export const NO_CI_MESSAGE = s__('MlExperimentTracking|Candidate not linked to a CI build');
-export const DELETE_CANDIDATE_CONFIRMATION_MESSAGE = s__(
- 'MlExperimentTracking|Deleting this candidate will delete the associated parameters, metrics, and metadata.',
-);
-export const DELETE_CANDIDATE_PRIMARY_ACTION_LABEL = s__('MlExperimentTracking|Delete candidate');
-export const DELETE_CANDIDATE_MODAL_TITLE = s__('MLExperimentTracking|Delete candidate?');
-export const CI_SECTION_LABEL = s__('MLExperimentTracking|CI Info');
-export const JOB_LABEL = __('Job');
-export const CI_USER_LABEL = s__('MlExperimentTracking|Triggered by');
-export const CI_MR_LABEL = __('Merge request');
diff --git a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
index 5a55d5669a8..e5e093db5ca 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue
@@ -1,10 +1,13 @@
<script>
import { isEmpty } from 'lodash';
+import { GlBadge } from '@gitlab/ui';
import Pagination from '~/vue_shared/components/incubation/pagination.vue';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import EmptyState from '../components/empty_state.vue';
import * as i18n from '../translations';
-import { BASE_SORT_FIELDS } from '../constants';
+import { BASE_SORT_FIELDS, MODEL_ENTITIES } from '../constants';
import SearchBar from '../components/search_bar.vue';
import ModelRow from '../components/model_row.vue';
@@ -16,6 +19,8 @@ export default {
SearchBar,
MetadataItem,
TitleArea,
+ GlBadge,
+ EmptyState,
},
props: {
models: {
@@ -39,23 +44,32 @@ export default {
},
i18n,
sortableFields: BASE_SORT_FIELDS,
+ docHref: helpPagePath('user/project/ml/model_registry/index.md'),
+ modelEntity: MODEL_ENTITIES.model,
};
</script>
<template>
<div>
- <title-area :title="$options.i18n.TITLE_LABEL">
+ <title-area>
+ <template #title>
+ <div class="gl-flex-grow-1 gl-display-flex gl-align-items-center">
+ <span>{{ $options.i18n.TITLE_LABEL }}</span>
+ <gl-badge variant="neutral" class="gl-mx-4" size="lg" :href="$options.docHref">
+ {{ __('Experiment') }}
+ </gl-badge>
+ </div>
+ </template>
<template #metadata-models-count>
<metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" />
</template>
</title-area>
-
<template v-if="hasModels">
<search-bar :sortable-fields="$options.sortableFields" />
<model-row v-for="model in models" :key="model.name" :model="model" />
<pagination v-bind="pageInfo" />
</template>
- <p v-else class="gl-text-secondary">{{ $options.i18n.NO_MODELS_LABEL }}</p>
+ <empty-state v-else :entity-type="$options.modelEntity" />
</div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
index e8ec8f157ef..51b8fca6511 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue
@@ -2,16 +2,23 @@
import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
import MetadataItem from '~/vue_shared/components/registry/metadata_item.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import ModelVersionDetail from '~/ml/model_registry/components/model_version_detail.vue';
+import { MODEL_ENTITIES } from '~/ml/model_registry/constants';
+import EmptyState from '../components/empty_state.vue';
import * as i18n from '../translations';
export default {
name: 'ShowMlModelApp',
components: {
+ ModelVersionList: () => import('../components/model_version_list.vue'),
+ CandidateList: () => import('../components/candidate_list.vue'),
+ EmptyState,
TitleArea,
GlTabs,
GlTab,
GlBadge,
MetadataItem,
+ ModelVersionDetail,
},
props: {
model: {
@@ -26,8 +33,12 @@ export default {
candidateCount() {
return this.model.candidateCount || 0;
},
+ latestVersionTitle() {
+ return `${i18n.LATEST_VERSION_LABEL}: ${this.model.latestVersion.version}`;
+ },
},
i18n,
+ modelVersionEntity: MODEL_ENTITIES.modelVersion,
};
</script>
@@ -48,23 +59,28 @@ export default {
<gl-tabs class="gl-mt-4">
<gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
- <h3 class="gl-font-lg">{{ $options.i18n.LATEST_VERSION_LABEL }}</h3>
<template v-if="model.latestVersion">
- {{ model.latestVersion.version }}
+ <h3 class="gl-font-lg">{{ latestVersionTitle }}</h3>
+ <model-version-detail :model-version="model.latestVersion" />
</template>
- <div v-else class="gl-text-secondary">{{ $options.i18n.NO_VERSIONS_LABEL }}</div>
+
+ <empty-state v-else :entity-type="$options.modelVersionEntity" />
</gl-tab>
<gl-tab>
<template #title>
{{ $options.i18n.MODEL_OTHER_VERSIONS_TAB_LABEL }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ versionCount }}</gl-badge>
</template>
+
+ <model-version-list :model-id="model.id" />
</gl-tab>
<gl-tab>
<template #title>
{{ $options.i18n.MODEL_CANDIDATES_TAB_LABEL }}
<gl-badge size="sm" class="gl-tab-counter-badge">{{ candidateCount }}</gl-badge>
</template>
+
+ <candidate-list :model-id="model.id" />
</gl-tab>
</gl-tabs>
</div>
diff --git a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
index a9440aff1ce..6608f44ecf7 100644
--- a/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
+++ b/app/assets/javascripts/ml/model_registry/apps/show_ml_model_version.vue
@@ -1,16 +1,30 @@
<script>
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import ModelVersionDetail from '../components/model_version_detail.vue';
+
export default {
name: 'ShowMlModelVersionApp',
- components: {},
+ components: {
+ ModelVersionDetail,
+ TitleArea,
+ },
props: {
modelVersion: {
type: Object,
required: true,
},
},
+ computed: {
+ title() {
+ return `${this.modelVersion.model.name} / ${this.modelVersion.version}`;
+ },
+ },
};
</script>
<template>
- <div>{{ modelVersion.model.name }} - {{ modelVersion.version }}</div>
+ <div>
+ <title-area :title="title" />
+ <model-version-detail :model-version="modelVersion" />
+ </div>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/candidate_detail.vue b/app/assets/javascripts/ml/model_registry/components/candidate_detail.vue
new file mode 100644
index 00000000000..58216a77e9e
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/candidate_detail.vue
@@ -0,0 +1,213 @@
+<script>
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { isEmpty, maxBy, range } from 'lodash';
+import { __, sprintf } from '~/locale';
+import {
+ INFO_LABEL,
+ ID_LABEL,
+ STATUS_LABEL,
+ EXPERIMENT_LABEL,
+ ARTIFACTS_LABEL,
+ PARAMETERS_LABEL,
+ METADATA_LABEL,
+ MLFLOW_ID_LABEL,
+ CI_SECTION_LABEL,
+ JOB_LABEL,
+ CI_USER_LABEL,
+ CI_MR_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
+} from '../translations';
+import DetailRow from './candidate_detail_row.vue';
+
+export default {
+ HEADER_CLASSES: ['gl-font-lg', 'gl-mt-5'],
+ name: 'MlCandidateDetail',
+ components: {
+ DetailRow,
+ GlAvatarLabeled,
+ GlLink,
+ GlTableLite,
+ },
+ props: {
+ candidate: {
+ type: Object,
+ required: true,
+ },
+ showInfoSection: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ },
+ i18n: {
+ INFO_LABEL,
+ ID_LABEL,
+ STATUS_LABEL,
+ EXPERIMENT_LABEL,
+ ARTIFACTS_LABEL,
+ MLFLOW_ID_LABEL,
+ CI_SECTION_LABEL,
+ JOB_LABEL,
+ CI_USER_LABEL,
+ CI_MR_LABEL,
+ PARAMETERS_LABEL,
+ METADATA_LABEL,
+ PERFORMANCE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
+ },
+ computed: {
+ info() {
+ return Object.freeze(this.candidate.info);
+ },
+ ciJob() {
+ return Object.freeze(this.info.ciJob);
+ },
+ hasMetadata() {
+ return !isEmpty(this.candidate.metadata);
+ },
+ hasParameters() {
+ return !isEmpty(this.candidate.params);
+ },
+ hasMetrics() {
+ return !isEmpty(this.candidate.metrics);
+ },
+ metricsTableFields() {
+ const maxStep = maxBy(this.candidate.metrics, 'step').step;
+ const rowClass = 'gl-p-3!';
+
+ const cssClasses = { thClass: rowClass, tdClass: rowClass };
+
+ const fields = range(maxStep + 1).map((step) => ({
+ key: step.toString(),
+ label: sprintf(__('Step %{step}'), { step }),
+ ...cssClasses,
+ }));
+
+ return [{ key: 'name', label: __('Metric'), ...cssClasses }, ...fields];
+ },
+ metricsTableItems() {
+ const items = {};
+ this.candidate.metrics.forEach((metric) => {
+ const metricRow = items[metric.name] || { name: metric.name };
+ metricRow[metric.step] = metric.value;
+ items[metric.name] = metricRow;
+ });
+
+ return Object.values(items);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <section v-if="showInfoSection" class="gl-mb-6">
+ <table class="candidate-details">
+ <tbody>
+ <detail-row :label="$options.i18n.ID_LABEL">
+ {{ info.iid }}
+ </detail-row>
+
+ <detail-row :label="$options.i18n.MLFLOW_ID_LABEL">{{ info.eid }}</detail-row>
+
+ <detail-row :label="$options.i18n.STATUS_LABEL">{{ info.status }}</detail-row>
+
+ <detail-row :label="$options.i18n.EXPERIMENT_LABEL">
+ <gl-link :href="info.pathToExperiment">
+ {{ info.experimentName }}
+ </gl-link>
+ </detail-row>
+
+ <detail-row v-if="info.pathToArtifact" :label="$options.i18n.ARTIFACTS_LABEL">
+ <gl-link :href="info.pathToArtifact">
+ {{ $options.i18n.ARTIFACTS_LABEL }}
+ </gl-link>
+ </detail-row>
+ </tbody>
+ </table>
+ </section>
+
+ <section class="gl-mb-6">
+ <h3 :class="$options.HEADER_CLASSES">{{ $options.i18n.CI_SECTION_LABEL }}</h3>
+
+ <table v-if="ciJob" class="candidate-details">
+ <tbody>
+ <detail-row
+ :label="$options.i18n.JOB_LABEL"
+ :section-label="$options.i18n.CI_SECTION_LABEL"
+ >
+ <gl-link :href="ciJob.path">
+ {{ ciJob.name }}
+ </gl-link>
+ </detail-row>
+
+ <detail-row v-if="ciJob.user" :label="$options.i18n.CI_USER_LABEL">
+ <gl-avatar-labeled label="" :size="24" :src="ciJob.user.avatar">
+ <gl-link :href="ciJob.user.path">
+ {{ ciJob.user.name }}
+ </gl-link>
+ </gl-avatar-labeled>
+ </detail-row>
+
+ <detail-row v-if="ciJob.mergeRequest" :label="$options.i18n.CI_MR_LABEL">
+ <gl-link :href="ciJob.mergeRequest.path">
+ !{{ ciJob.mergeRequest.iid }} {{ ciJob.mergeRequest.title }}
+ </gl-link>
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_CI_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h3 :class="$options.HEADER_CLASSES">{{ $options.i18n.PARAMETERS_LABEL }}</h3>
+
+ <table v-if="hasParameters" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.params" :key="item.name" :label="item.name">
+ {{ item.value }}
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_PARAMETERS_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h3 :class="$options.HEADER_CLASSES">{{ $options.i18n.METADATA_LABEL }}</h3>
+
+ <table v-if="hasMetadata" class="candidate-details">
+ <tbody>
+ <detail-row v-for="item in candidate.metadata" :key="item.name" :label="item.name">
+ {{ item.value }}
+ </detail-row>
+ </tbody>
+ </table>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METADATA_MESSAGE }}</div>
+ </section>
+
+ <section class="gl-mb-6">
+ <h3 :class="$options.HEADER_CLASSES">{{ $options.i18n.PERFORMANCE_LABEL }}</h3>
+
+ <div v-if="hasMetrics" class="gl-overflow-x-auto">
+ <gl-table-lite
+ :items="metricsTableItems"
+ :fields="metricsTableFields"
+ class="gl-w-auto"
+ hover
+ />
+ </div>
+
+ <div v-else class="gl-text-secondary">{{ $options.i18n.NO_METRICS_MESSAGE }}</div>
+ </section>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue b/app/assets/javascripts/ml/model_registry/components/candidate_detail_row.vue
index 8c7460940a0..8c7460940a0 100644
--- a/app/assets/javascripts/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue
+++ b/app/assets/javascripts/ml/model_registry/components/candidate_detail_row.vue
diff --git a/app/assets/javascripts/ml/model_registry/components/candidate_list.vue b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue
new file mode 100644
index 00000000000..fc24a538293
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue
@@ -0,0 +1,139 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import CandidateListRow from '~/ml/model_registry/components/candidate_list_row.vue';
+import { makeLoadCandidatesErrorMessage, NO_CANDIDATES_LABEL } from '../translations';
+import getModelCandidatesQuery from '../graphql/queries/get_model_candidates.query.graphql';
+import { GRAPHQL_PAGE_SIZE } from '../constants';
+
+export default {
+ name: 'MlCandidateList',
+ components: {
+ GlAlert,
+ CandidateListRow,
+ PackagesListLoader,
+ RegistryList,
+ },
+ props: {
+ modelId: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ modelVersions: {},
+ errorMessage: undefined,
+ };
+ },
+ apollo: {
+ candidates: {
+ query: getModelCandidatesQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.mlModel?.candidates ?? {};
+ },
+ error(error) {
+ this.errorMessage = makeLoadCandidatesErrorMessage(error.message);
+ Sentry.captureException(error);
+ },
+ },
+ },
+ computed: {
+ gid() {
+ return convertToGraphQLId('Ml::Model', this.modelId);
+ },
+ isListEmpty() {
+ return this.count === 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.candidates.loading;
+ },
+ pageInfo() {
+ return this.candidates?.pageInfo ?? {};
+ },
+ listTitle() {
+ return n__('%d candidate', '%d candidates', this.count);
+ },
+ queryVariables() {
+ return {
+ id: this.gid,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ },
+ items() {
+ return this.candidates?.nodes ?? [];
+ },
+ count() {
+ return this.candidates?.count ?? 0;
+ },
+ },
+ methods: {
+ fetchPage({ first = null, last = null, before = null, after = null } = {}) {
+ const variables = {
+ ...this.queryVariables,
+ first,
+ last,
+ before,
+ after,
+ };
+
+ this.$apollo.queries.candidates.fetchMore({
+ variables,
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return fetchMoreResult;
+ },
+ });
+ },
+ fetchPreviousCandidatesPage() {
+ this.fetchPage({
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ });
+ },
+ fetchNextCandidatesPage() {
+ this.fetchPage({
+ first: GRAPHQL_PAGE_SIZE,
+ after: this.pageInfo?.endCursor,
+ });
+ },
+ },
+ i18n: {
+ NO_CANDIDATES_LABEL,
+ },
+};
+</script>
+<template>
+ <div>
+ <div v-if="isLoading">
+ <packages-list-loader />
+ </div>
+ <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
+ errorMessage
+ }}</gl-alert>
+ <div v-else-if="isListEmpty" class="gl-text-secondary">
+ {{ $options.i18n.NO_CANDIDATES_LABEL }}
+ </div>
+ <div v-else>
+ <registry-list
+ :hidden-delete="true"
+ :is-loading="isLoading"
+ :items="items"
+ :pagination="pageInfo"
+ :title="listTitle"
+ @prev-page="fetchPreviousCandidatesPage"
+ @next-page="fetchNextCandidatesPage"
+ >
+ <template #default="{ item }">
+ <candidate-list-row :candidate="item" />
+ </template>
+ </registry-list>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/candidate_list_row.vue b/app/assets/javascripts/ml/model_registry/components/candidate_list_row.vue
new file mode 100644
index 00000000000..24248c0981b
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/candidate_list_row.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ name: 'MlCandidateListRow',
+ components: {
+ ListItem,
+ GlLink,
+ GlTruncate,
+ GlSprintf,
+ TimeAgoTooltip,
+ },
+ props: {
+ candidate: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pathToDetails() {
+ return this.candidate._links?.showPath;
+ },
+ },
+};
+</script>
+
+<template>
+ <list-item v-bind="$attrs">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-link class="gl-text-body" :href="pathToDetails">
+ <gl-truncate :text="candidate.name" />
+ </gl-link>
+ </div>
+ </template>
+
+ <template #left-secondary>
+ <span>
+ <gl-sprintf :message="__('Created %{timestamp}')">
+ <template #timestamp>
+ <time-ago-tooltip :time="candidate.createdAt" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/empty_state.vue b/app/assets/javascripts/ml/model_registry/components/empty_state.vue
new file mode 100644
index 00000000000..017ddba78f1
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/empty_state.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlEmptyState } from '@gitlab/ui';
+import emptySvgUrl from '@gitlab/svgs/dist/illustrations/empty-state/empty-dag-md.svg?url';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { s__ } from '~/locale';
+import { MODEL_ENTITIES } from '../constants';
+
+const emptyStateTranslations = {
+ [MODEL_ENTITIES.model]: {
+ title: s__('MlModelRegistry|Start tracking your machine learning models'),
+ description: s__('MlModelRegistry|Store and manage your machine learning models and versions'),
+ createNew: s__('MlModelRegistry|Add a model'),
+ },
+ [MODEL_ENTITIES.modelVersion]: {
+ title: s__('MlModelRegistry|Manage versions of your machine learning model'),
+ description: s__('MlModelRegistry|Use versions to track performance, parameters, and metadata'),
+ createNew: s__('MlModelRegistry|Create a model version'),
+ },
+};
+
+export default {
+ components: {
+ GlEmptyState,
+ },
+ props: {
+ entityType: {
+ type: String,
+ required: true,
+ validator(value) {
+ return MODEL_ENTITIES[value] !== undefined;
+ },
+ },
+ },
+ computed: {
+ emptyStateValues() {
+ return {
+ ...emptyStateTranslations[this.entityType],
+ helpPath: helpPagePath('user/project/ml/model_registry/index', {
+ anchor: 'creating-machine-learning-models-and-model-versions',
+ }),
+ emptySvgPath: emptySvgUrl,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-empty-state
+ :title="emptyStateValues.title"
+ :primary-button-text="emptyStateValues.createNew"
+ :primary-button-link="emptyStateValues.helpPath"
+ :svg-path="emptyStateValues.emptySvgPath"
+ :svg-height="null"
+ :description="emptyStateValues.description"
+ class="gl-py-8"
+ />
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue
new file mode 100644
index 00000000000..8d3e8cf2023
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_detail.vue
@@ -0,0 +1,61 @@
+<script>
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_PACKAGES_PACKAGE } from '~/graphql_shared/constants';
+import * as i18n from '../translations';
+import CandidateDetail from './candidate_detail.vue';
+
+export default {
+ name: 'ModelVersionDetail',
+ components: {
+ PackageFiles: () =>
+ import('~/packages_and_registries/package_registry/components/details/package_files.vue'),
+ CandidateDetail,
+ },
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ packageId() {
+ return convertToGraphQLId(TYPENAME_PACKAGES_PACKAGE, this.modelVersion.packageId);
+ },
+ projectPath() {
+ return this.modelVersion.projectPath;
+ },
+ packageType() {
+ return 'ml_model';
+ },
+ },
+ i18n,
+};
+</script>
+
+<template>
+ <div>
+ <h3 class="gl-font-lg gl-mt-5">{{ $options.i18n.DESCRIPTION_LABEL }}</h3>
+
+ <div v-if="modelVersion.description">
+ {{ modelVersion.description }}
+ </div>
+ <div v-else class="gl-text-secondary">
+ {{ $options.i18n.NO_DESCRIPTION_PROVIDED_LABEL }}
+ </div>
+
+ <template v-if="modelVersion.packageId">
+ <package-files
+ :package-id="packageId"
+ :project-path="projectPath"
+ :package-type="packageType"
+ />
+ </template>
+
+ <div class="gl-mt-5">
+ <span class="gl-font-weight-bold">{{ $options.i18n.MLFLOW_ID_LABEL }}:</span>
+ {{ modelVersion.candidate.info.eid }}
+ </div>
+
+ <candidate-detail :candidate="modelVersion.candidate" :show-info-section="false" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_list.vue b/app/assets/javascripts/ml/model_registry/components/model_version_list.vue
new file mode 100644
index 00000000000..6b44cb2f613
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_list.vue
@@ -0,0 +1,137 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import { n__ } from '~/locale';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { makeLoadVersionsErrorMessage } from '~/ml/model_registry/translations';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import getModelVersionsQuery from '../graphql/queries/get_model_versions.query.graphql';
+import { GRAPHQL_PAGE_SIZE, MODEL_ENTITIES } from '../constants';
+import EmptyState from './empty_state.vue';
+import ModelVersionRow from './model_version_row.vue';
+
+export default {
+ components: {
+ EmptyState,
+ GlAlert,
+ ModelVersionRow,
+ PackagesListLoader,
+ RegistryList,
+ },
+ props: {
+ modelId: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ modelVersions: {},
+ errorMessage: undefined,
+ };
+ },
+ apollo: {
+ modelVersions: {
+ query: getModelVersionsQuery,
+ variables() {
+ return this.queryVariables;
+ },
+ update(data) {
+ return data.mlModel?.versions ?? {};
+ },
+ error(error) {
+ this.errorMessage = makeLoadVersionsErrorMessage(error.message);
+ Sentry.captureException(error);
+ },
+ },
+ },
+ computed: {
+ gid() {
+ return convertToGraphQLId('Ml::Model', this.modelId);
+ },
+ isListEmpty() {
+ return this.count === 0;
+ },
+ isLoading() {
+ return this.$apollo.queries.modelVersions.loading;
+ },
+ pageInfo() {
+ return this.modelVersions?.pageInfo ?? {};
+ },
+ listTitle() {
+ return n__('%d version', '%d versions', this.versions.length);
+ },
+ queryVariables() {
+ return {
+ id: this.gid,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+ },
+ versions() {
+ return this.modelVersions?.nodes ?? [];
+ },
+ count() {
+ return this.modelVersions?.count ?? 0;
+ },
+ },
+ methods: {
+ fetchPreviousVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo?.startCursor,
+ };
+ this.$apollo.queries.modelVersions.fetchMore({
+ variables,
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return fetchMoreResult;
+ },
+ });
+ },
+ fetchNextVersionsPage() {
+ const variables = {
+ ...this.queryVariables,
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.pageInfo?.endCursor,
+ };
+
+ this.$apollo.queries.modelVersions.fetchMore({
+ variables,
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return fetchMoreResult;
+ },
+ });
+ },
+ },
+ modelVersionEntity: MODEL_ENTITIES.modelVersion,
+};
+</script>
+<template>
+ <div>
+ <div v-if="isLoading">
+ <packages-list-loader />
+ </div>
+ <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">{{
+ errorMessage
+ }}</gl-alert>
+ <empty-state v-else-if="isListEmpty" :entity-type="$options.modelVersionEntity" />
+ <div v-else>
+ <registry-list
+ :hidden-delete="true"
+ :is-loading="isLoading"
+ :items="versions"
+ :pagination="pageInfo"
+ :title="listTitle"
+ @prev-page="fetchPreviousVersionsPage"
+ @next-page="fetchNextVersionsPage"
+ >
+ <template #default="{ item }">
+ <model-version-row :model-version="item" />
+ </template>
+ </registry-list>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/model_version_row.vue b/app/assets/javascripts/ml/model_registry/components/model_version_row.vue
new file mode 100644
index 00000000000..7e024ff546d
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_row.vue
@@ -0,0 +1,49 @@
+<script>
+import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+export default {
+ name: 'MlModelVersionRow',
+ components: {
+ ListItem,
+ GlLink,
+ GlTruncate,
+ GlSprintf,
+ TimeAgoTooltip,
+ },
+ props: {
+ modelVersion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ pathToDetails() {
+ return this.modelVersion._links?.showPath;
+ },
+ },
+};
+</script>
+
+<template>
+ <list-item v-bind="$attrs">
+ <template #left-primary>
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-link class="gl-text-body" :href="pathToDetails">
+ <gl-truncate :text="modelVersion.version" />
+ </gl-link>
+ </div>
+ </template>
+
+ <template #left-secondary>
+ <span>
+ <gl-sprintf :message="__('Created %{timestamp}')">
+ <template #timestamp>
+ <time-ago-tooltip :time="modelVersion.createdAt" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </template>
+ </list-item>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/constants.js b/app/assets/javascripts/ml/model_registry/constants.js
index 10c21ec4f12..02f9508b4c8 100644
--- a/app/assets/javascripts/ml/model_registry/constants.js
+++ b/app/assets/javascripts/ml/model_registry/constants.js
@@ -11,3 +11,10 @@ export const BASE_SORT_FIELDS = Object.freeze([
label: s__('MlExperimentTracking|Created at'),
},
]);
+
+export const GRAPHQL_PAGE_SIZE = 30;
+
+export const MODEL_ENTITIES = {
+ model: 'model',
+ modelVersion: 'modelVersion',
+};
diff --git a/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_candidates.query.graphql b/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_candidates.query.graphql
new file mode 100644
index 00000000000..81875cf509e
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_candidates.query.graphql
@@ -0,0 +1,28 @@
+query getModelCandidates(
+ $id: MlModelID!
+ $first: Int
+ $last: Int
+ $after: String
+ $before: String
+) {
+ mlModel(id: $id) {
+ id
+ candidates(after: $after, before: $before, first: $first, last: $last) {
+ count
+ nodes {
+ id
+ name
+ createdAt
+ _links {
+ showPath
+ }
+ }
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ startCursor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_versions.query.graphql b/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_versions.query.graphql
new file mode 100644
index 00000000000..1b48a67a0bd
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/graphql/queries/get_model_versions.query.graphql
@@ -0,0 +1,22 @@
+query getModelVersions($id: MlModelID!, $first: Int, $last: Int, $after: String, $before: String) {
+ mlModel(id: $id) {
+ id
+ versions(after: $after, before: $before, first: $first, last: $last) {
+ count
+ nodes {
+ id
+ version
+ createdAt
+ _links {
+ showPath
+ }
+ }
+ pageInfo {
+ hasNextPage
+ hasPreviousPage
+ endCursor
+ startCursor
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
index 89b3f45ed94..968ec83434d 100644
--- a/app/assets/javascripts/ml/model_registry/translations.js
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -1,16 +1,45 @@
-import { s__, n__ } from '~/locale';
+import { __, s__, n__, sprintf } from '~/locale';
export const MODEL_DETAILS_TAB_LABEL = s__('MlModelRegistry|Details');
export const MODEL_OTHER_VERSIONS_TAB_LABEL = s__('MlModelRegistry|Versions');
export const MODEL_CANDIDATES_TAB_LABEL = s__('MlModelRegistry|Version candidates');
export const LATEST_VERSION_LABEL = s__('MlModelRegistry|Latest version');
-export const NO_VERSIONS_LABEL = s__('MlModelRegistry|This model has no versions');
export const versionsCountLabel = (versionCount) =>
n__('MlModelRegistry|%d version', 'MlModelRegistry|%d versions', versionCount);
export const TITLE_LABEL = s__('MlModelRegistry|Model registry');
-export const NO_MODELS_LABEL = s__('MlModelRegistry|No models registered in this project');
export const modelsCountLabel = (modelCount) =>
n__('MlModelRegistry|%d model', 'MlModelRegistry|%d models', modelCount);
+
+export const DESCRIPTION_LABEL = __('Description');
+export const NO_DESCRIPTION_PROVIDED_LABEL = s__('MlModelRegistry|No description provided');
+export const INFO_LABEL = s__('MlModelRegistry|Info');
+export const ID_LABEL = s__('MlModelRegistry|ID');
+export const MLFLOW_ID_LABEL = s__('MlModelRegistry|MLflow run ID');
+export const STATUS_LABEL = s__('MlModelRegistry|Status');
+export const EXPERIMENT_LABEL = s__('MlModelRegistry|Experiment');
+export const ARTIFACTS_LABEL = s__('MlModelRegistry|Artifacts');
+export const PARAMETERS_LABEL = s__('MlModelRegistry|Parameters');
+export const PERFORMANCE_LABEL = s__('MlModelRegistry|Model performance');
+export const METADATA_LABEL = s__('MlModelRegistry|Metadata');
+export const NO_PARAMETERS_MESSAGE = s__('MlModelRegistry|No logged parameters');
+export const NO_METRICS_MESSAGE = s__('MlModelRegistry|No logged metrics');
+export const NO_METADATA_MESSAGE = s__('MlModelRegistry|No logged metadata');
+export const NO_CI_MESSAGE = s__('MlModelRegistry|Candidate not linked to a CI build');
+export const CI_SECTION_LABEL = s__('MlModelRegistry|CI Info');
+export const JOB_LABEL = __('Job');
+export const CI_USER_LABEL = s__('MlModelRegistry|Triggered by');
+export const CI_MR_LABEL = __('Merge request');
+
+export const makeLoadVersionsErrorMessage = (message) =>
+ sprintf(s__('MlModelRegistry|Failed to load model versions with error: %{message}'), {
+ message,
+ });
+
+export const NO_CANDIDATES_LABEL = s__('MlModelRegistry|This model has no candidates');
+export const makeLoadCandidatesErrorMessage = (message) =>
+ sprintf(s__('MlModelRegistry|Failed to load model candidates with error: %{message}'), {
+ message,
+ });
diff --git a/app/assets/javascripts/nav/components/new_nav_toggle.vue b/app/assets/javascripts/nav/components/new_nav_toggle.vue
deleted file mode 100644
index 4e5d6b0ce6c..00000000000
--- a/app/assets/javascripts/nav/components/new_nav_toggle.vue
+++ /dev/null
@@ -1,105 +0,0 @@
-<script>
-import { GlToggle, GlDisclosureDropdownItem } from '@gitlab/ui';
-import axios from '~/lib/utils/axios_utils';
-import { createAlert } from '~/alert';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-
-export default {
- i18n: {
- sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
- toggleMenuItemLabel: s__('NorthstarNavigation|New navigation'),
- toggleLabel: s__('NorthstarNavigation|Toggle new navigation'),
- updateError: s__(
- 'NorthstarNavigation|Could not update the new navigation preference. Please try again later.',
- ),
- },
- components: {
- GlToggle,
- GlDisclosureDropdownItem,
- },
- props: {
- enabled: {
- type: Boolean,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
- newNavigation: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- data() {
- return {
- isEnabled: this.enabled,
- };
- },
- methods: {
- toggleNav() {
- this.isEnabled = !this.isEnabled;
- this.updateAndReload();
- },
- async updateAndReload() {
- try {
- await axios.put(this.endpoint, { user: { use_new_navigation: this.isEnabled } });
-
- Tracking.event(undefined, 'click_toggle', {
- label: this.enabled ? 'disable_new_nav_beta' : 'enable_new_nav_beta',
- property: this.enabled ? 'nav_user_menu' : 'navigation_top',
- });
-
- window.location.reload();
- } catch (error) {
- createAlert({
- message: this.$options.i18n.updateError,
- error,
- });
- }
- },
- },
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown-item v-if="newNavigation" @action="toggleNav">
- <div class="gl-new-dropdown-item-content">
- <div
- class="gl-new-dropdown-item-text-wrapper gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-py-2! gl-gap-3"
- >
- {{ $options.i18n.toggleMenuItemLabel }}
- <gl-toggle
- class="gl-flex-grow-0!"
- :value="isEnabled"
- :label="$options.i18n.toggleLabel"
- label-position="hidden"
- />
- </div>
- </div>
- </gl-disclosure-dropdown-item>
-
- <li v-else>
- <div
- class="gl-px-4 gl-py-2 gl-display-flex gl-justify-content-space-between gl-align-items-center"
- >
- <b>{{ $options.i18n.sectionTitle }}</b>
- </div>
-
- <div
- class="menu-item gl-cursor-pointer gl-display-flex! gl-justify-content-space-between gl-align-items-center gl-gap-3"
- @click.prevent.stop="toggleNav"
- >
- {{ $options.i18n.toggleMenuItemLabel }}
- <gl-toggle
- class="gl-flex-grow-0!"
- :value="isEnabled"
- :label="$options.i18n.toggleLabel"
- label-position="hidden"
- data-testid="new_navigation_toggle"
- />
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/nav/components/responsive_app.vue b/app/assets/javascripts/nav/components/responsive_app.vue
deleted file mode 100644
index 68a39f862fc..00000000000
--- a/app/assets/javascripts/nav/components/responsive_app.vue
+++ /dev/null
@@ -1,95 +0,0 @@
-<script>
-import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
-import { BV_DROPDOWN_SHOW, BV_DROPDOWN_HIDE } from '~/lib/utils/constants';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import { resetMenuItemsActive } from '../utils';
-import ResponsiveHeader from './responsive_header.vue';
-import ResponsiveHome from './responsive_home.vue';
-import TopNavContainerView from './top_nav_container_view.vue';
-
-export default {
- components: {
- KeepAliveSlots,
- ResponsiveHeader,
- ResponsiveHome,
- TopNavContainerView,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- activeView: 'home',
- hasMobileOverlay: false,
- };
- },
- computed: {
- nav() {
- return resetMenuItemsActive(this.navData);
- },
- },
- created() {
- this.$root.$on(BV_DROPDOWN_SHOW, this.showMobileOverlay);
- this.$root.$on(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
- },
- beforeDestroy() {
- this.$root.$off(BV_DROPDOWN_SHOW, this.showMobileOverlay);
- this.$root.$off(BV_DROPDOWN_HIDE, this.hideMobileOverlay);
- },
- methods: {
- onMenuItemClick({ view }) {
- if (view) {
- this.activeView = view;
- }
- },
- showMobileOverlay() {
- this.hasMobileOverlay = true;
- },
- hideMobileOverlay() {
- this.hasMobileOverlay = false;
- },
- },
- FREQUENT_ITEMS_PROJECTS,
- FREQUENT_ITEMS_GROUPS,
-};
-</script>
-
-<template>
- <div>
- <div
- class="mobile-overlay"
- :class="{ 'mobile-nav-open': hasMobileOverlay }"
- data-testid="mobile-overlay"
- ></div>
- <keep-alive-slots :slot-key="activeView">
- <template #home>
- <responsive-home :nav-data="nav" @menu-item-click="onMenuItemClick" />
- </template>
- <template #projects>
- <responsive-header @menu-item-click="onMenuItemClick">
- {{ __('Projects') }}
- </responsive-header>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
- container-class="gl-px-3"
- v-bind="nav.views.projects"
- />
- </template>
- <template #groups>
- <responsive-header @menu-item-click="onMenuItemClick">
- {{ __('Groups') }}
- </responsive-header>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
- container-class="gl-px-3"
- v-bind="nav.views.groups"
- />
- </template>
- </keep-alive-slots>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/responsive_header.vue b/app/assets/javascripts/nav/components/responsive_header.vue
deleted file mode 100644
index e29b4a67383..00000000000
--- a/app/assets/javascripts/nav/components/responsive_header.vue
+++ /dev/null
@@ -1,37 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import TopNavMenuItem from './top_nav_menu_item.vue';
-
-export default {
- components: {
- TopNavMenuItem,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- computed: {
- menuItem() {
- return {
- id: 'home',
- view: 'home',
- icon: 'chevron-lg-left',
- };
- },
- },
-};
-</script>
-
-<template>
- <header class="gl-py-4 gl-display-flex gl-align-items-center">
- <top-nav-menu-item
- v-gl-tooltip="{ title: s__('TopNav|Go back') }"
- class="gl-p-3!"
- :menu-item="menuItem"
- icon-only
- @click="$emit('menu-item-click', menuItem)"
- />
- <span class="gl-font-size-h2 gl-font-weight-bold gl-ml-2">
- <slot></slot>
- </span>
- </header>
-</template>
diff --git a/app/assets/javascripts/nav/components/responsive_home.vue b/app/assets/javascripts/nav/components/responsive_home.vue
deleted file mode 100644
index 371b252a6ba..00000000000
--- a/app/assets/javascripts/nav/components/responsive_home.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { GlTooltipDirective } from '@gitlab/ui';
-import TopNavMenuItem from './top_nav_menu_item.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-import TopNavNewDropdown from './top_nav_new_dropdown.vue';
-
-const NEW_VIEW = 'new';
-const SEARCH_VIEW = 'search';
-
-export default {
- components: {
- TopNavMenuItem,
- TopNavMenuSections,
- TopNavNewDropdown,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- computed: {
- menuSections() {
- return [
- { id: 'primary', menuItems: this.navData.primary },
- { id: 'secondary', menuItems: this.navData.secondary },
- ].filter((x) => x.menuItems?.length);
- },
- newDropdownViewModel() {
- return this.navData.views[NEW_VIEW];
- },
- searchMenuItem() {
- return this.navData.views[SEARCH_VIEW];
- },
- },
-};
-</script>
-
-<template>
- <div>
- <header class="gl-display-flex gl-align-items-center gl-py-4 gl-pl-4">
- <h1 class="gl-m-0 gl-font-size-h2 gl-reset-color gl-mr-auto">{{ __('Menu') }}</h1>
- <top-nav-menu-item
- v-if="searchMenuItem"
- v-gl-tooltip="{ title: searchMenuItem.title }"
- class="gl-ml-3"
- :menu-item="searchMenuItem"
- icon-only
- />
- <top-nav-new-dropdown
- v-if="newDropdownViewModel"
- v-gl-tooltip="{ title: newDropdownViewModel.title }"
- :view-model="newDropdownViewModel"
- class="gl-ml-3"
- data-testid="mobile_new_dropdown"
- />
- </header>
- <top-nav-menu-sections class="gl-h-full" :sections="menuSections" v-on="$listeners" />
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_app.vue b/app/assets/javascripts/nav/components/top_nav_app.vue
deleted file mode 100644
index 22c77e9ae32..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_app.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlNav, GlIcon, GlNavItemDropdown, GlDropdownForm, GlTooltipDirective } from '@gitlab/ui';
-import Tracking from '~/tracking';
-import TopNavDropdownMenu from './top_nav_dropdown_menu.vue';
-
-export default {
- components: {
- GlIcon,
- GlNav,
- GlNavItemDropdown,
- GlDropdownForm,
- TopNavDropdownMenu,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- navData: {
- type: Object,
- required: true,
- },
- },
- methods: {
- trackToggleEvent() {
- Tracking.event(undefined, 'click_nav', {
- label: 'hamburger_menu',
- property: 'navigation_top',
- });
- },
- },
-};
-</script>
-
-<template>
- <gl-nav class="navbar-sub-nav">
- <gl-nav-item-dropdown
- v-gl-tooltip.bottom="navData.menuTooltip"
- data-testid="navbar_dropdown"
- data-qa-title="Menu"
- menu-class="gl-mt-3! gl-max-w-none! gl-max-h-none! gl-sm-w-auto! js-top-nav-dropdown-menu"
- toggle-class="top-nav-toggle js-top-nav-dropdown-toggle gl-px-3!"
- no-flip
- no-caret
- @toggle="trackToggleEvent"
- >
- <template #button-content>
- <gl-icon name="hamburger" />
- <span v-if="navData.menuTitle" class="gl-ml-3">
- {{ navData.menuTitle }}
- </span>
- </template>
- <gl-dropdown-form>
- <top-nav-dropdown-menu
- :primary="navData.primary"
- :secondary="navData.secondary"
- :views="navData.views"
- />
- </gl-dropdown-form>
- </gl-nav-item-dropdown>
- </gl-nav>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_container_view.vue b/app/assets/javascripts/nav/components/top_nav_container_view.vue
deleted file mode 100644
index 36e4a278da9..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_container_view.vue
+++ /dev/null
@@ -1,81 +0,0 @@
-<script>
-import FrequentItemsApp from '~/frequent_items/components/app.vue';
-import eventHub from '~/frequent_items/event_hub';
-import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-
-export default {
- components: {
- FrequentItemsApp,
- TopNavMenuSections,
- VuexModuleProvider,
- },
- inheritAttrs: false,
- props: {
- frequentItemsVuexModule: {
- type: String,
- required: true,
- },
- frequentItemsDropdownType: {
- type: String,
- required: true,
- },
- currentItem: {
- type: Object,
- required: true,
- },
- containerClass: {
- type: String,
- required: false,
- default: '',
- },
- linksPrimary: {
- type: Array,
- required: false,
- default: () => [],
- },
- linksSecondary: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- computed: {
- menuSections() {
- return [
- { id: 'primary', menuItems: this.linksPrimary },
- { id: 'secondary', menuItems: this.linksSecondary },
- ].filter((x) => x.menuItems?.length);
- },
- currentItemTimestamped() {
- return {
- ...this.currentItem,
- lastAccessedOn: Date.now(),
- };
- },
- },
- mounted() {
- // For historic reasons, the frequent-items-app component requires this too start up.
- this.$nextTick(() => {
- eventHub.$emit(`${this.frequentItemsDropdownType}-dropdownOpen`);
- });
- },
-};
-</script>
-
-<template>
- <div class="top-nav-container-view gl-display-flex gl-flex-direction-column">
- <div
- class="frequent-items-dropdown-container gl-w-auto"
- :class="containerClass"
- data-testid="frequent-items-container"
- >
- <div class="frequent-items-dropdown-content gl-w-full! gl-pt-0!">
- <vuex-module-provider :vuex-module="frequentItemsVuexModule">
- <frequent-items-app :current-item="currentItemTimestamped" v-bind="$attrs" />
- </vuex-module-provider>
- </div>
- </div>
- <top-nav-menu-sections class="gl-mt-auto" :sections="menuSections" with-top-border />
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue b/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
deleted file mode 100644
index fa202a0574d..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_dropdown_menu.vue
+++ /dev/null
@@ -1,107 +0,0 @@
-<script>
-import { cloneDeep } from 'lodash';
-import { FREQUENT_ITEMS_PROJECTS, FREQUENT_ITEMS_GROUPS } from '~/frequent_items/constants';
-import KeepAliveSlots from '~/vue_shared/components/keep_alive_slots.vue';
-import TopNavContainerView from './top_nav_container_view.vue';
-import TopNavMenuSections from './top_nav_menu_sections.vue';
-
-export default {
- components: {
- KeepAliveSlots,
- TopNavContainerView,
- TopNavMenuSections,
- },
- props: {
- primary: {
- type: Array,
- required: false,
- default: () => [],
- },
- secondary: {
- type: Array,
- required: false,
- default: () => [],
- },
- views: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- },
- data() {
- // It's expected that primary & secondary never change, so these are treated as "init" props.
- // We need to clone so that we can mutate the data without mutating the props
- const menuSections = [
- { id: 'primary', menuItems: cloneDeep(this.primary) },
- { id: 'secondary', menuItems: cloneDeep(this.secondary) },
- ].filter((x) => x.menuItems?.length);
-
- return {
- menuSections,
- };
- },
- computed: {
- allMenuItems() {
- return this.menuSections.flatMap((x) => x.menuItems);
- },
- activeView() {
- const active = this.allMenuItems.find((x) => x.active);
-
- return active?.view;
- },
- menuClass() {
- if (!this.activeView) {
- return 'gl-w-full';
- }
-
- return '';
- },
- },
- methods: {
- onMenuItemClick({ id }) {
- this.allMenuItems.forEach((menuItem) => {
- this.$set(menuItem, 'active', id === menuItem.id);
- });
- },
- },
- FREQUENT_ITEMS_PROJECTS,
- FREQUENT_ITEMS_GROUPS,
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-align-items-stretch">
- <div
- class="gl-w-grid-size-30 gl-flex-shrink-0 gl-bg-gray-10 gl-p-3"
- :class="menuClass"
- data-testid="menu-sidebar"
- >
- <top-nav-menu-sections
- :sections="menuSections"
- :is-primary-section="true"
- @menu-item-click="onMenuItemClick"
- />
- </div>
- <keep-alive-slots
- v-show="activeView"
- :slot-key="activeView"
- class="gl-w-grid-size-40 gl-overflow-hidden gl-p-3"
- data-testid="menu-subview"
- >
- <template #projects>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_PROJECTS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_PROJECTS.vuexModule"
- v-bind="views.projects"
- />
- </template>
- <template #groups>
- <top-nav-container-view
- :frequent-items-dropdown-type="$options.FREQUENT_ITEMS_GROUPS.namespace"
- :frequent-items-vuex-module="$options.FREQUENT_ITEMS_GROUPS.vuexModule"
- v-bind="views.groups"
- />
- </template>
- </keep-alive-slots>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_item.vue b/app/assets/javascripts/nav/components/top_nav_menu_item.vue
deleted file mode 100644
index bf1fd691ca8..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_menu_item.vue
+++ /dev/null
@@ -1,52 +0,0 @@
-<script>
-import { GlButton, GlIcon } from '@gitlab/ui';
-import { kebabCase, mapKeys } from 'lodash';
-
-const getDataKey = (key) => `data-${kebabCase(key)}`;
-
-const ACTIVE_CLASS = 'gl-shadow-none! gl-font-weight-bold! active';
-
-export default {
- components: {
- GlButton,
- GlIcon,
- },
- props: {
- menuItem: {
- type: Object,
- required: true,
- },
- iconOnly: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- computed: {
- dataAttrs() {
- return mapKeys(this.menuItem.data || {}, (value, key) => getDataKey(key));
- },
- },
- ACTIVE_CLASS,
-};
-</script>
-
-<template>
- <gl-button
- category="tertiary"
- :href="menuItem.href"
- class="top-nav-menu-item gl-display-block gl-pr-3!"
- :class="[menuItem.css_class, { [$options.ACTIVE_CLASS]: menuItem.active }]"
- :aria-label="menuItem.title"
- v-bind="dataAttrs"
- v-on="$listeners"
- >
- <span class="gl-display-flex">
- <gl-icon v-if="menuItem.icon" :name="menuItem.icon" :class="{ 'gl-mr-3!': !iconOnly }" />
- <template v-if="!iconOnly">
- {{ menuItem.title }}
- <gl-icon v-if="menuItem.view" name="chevron-right" class="gl-ml-auto" />
- </template>
- </span>
- </gl-button>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue b/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
deleted file mode 100644
index 1f3f11dc624..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_menu_sections.vue
+++ /dev/null
@@ -1,82 +0,0 @@
-<script>
-import TopNavMenuItem from './top_nav_menu_item.vue';
-
-const BORDER_CLASSES = 'gl-pt-3 gl-border-1 gl-border-t-solid';
-
-export default {
- components: {
- TopNavMenuItem,
- },
- props: {
- sections: {
- type: Array,
- required: true,
- },
- withTopBorder: {
- type: Boolean,
- required: false,
- default: false,
- },
- isPrimarySection: {
- type: Boolean,
- required: false,
- default: false,
- },
- },
- methods: {
- onClick(menuItem) {
- // If we're a link, let's just do the default behavior so the view won't change
- if (menuItem.href) {
- return;
- }
-
- this.$emit('menu-item-click', menuItem);
- },
- getMenuSectionClasses(index) {
- // This is a method instead of a computed so we don't have to incur the cost of
- // creating a whole new array/object.
- const hasBorder = this.withTopBorder || index > 0;
- return {
- [BORDER_CLASSES]: hasBorder,
- 'gl-border-gray-100': hasBorder && this.isPrimarySection,
- 'gl-border-gray-50': hasBorder && !this.isPrimarySection,
- 'gl-mt-3': index > 0,
- };
- },
- },
- // Expose for unit tests
- BORDER_CLASSES,
-};
-</script>
-
-<template>
- <div class="gl-display-flex gl-align-items-stretch gl-flex-direction-column">
- <div
- v-for="({ id, menuItems }, sectionIndex) in sections"
- :key="id"
- :class="getMenuSectionClasses(sectionIndex)"
- data-testid="menu-section"
- >
- <template v-for="(menuItem, menuItemIndex) in menuItems">
- <strong
- v-if="menuItem.type == 'header'"
- :key="menuItem.title"
- class="gl-px-4 gl-py-2 gl-text-gray-900 gl-display-block"
- :class="{ 'gl-pt-3!': menuItemIndex > 0 }"
- data-testid="menu-header"
- >
- {{ menuItem.title }}
- </strong>
- <top-nav-menu-item
- v-else
- :key="menuItem.id"
- :menu-item="menuItem"
- data-testid="menu-item"
- class="gl-w-full"
- :class="{ 'gl-mt-1': menuItemIndex > 0 }"
- @click="onClick(menuItem)"
- />
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue b/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
deleted file mode 100644
index 2dfd77bc02e..00000000000
--- a/app/assets/javascripts/nav/components/top_nav_new_dropdown.vue
+++ /dev/null
@@ -1,73 +0,0 @@
-<script>
-import { GlDropdown, GlDropdownDivider, GlDropdownItem, GlDropdownSectionHeader } from '@gitlab/ui';
-import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
-import { TOP_NAV_INVITE_MEMBERS_COMPONENT } from '~/invite_members/constants';
-
-export default {
- components: {
- GlDropdown,
- GlDropdownDivider,
- GlDropdownItem,
- GlDropdownSectionHeader,
- InviteMembersTrigger,
- },
- props: {
- viewModel: {
- type: Object,
- required: true,
- },
- },
- computed: {
- sections() {
- return this.viewModel.menu_sections || [];
- },
- showHeaders() {
- return this.sections.length > 1;
- },
- },
- methods: {
- isInvitedMembers(menuItem) {
- return menuItem.component === TOP_NAV_INVITE_MEMBERS_COMPONENT;
- },
- },
-};
-</script>
-
-<template>
- <gl-dropdown
- toggle-class="top-nav-menu-item"
- icon="plus"
- :text="viewModel.title"
- category="tertiary"
- text-sr-only
- no-caret
- right
- >
- <template v-for="({ title, menu_items }, index) in sections">
- <gl-dropdown-divider v-if="index > 0" :key="`${index}_divider`" data-testid="divider" />
- <gl-dropdown-section-header v-if="showHeaders" :key="`${index}_header`" data-testid="header">
- {{ title }}
- </gl-dropdown-section-header>
- <template v-for="menuItem in menu_items">
- <invite-members-trigger
- v-if="isInvitedMembers(menuItem)"
- :key="`${index}_item_${menuItem.id}`"
- :trigger-element="`dropdown-${menuItem.data.trigger_element}`"
- :display-text="menuItem.title"
- :icon="menuItem.icon"
- :trigger-source="menuItem.data.trigger_source"
- />
- <gl-dropdown-item
- v-else
- :key="`${index}_item_${menuItem.id}`"
- link-class="top-nav-menu-item"
- :href="menuItem.href"
- data-testid="item"
- :data-qa-selector="`${menuItem.title.toLowerCase().replace(' ', '_')}_mobile_button`"
- >
- {{ menuItem.title }}
- </gl-dropdown-item>
- </template>
- </template>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/nav/index.js b/app/assets/javascripts/nav/index.js
deleted file mode 100644
index abd537d2c9a..00000000000
--- a/app/assets/javascripts/nav/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-// TODO: With the combined_menu feature flag removed, there's likely a better
-// way to slice up the async import (i.e., include trigger in main bundle, but
-// async import subviews. Don't do this at the cost of UX).
-// See https://gitlab.com/gitlab-org/gitlab/-/issues/336042
-const importModule = () => import(/* webpackChunkName: 'top_nav' */ './mount');
-
-const tryMountTopNav = async () => {
- const el = document.getElementById('js-top-nav');
-
- if (!el) {
- return;
- }
-
- const { mountTopNav } = await importModule();
-
- mountTopNav(el);
-};
-
-const tryMountTopNavResponsive = async () => {
- const el = document.getElementById('js-top-nav-responsive');
-
- if (!el) {
- return;
- }
-
- const { mountTopNavResponsive } = await importModule();
-
- mountTopNavResponsive(el);
-};
-
-export const initTopNav = async () => Promise.all([tryMountTopNav(), tryMountTopNavResponsive()]);
diff --git a/app/assets/javascripts/nav/mount.js b/app/assets/javascripts/nav/mount.js
deleted file mode 100644
index 0fc946bea76..00000000000
--- a/app/assets/javascripts/nav/mount.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import ResponsiveApp from './components/responsive_app.vue';
-import App from './components/top_nav_app.vue';
-import { createStore } from './stores';
-
-Vue.use(Vuex);
-
-const mount = (el, Component) => {
- const viewModel = JSON.parse(el.dataset.viewModel);
- const store = createStore();
-
- return new Vue({
- el,
- name: 'TopNavRoot',
- store,
- render(h) {
- return h(Component, {
- props: {
- navData: viewModel,
- },
- });
- },
- });
-};
-
-export const mountTopNav = (el) => mount(el, App);
-
-export const mountTopNavResponsive = (el) => mount(el, ResponsiveApp);
diff --git a/app/assets/javascripts/nav/stores/index.js b/app/assets/javascripts/nav/stores/index.js
deleted file mode 100644
index 7c8f93f042c..00000000000
--- a/app/assets/javascripts/nav/stores/index.js
+++ /dev/null
@@ -1,5 +0,0 @@
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import { createStoreOptions } from '~/frequent_items/store';
-
-export const createStore = () => new Vuex.Store(createStoreOptions());
diff --git a/app/assets/javascripts/nav/utils/index.js b/app/assets/javascripts/nav/utils/index.js
deleted file mode 100644
index 6d93818f0d3..00000000000
--- a/app/assets/javascripts/nav/utils/index.js
+++ /dev/null
@@ -1 +0,0 @@
-export * from './reset_menu_items_active';
diff --git a/app/assets/javascripts/nav/utils/reset_menu_items_active.js b/app/assets/javascripts/nav/utils/reset_menu_items_active.js
deleted file mode 100644
index 9b5d8e97c9c..00000000000
--- a/app/assets/javascripts/nav/utils/reset_menu_items_active.js
+++ /dev/null
@@ -1,14 +0,0 @@
-const resetActiveInArray = (arr) => arr?.map((menuItem) => ({ ...menuItem, active: false }));
-
-/**
- * This method sets `active: false` for the menu items within the given nav data.
- *
- * @returns navData with the menu items updated with `active: false`
- */
-export const resetMenuItemsActive = ({ primary, secondary, ...navData }) => {
- return {
- ...navData,
- primary: resetActiveInArray(primary),
- secondary: resetActiveInArray(secondary),
- };
-};
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index 329d6cfec00..87b55b19c08 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -3,7 +3,6 @@ import { GlAlert, GlButton, GlIcon, GlFormCheckbox, GlTooltipDirective } from '@
import $ from 'jquery';
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapGetters, mapState } from 'vuex';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
import { STATUS_CLOSED, STATUS_MERGED, STATUS_OPEN, STATUS_REOPENED } from '~/issues/constants';
import { containsSensitiveToken, confirmSensitiveAction } from '~/lib/utils/secret_detection';
@@ -17,6 +16,7 @@ import { badgeState } from '~/merge_requests/components/merge_request_header.vue
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import { trackSavedUsingEditor } from '~/vue_shared/components/markdown/tracking';
+import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch';
import * as constants from '../constants';
import eventHub from '../event_hub';
@@ -297,8 +297,10 @@ export default {
const toggleState = this.isOpen ? this.closeIssuable : this.reopenIssuable;
toggleState()
- .then(() => badgeState.updateStatus && badgeState.updateStatus())
- .then(refreshUserMergeRequestCounts)
+ .then(() => {
+ fetchUserCounts();
+ return badgeState?.updateStatus();
+ })
.catch(() =>
createAlert({
message: constants.toggleStateErrorMessage[this.noteableType][this.openState],
diff --git a/app/assets/javascripts/notes/components/discussion_locked_widget.vue b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
index a999b633f64..88fec0dfb9b 100644
--- a/app/assets/javascripts/notes/components/discussion_locked_widget.vue
+++ b/app/assets/javascripts/notes/components/discussion_locked_widget.vue
@@ -35,7 +35,7 @@ export default {
</script>
<template>
- <div class="disabled-comments gl-mt-3">
+ <div class="gl-mt-3" data-testid="disabled-comments">
<span
class="issuable-note-warning gl-display-inline-block gl-w-full gl-px-5 gl-py-4 gl-rounded-base"
>
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index f8a0db93e37..9aaae960b6f 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -435,7 +435,7 @@ export default {
category="primary"
variant="confirm"
data-testid="reply-comment-button"
- class="gl-sm-mr-3 gl-xs-mb-3 js-vue-issue-save js-comment-button"
+ class="gl-sm-mr-3 gl-mb-3 gl-sm-mb-0 js-vue-issue-save js-comment-button"
@click="handleUpdate()"
>
{{ saveButtonTitle }}
diff --git a/app/assets/javascripts/notes/components/notes_activity_header.vue b/app/assets/javascripts/notes/components/notes_activity_header.vue
index b4eeea8db02..be9c768ae60 100644
--- a/app/assets/javascripts/notes/components/notes_activity_header.vue
+++ b/app/assets/javascripts/notes/components/notes_activity_header.vue
@@ -38,11 +38,7 @@ export default {
},
computed: {
showAiActions() {
- return (
- this.resourceGlobalId &&
- (this.glFeatures.openaiExperimentation || this.glFeatures.aiGlobalSwitch) &&
- this.glFeatures.summarizeNotes
- );
+ return this.resourceGlobalId && this.glFeatures.summarizeNotes;
},
},
};
diff --git a/app/assets/javascripts/notifications/constants.js b/app/assets/javascripts/notifications/constants.js
index f5891c9acb5..8d004188c39 100644
--- a/app/assets/javascripts/notifications/constants.js
+++ b/app/assets/javascripts/notifications/constants.js
@@ -55,5 +55,6 @@ export const i18n = {
reopen_merge_request: s__('NotificationEvent|Reopen merge request'),
merge_when_pipeline_succeeds: s__('NotificationEvent|Merge when pipeline succeeds'),
success_pipeline: s__('NotificationEvent|Successful pipeline'),
+ approver: s__('NotificationEvent|Added as approver'),
},
};
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 32ff7fff128..3a793c9dc14 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -147,7 +147,7 @@ function filterObjToQueryParams(filterObj) {
const filterParams = new URLSearchParams();
Object.keys(SUPPORTED_FILTERS).forEach((filterName) => {
- const filterValues = filterObj[filterName] || [];
+ const filterValues = Array.isArray(filterObj[filterName]) ? filterObj[filterName] : [];
const validFilters = filterValues.filter((f) =>
SUPPORTED_FILTERS[filterName].includes(f.operator),
);
@@ -251,10 +251,26 @@ async function fetchOperations(operationsUrl, serviceName) {
}
}
-async function fetchMetrics(metricsUrl) {
+async function fetchMetrics(metricsUrl, { filters = {}, limit } = {}) {
try {
+ const params = new URLSearchParams();
+
+ if (Array.isArray(filters.search)) {
+ const searchPrefix = filters.search
+ .map((f) => f.value)
+ .join(' ')
+ .trim();
+
+ if (searchPrefix) {
+ params.append('starts_with', searchPrefix);
+ if (limit) {
+ params.append('limit', limit);
+ }
+ }
+ }
const { data } = await axios.get(metricsUrl, {
withCredentials: true,
+ params,
});
if (!Array.isArray(data.metrics)) {
throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
@@ -265,12 +281,46 @@ async function fetchMetrics(metricsUrl) {
}
}
+async function fetchMetric(searchUrl, name, type) {
+ try {
+ if (!name) {
+ throw new Error('fetchMetric() - metric name is required.');
+ }
+ if (!type) {
+ throw new Error('fetchMetric() - metric type is required.');
+ }
+
+ const params = new URLSearchParams({
+ mname: name,
+ mtype: type,
+ });
+
+ const { data } = await axios.get(searchUrl, {
+ params,
+ withCredentials: true,
+ });
+ if (!Array.isArray(data.results)) {
+ throw new Error('metrics are missing/invalid in the response'); // eslint-disable-line @gitlab/require-i18n-strings
+ }
+ return data.results;
+ } catch (e) {
+ return reportErrorAndThrow(e);
+ }
+}
+
export function buildClient(config) {
if (!config) {
throw new Error('No options object provided'); // eslint-disable-line @gitlab/require-i18n-strings
}
- const { provisioningUrl, tracingUrl, servicesUrl, operationsUrl, metricsUrl } = config;
+ const {
+ provisioningUrl,
+ tracingUrl,
+ servicesUrl,
+ operationsUrl,
+ metricsUrl,
+ metricsSearchUrl,
+ } = config;
if (typeof provisioningUrl !== 'string') {
throw new Error('provisioningUrl param must be a string');
@@ -292,6 +342,10 @@ export function buildClient(config) {
throw new Error('metricsUrl param must be a string');
}
+ if (typeof metricsSearchUrl !== 'string') {
+ throw new Error('metricsSearchUrl param must be a string');
+ }
+
return {
enableObservability: () => enableObservability(provisioningUrl),
isObservabilityEnabled: () => isObservabilityEnabled(provisioningUrl),
@@ -299,6 +353,7 @@ export function buildClient(config) {
fetchTrace: (traceId) => fetchTrace(tracingUrl, traceId),
fetchServices: () => fetchServices(servicesUrl),
fetchOperations: (serviceName) => fetchOperations(operationsUrl, serviceName),
- fetchMetrics: () => fetchMetrics(metricsUrl),
+ fetchMetrics: (options) => fetchMetrics(metricsUrl, options),
+ fetchMetric: (metricName, metricType) => fetchMetric(metricsSearchUrl, metricName, metricType),
};
}
diff --git a/app/assets/javascripts/organizations/constants.js b/app/assets/javascripts/organizations/constants.js
index 8ade37b169e..d3da072b38d 100644
--- a/app/assets/javascripts/organizations/constants.js
+++ b/app/assets/javascripts/organizations/constants.js
@@ -2,3 +2,5 @@ export const RESOURCE_TYPE_GROUPS = 'groups';
export const RESOURCE_TYPE_PROJECTS = 'projects';
export const ORGANIZATION_ROOT_ROUTE_NAME = 'root';
+
+export const ORGANIZATION_USERS_PER_PAGE = 20;
diff --git a/app/assets/javascripts/organizations/index/components/app.vue b/app/assets/javascripts/organizations/index/components/app.vue
index c47f4ed52c5..71a6aae4e93 100644
--- a/app/assets/javascripts/organizations/index/components/app.vue
+++ b/app/assets/javascripts/organizations/index/components/app.vue
@@ -2,7 +2,8 @@
import { GlButton } from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
-import organizationsQuery from '../graphql/organizations.query.graphql';
+import { DEFAULT_PER_PAGE } from '~/api';
+import organizationsQuery from '../../shared/graphql/queries/organizations.query.graphql';
import OrganizationsView from './organizations_view.vue';
export default {
@@ -21,14 +22,23 @@ export default {
inject: ['newOrganizationUrl'],
data() {
return {
- organizations: [],
+ organizations: {},
+ pagination: {
+ first: DEFAULT_PER_PAGE,
+ after: null,
+ last: null,
+ before: null,
+ },
};
},
apollo: {
organizations: {
query: organizationsQuery,
+ variables() {
+ return this.pagination;
+ },
update(data) {
- return data.currentUser.organizations.nodes;
+ return data.currentUser.organizations;
},
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
@@ -37,12 +47,30 @@ export default {
},
computed: {
showHeader() {
- return this.loading || this.organizations.length;
+ return this.loading || this.organizations.nodes?.length;
},
loading() {
return this.$apollo.queries.organizations.loading;
},
},
+ methods: {
+ onNext(endCursor) {
+ this.pagination = {
+ first: DEFAULT_PER_PAGE,
+ after: endCursor,
+ last: null,
+ before: null,
+ };
+ },
+ onPrev(startCursor) {
+ this.pagination = {
+ first: null,
+ after: null,
+ last: DEFAULT_PER_PAGE,
+ before: startCursor,
+ };
+ },
+ },
};
</script>
@@ -56,6 +84,11 @@ export default {
}}</gl-button>
</div>
</div>
- <organizations-view :organizations="organizations" :loading="loading" />
+ <organizations-view
+ :organizations="organizations"
+ :loading="loading"
+ @next="onNext"
+ @prev="onPrev"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_list.vue b/app/assets/javascripts/organizations/index/components/organizations_list.vue
index 539a4fcfe29..971d4710be2 100644
--- a/app/assets/javascripts/organizations/index/components/organizations_list.vue
+++ b/app/assets/javascripts/organizations/index/components/organizations_list.vue
@@ -1,26 +1,52 @@
<script>
+import { GlKeysetPagination } from '@gitlab/ui';
+import { __ } from '~/locale';
import OrganizationsListItem from './organizations_list_item.vue';
export default {
name: 'OrganizationsList',
components: {
OrganizationsListItem,
+ GlKeysetPagination,
+ },
+ i18n: {
+ prev: __('Prev'),
+ next: __('Next'),
},
props: {
organizations: {
- type: Array,
+ type: Object,
required: true,
},
},
+ computed: {
+ nodes() {
+ return this.organizations.nodes || [];
+ },
+ pageInfo() {
+ return this.organizations.pageInfo || {};
+ },
+ },
};
</script>
<template>
- <ul class="gl-p-0 gl-list-style-none">
- <organizations-list-item
- v-for="organization in organizations"
- :key="organization.id"
- :organization="organization"
- />
- </ul>
+ <div>
+ <ul class="gl-p-0 gl-list-style-none">
+ <organizations-list-item
+ v-for="organization in nodes"
+ :key="organization.id"
+ :organization="organization"
+ />
+ </ul>
+ <div v-if="pageInfo.hasNextPage || pageInfo.hasPreviousPage" class="gl-text-center gl-mt-5">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="$options.i18n.prev"
+ :next-text="$options.i18n.next"
+ @prev="$emit('prev', $event)"
+ @next="$emit('next', $event)"
+ />
+ </div>
+ </div>
</template>
diff --git a/app/assets/javascripts/organizations/index/components/organizations_view.vue b/app/assets/javascripts/organizations/index/components/organizations_view.vue
index 9720646bca3..59e94670826 100644
--- a/app/assets/javascripts/organizations/index/components/organizations_view.vue
+++ b/app/assets/javascripts/organizations/index/components/organizations_view.vue
@@ -20,9 +20,9 @@ export default {
inject: ['newOrganizationUrl', 'organizationsEmptyStateSvgPath'],
props: {
organizations: {
- type: Array,
+ type: Object,
required: false,
- default: () => [],
+ default: () => {},
},
loading: {
type: Boolean,
@@ -30,15 +30,22 @@ export default {
default: false,
},
},
+ computed: {
+ nodes() {
+ return this.organizations.nodes || [];
+ },
+ },
};
</script>
<template>
<gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
<organizations-list
- v-else-if="organizations.length"
+ v-else-if="nodes.length"
:organizations="organizations"
class="gl-border-t"
+ @prev="$emit('prev', $event)"
+ @next="$emit('next', $event)"
/>
<gl-empty-state
v-else
diff --git a/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql b/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
deleted file mode 100644
index 6090e2ec789..00000000000
--- a/app/assets/javascripts/organizations/index/graphql/organizations.query.graphql
+++ /dev/null
@@ -1,14 +0,0 @@
-query getCurrentUserOrganizations {
- currentUser {
- id
- organizations @client {
- nodes {
- id
- name
- descriptionHtml
- avatarUrl
- webUrl
- }
- }
- }
-}
diff --git a/app/assets/javascripts/organizations/index/index.js b/app/assets/javascripts/organizations/index/index.js
index 7cbb9c9165d..df9ed2a4cce 100644
--- a/app/assets/javascripts/organizations/index/index.js
+++ b/app/assets/javascripts/organizations/index/index.js
@@ -2,7 +2,6 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import resolvers from '../shared/graphql/resolvers';
import OrganizationsIndexApp from './components/app.vue';
export const initOrganizationsIndex = () => {
@@ -11,7 +10,7 @@ export const initOrganizationsIndex = () => {
if (!el) return false;
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(),
});
const { newOrganizationUrl, organizationsEmptyStateSvgPath } = convertObjectPropsToCamelCase(
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 725b6ac1ad8..0c363cf7c7f 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -6,7 +6,7 @@
export const organizations = [
{
- id: 'gid://gitlab/Organization/1',
+ id: 'gid://gitlab/Organizations::Organization/1',
name: 'My First Organization',
descriptionHtml:
'<p>This is where an organization can be explained in <strong>detail</strong></p>',
@@ -15,7 +15,7 @@ export const organizations = [
__typename: 'Organization',
},
{
- id: 'gid://gitlab/Organization/2',
+ id: 'gid://gitlab/Organizations::Organization/2',
name: 'Vegetation Co.',
descriptionHtml:
'<p> Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt Lorem ipsum dolor sit amet Lorem ipsum dolt<script>alert(1)</script></p>',
@@ -24,7 +24,7 @@ export const organizations = [
__typename: 'Organization',
},
{
- id: 'gid://gitlab/Organization/3',
+ id: 'gid://gitlab/Organizations::Organization/3',
name: 'Dude where is my car?',
descriptionHtml: null,
avatarUrl: null,
@@ -302,10 +302,48 @@ export const organizationCreateResponseWithErrors = {
},
};
-export const updateOrganizationResponse = {
- organization: {
- id: 'gid://gitlab/Organizations/1',
- name: 'Default updated',
+export const organizationUpdateResponse = {
+ data: {
+ organizationUpdate: {
+ organization: {
+ id: 'gid://gitlab/Organizations::Organization/1',
+ name: 'Default updated',
+ webUrl: 'http://127.0.0.1:3000/-/organizations/default',
+ },
+ errors: [],
+ },
+ },
+};
+
+export const organizationUpdateResponseWithErrors = {
+ data: {
+ organizationUpdate: {
+ organization: null,
+ errors: ['Path is too short (minimum is 2 characters)'],
+ },
},
- errors: [],
+};
+
+export const pageInfo = {
+ endCursor: 'eyJpZCI6IjEwNTMifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ startCursor: 'eyJpZCI6IjEwNzIifQ',
+ __typename: 'PageInfo',
+};
+
+export const pageInfoOnePage = {
+ endCursor: 'eyJpZCI6IjEwNTMifQ',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjEwNzIifQ',
+ __typename: 'PageInfo',
+};
+
+export const pageInfoEmpty = {
+ endCursor: null,
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ __typename: 'PageInfo',
};
diff --git a/app/assets/javascripts/organizations/profile/preferences/index.js b/app/assets/javascripts/organizations/profile/preferences/index.js
index 0b0dd313cd8..11686b62eca 100644
--- a/app/assets/javascripts/organizations/profile/preferences/index.js
+++ b/app/assets/javascripts/organizations/profile/preferences/index.js
@@ -30,8 +30,8 @@ export const initHomeOrganizationSetting = () => {
block: true,
label: s__('Organization|Home organization'),
description: s__('Organization|Choose what organization you want to see by default.'),
- inputName: 'home_organization',
- inputId: 'home_organization',
+ inputName: 'user[home_organization_id]',
+ inputId: 'user_home_organization_id',
initialSelection,
toggleClass: 'gl-form-input-xl',
},
diff --git a/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue b/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue
new file mode 100644
index 00000000000..879e7b230a1
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/advanced_settings.vue
@@ -0,0 +1,26 @@
+<script>
+import { s__, __ } from '~/locale';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import ChangeUrl from './change_url.vue';
+
+export default {
+ name: 'AdvancedSettings',
+ components: { SettingsBlock, ChangeUrl },
+ i18n: {
+ settingsBlock: {
+ title: __('Advanced'),
+ description: s__('Organization|Perform advanced options such as deleting the organization.'),
+ },
+ },
+};
+</script>
+
+<template>
+ <settings-block slide-animated>
+ <template #title>{{ $options.i18n.settingsBlock.title }}</template>
+ <template #description>{{ $options.i18n.settingsBlock.description }}</template>
+ <template #default>
+ <change-url />
+ </template>
+ </settings-block>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/app.vue b/app/assets/javascripts/organizations/settings/general/components/app.vue
index 134fcc17b54..ba8ab5a09fd 100644
--- a/app/assets/javascripts/organizations/settings/general/components/app.vue
+++ b/app/assets/javascripts/organizations/settings/general/components/app.vue
@@ -1,14 +1,16 @@
<script>
import OrganizationSettings from './organization_settings.vue';
+import AdvancedSettings from './advanced_settings.vue';
export default {
name: 'OrganizationSettingsGeneralApp',
- components: { OrganizationSettings },
+ components: { OrganizationSettings, AdvancedSettings },
};
</script>
<template>
<div>
<organization-settings />
+ <advanced-settings />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/change_url.vue b/app/assets/javascripts/organizations/settings/general/components/change_url.vue
new file mode 100644
index 00000000000..8b65947ab2f
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/components/change_url.vue
@@ -0,0 +1,139 @@
+<script>
+import { GlFormFields, GlButton, GlForm, GlCard } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { visitUrlWithAlerts, joinPaths } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
+import OrganizationUrlField from '~/organizations/shared/components/organization_url_field.vue';
+import { FORM_FIELD_PATH, FORM_FIELD_PATH_VALIDATORS } from '~/organizations/shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import organizationUpdateMutation from '../graphql/mutations/organization_update.mutation.graphql';
+
+export default {
+ name: 'OrganizationSettings',
+ components: { OrganizationUrlField, GlFormFields, GlButton, GlForm, GlCard, FormErrorsAlert },
+ inject: ['organization'],
+ i18n: {
+ cardHeaderTitle: s__('Organization|Change organization URL'),
+ cardHeaderDescription: s__(
+ "Organization|Changing an organization's URL can have unintended side effects.",
+ ),
+ submitButtonText: s__('Organization|Change organization URL'),
+ errorMessage: s__(
+ 'Organization|An error occurred changing your organization URL. Please try again.',
+ ),
+ successAlertMessage: s__('Organization|Organization URL successfully changed.'),
+ },
+ formId: 'change-organization-url-form',
+ fields: {
+ [FORM_FIELD_PATH]: {
+ label: s__('Organization|Organization URL'),
+ validators: FORM_FIELD_PATH_VALIDATORS,
+ groupAttrs: {
+ class: 'gl-w-full',
+ labelSrOnly: true,
+ },
+ },
+ },
+ data() {
+ return {
+ formValues: {
+ path: this.organization.path,
+ },
+ loading: false,
+ errors: [],
+ };
+ },
+ computed: {
+ isSubmitButtonDisabled() {
+ return this.formValues.path === this.organization.path;
+ },
+ },
+ methods: {
+ async onSubmit() {
+ this.errors = [];
+ this.loading = true;
+ try {
+ const {
+ data: {
+ organizationUpdate: { errors, organization },
+ },
+ } = await this.$apollo.mutate({
+ mutation: organizationUpdateMutation,
+ variables: {
+ input: {
+ id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id),
+ path: this.formValues.path,
+ },
+ },
+ });
+
+ if (errors.length) {
+ this.errors = errors;
+
+ return;
+ }
+
+ visitUrlWithAlerts(joinPaths(organization.webUrl, '/settings/general'), [
+ {
+ id: 'organization-url-successfully-changed',
+ message: this.$options.i18n.successAlertMessage,
+ variant: 'info',
+ },
+ ]);
+ } catch (error) {
+ createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
+ } finally {
+ this.loading = false;
+ }
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <form-errors-alert v-model="errors" />
+ <gl-card
+ class="gl-new-card"
+ header-class="gl-new-card-header gl-flex-direction-column"
+ body-class="gl-new-card-body gl-px-5 gl-py-4"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper">
+ <h4 class="gl-new-card-title">{{ $options.i18n.cardHeaderTitle }}</h4>
+ </div>
+ <p class="gl-new-card-description">{{ $options.i18n.cardHeaderDescription }}</p>
+ </template>
+ <gl-form :id="$options.formId">
+ <gl-form-fields
+ v-model="formValues"
+ :form-id="$options.formId"
+ :fields="$options.fields"
+ @submit="onSubmit"
+ >
+ <template #input(path)="{ id, value, validation, input, blur }">
+ <organization-url-field
+ :id="id"
+ :value="value"
+ :validation="validation"
+ @input="input"
+ @blur="blur"
+ />
+ </template>
+ </gl-form-fields>
+ <div class="gl-display-flex gl-gap-3">
+ <gl-button
+ type="submit"
+ variant="danger"
+ class="js-no-auto-disable"
+ :loading="loading"
+ :disabled="isSubmitButtonDisabled"
+ >{{ $options.i18n.submitButtonText }}</gl-button
+ >
+ </div>
+ </gl-form>
+ </gl-card>
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
index 14826825cd6..1acc4c54f75 100644
--- a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -1,14 +1,18 @@
<script>
import { s__, __ } from '~/locale';
-import { createAlert, VARIANT_INFO } from '~/alert';
+import { createAlert } from '~/alert';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
import { FORM_FIELD_NAME, FORM_FIELD_ID } from '~/organizations/shared/constants';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
-import updateOrganizationMutation from '../graphql/mutations/update_organization.mutation.graphql';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
+import FormErrorsAlert from '~/vue_shared/components/form/errors_alert.vue';
+import organizationUpdateMutation from '../graphql/mutations/organization_update.mutation.graphql';
export default {
name: 'OrganizationSettings',
- components: { NewEditForm, SettingsBlock },
+ components: { NewEditForm, SettingsBlock, FormErrorsAlert },
inject: ['organization'],
i18n: {
submitButtonText: __('Save changes'),
@@ -25,30 +29,41 @@ export default {
data() {
return {
loading: false,
+ errors: [],
};
},
methods: {
async onSubmit(formValues) {
+ this.errors = [];
this.loading = true;
try {
const {
data: {
- updateOrganization: { errors },
+ organizationUpdate: { errors },
},
} = await this.$apollo.mutate({
- mutation: updateOrganizationMutation,
+ mutation: organizationUpdateMutation,
variables: {
- id: this.organization.id,
- name: formValues.name,
+ input: {
+ id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id),
+ name: formValues.name,
+ },
},
});
if (errors.length) {
- // TODO: handle errors when using real API after https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
+ this.errors = errors;
+
return;
}
- createAlert({ message: this.$options.i18n.successMessage, variant: VARIANT_INFO });
+ visitUrlWithAlerts(window.location.href, [
+ {
+ id: 'organization-successfully-updated',
+ message: this.$options.i18n.successMessage,
+ variant: 'info',
+ },
+ ]);
} catch (error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
} finally {
@@ -64,6 +79,7 @@ export default {
<template #title>{{ $options.i18n.settingsBlock.title }}</template>
<template #description>{{ $options.i18n.settingsBlock.description }}</template>
<template #default>
+ <form-errors-alert v-model="errors" />
<new-edit-form
:loading="loading"
:initial-form-values="organization"
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql
new file mode 100644
index 00000000000..566db101ab4
--- /dev/null
+++ b/app/assets/javascripts/organizations/settings/general/graphql/mutations/organization_update.mutation.graphql
@@ -0,0 +1,10 @@
+mutation organizationUpdate($input: OrganizationUpdateInput!) {
+ organizationUpdate(input: $input) {
+ organization {
+ id
+ name
+ webUrl
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql b/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
deleted file mode 100644
index b571a523260..00000000000
--- a/app/assets/javascripts/organizations/settings/general/graphql/mutations/update_organization.mutation.graphql
+++ /dev/null
@@ -1,9 +0,0 @@
-mutation updateOrganization($input: LocalUpdateOrganizationInput!) {
- updateOrganization(input: $input) @client {
- organization {
- id
- name
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql b/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
deleted file mode 100644
index eb81a7b0321..00000000000
--- a/app/assets/javascripts/organizations/settings/general/graphql/typedefs.graphql
+++ /dev/null
@@ -1,5 +0,0 @@
-# TODO: Use real input type when https://gitlab.com/gitlab-org/gitlab/-/issues/419608 is complete.
-input LocalUpdateOrganizationInput {
- id: ID!
- name: String
-}
diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js
index 36303c32b94..138606a0aab 100644
--- a/app/assets/javascripts/organizations/settings/general/index.js
+++ b/app/assets/javascripts/organizations/settings/general/index.js
@@ -3,7 +3,6 @@ import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import createDefaultClient from '~/lib/graphql';
-import resolvers from '../../shared/graphql/resolvers';
import App from './components/app.vue';
export const initOrganizationsSettingsGeneral = () => {
@@ -19,7 +18,7 @@ export const initOrganizationsSettingsGeneral = () => {
);
const apolloProvider = new VueApollo({
- defaultClient: createDefaultClient(resolvers),
+ defaultClient: createDefaultClient(),
});
return new Vue({
diff --git a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
index 8aaa680036f..c5bb16b944a 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -1,18 +1,15 @@
<script>
-import {
- GlForm,
- GlFormFields,
- GlButton,
- GlFormInputGroup,
- GlFormInput,
- GlInputGroupText,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlForm, GlFormFields, GlButton } from '@gitlab/ui';
import { formValidators } from '@gitlab/ui/dist/utils';
import { s__, __ } from '~/locale';
import { slugify } from '~/lib/utils/text_utility';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_PATH } from '../constants';
+import {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_PATH,
+ FORM_FIELD_PATH_VALIDATORS,
+} from '../constants';
+import OrganizationUrlField from './organization_url_field.vue';
export default {
name: 'NewEditForm',
@@ -20,17 +17,13 @@ export default {
GlForm,
GlFormFields,
GlButton,
- GlFormInputGroup,
- GlFormInput,
- GlInputGroupText,
- GlTruncate,
+ OrganizationUrlField,
},
i18n: {
cancel: __('Cancel'),
- pathPlaceholder: s__('Organization|my-organization'),
},
formId: 'new-organization-form',
- inject: ['organizationsPath', 'rootUrl'],
+ inject: ['organizationsPath'],
props: {
loading: {
type: Boolean,
@@ -71,9 +64,6 @@ export default {
};
},
computed: {
- baseUrl() {
- return joinPaths(this.rootUrl, this.organizationsPath, '/');
- },
fields() {
const fields = {
[FORM_FIELD_NAME]: {
@@ -103,13 +93,7 @@ export default {
},
[FORM_FIELD_PATH]: {
label: s__('Organization|Organization URL'),
- validators: [
- formValidators.required(s__('Organization|Organization URL is required.')),
- formValidators.factory(
- s__('Organization|Organization URL must be a minimum of two characters.'),
- (val) => val.length >= 2,
- ),
- ],
+ validators: FORM_FIELD_PATH_VALIDATORS,
groupAttrs: {
class: 'gl-w-full',
},
@@ -156,22 +140,13 @@ export default {
@submit="$emit('submit', formValues)"
>
<template #input(path)="{ id, value, validation, input, blur }">
- <gl-form-input-group>
- <template #prepend>
- <gl-input-group-text class="organization-root-path">
- <gl-truncate :text="baseUrl" position="middle" />
- </gl-input-group-text>
- </template>
- <gl-form-input
- v-bind="validation"
- :id="id"
- :value="value"
- :placeholder="$options.i18n.pathPlaceholder"
- class="gl-h-auto! gl-md-form-input-lg"
- @input="onPathInput($event, input)"
- @blur="blur"
- />
- </gl-form-input-group>
+ <organization-url-field
+ :id="id"
+ :value="value"
+ :validation="validation"
+ @input="onPathInput($event, input)"
+ @blur="blur"
+ />
</template>
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
diff --git a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
new file mode 100644
index 00000000000..d36f62477e6
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlFormInputGroup, GlFormInput, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { joinPaths } from '~/lib/utils/url_utility';
+
+export default {
+ name: 'OrganizationUrlField',
+ components: {
+ GlFormInputGroup,
+ GlFormInput,
+ GlInputGroupText,
+ GlTruncate,
+ },
+ i18n: {
+ pathPlaceholder: s__('Organization|my-organization'),
+ },
+ formId: 'new-organization-form',
+ inject: ['organizationsPath', 'rootUrl'],
+ props: {
+ id: {
+ type: String,
+ required: true,
+ },
+ value: {
+ type: String,
+ required: true,
+ },
+ validation: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ baseUrl() {
+ return joinPaths(this.rootUrl, this.organizationsPath, '/');
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-input-group>
+ <template #prepend>
+ <gl-input-group-text class="organization-root-path">
+ <gl-truncate :text="baseUrl" position="middle" />
+ </gl-input-group-text>
+ </template>
+ <gl-form-input
+ v-bind="validation"
+ :id="id"
+ :value="value"
+ :placeholder="$options.i18n.pathPlaceholder"
+ class="gl-h-auto! gl-md-form-input-lg"
+ @input="$emit('input', $event)"
+ @blur="$emit('blur', $event)"
+ />
+ </gl-form-input-group>
+</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
index 010613bc9fd..7287d84f99f 100644
--- a/app/assets/javascripts/organizations/shared/constants.js
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -1,3 +1,14 @@
+import { formValidators } from '@gitlab/ui/dist/utils';
+import { s__ } from '~/locale';
+
export const FORM_FIELD_NAME = 'name';
export const FORM_FIELD_ID = 'id';
export const FORM_FIELD_PATH = 'path';
+
+export const FORM_FIELD_PATH_VALIDATORS = [
+ formValidators.required(s__('Organization|Organization URL is required.')),
+ formValidators.factory(
+ s__('Organization|Organization URL is too short (minimum is 2 characters).'),
+ (val) => val.length >= 2,
+ ),
+];
diff --git a/app/assets/javascripts/organizations/shared/graphql/fragments/organization.fragment.graphql b/app/assets/javascripts/organizations/shared/graphql/fragments/organization.fragment.graphql
new file mode 100644
index 00000000000..c0bccdcc120
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/graphql/fragments/organization.fragment.graphql
@@ -0,0 +1,7 @@
+fragment Organization on Organization {
+ id
+ name
+ descriptionHtml
+ avatarUrl
+ webUrl
+}
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
index 1d95786fcb0..a8d8d63c27a 100644
--- a/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/organization.query.graphql
@@ -1,9 +1,7 @@
-query getOrganization($id: ID!) {
- organization(id: $id) @client {
- id
- name
- descriptionHtml
- avatarUrl
- webUrl
+#import "../fragments/organization.fragment.graphql"
+
+query getOrganization($id: OrganizationsOrganizationID!) {
+ organization(id: $id) {
+ ...Organization
}
}
diff --git a/app/assets/javascripts/organizations/shared/graphql/queries/organizations.query.graphql b/app/assets/javascripts/organizations/shared/graphql/queries/organizations.query.graphql
new file mode 100644
index 00000000000..d69e7916512
--- /dev/null
+++ b/app/assets/javascripts/organizations/shared/graphql/queries/organizations.query.graphql
@@ -0,0 +1,16 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "../fragments/organization.fragment.graphql"
+
+query getCurrentUserOrganizations($first: Int, $last: Int, $before: String, $after: String) {
+ currentUser {
+ id
+ organizations(first: $first, last: $last, before: $before, after: $after) {
+ nodes {
+ ...Organization
+ }
+ pageInfo {
+ ...PageInfo
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/organizations/shared/graphql/resolvers.js b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
index 9ed1be62352..efde13852d8 100644
--- a/app/assets/javascripts/organizations/shared/graphql/resolvers.js
+++ b/app/assets/javascripts/organizations/shared/graphql/resolvers.js
@@ -1,9 +1,4 @@
-import {
- organizations,
- organizationProjects,
- organizationGroups,
- updateOrganizationResponse,
-} from '../../mock_data';
+import { organizations, organizationProjects, organizationGroups } from '../../mock_data';
const simulateLoading = () => {
return new Promise((resolve) => {
@@ -24,21 +19,4 @@ export default {
};
},
},
- UserCore: {
- organizations: async () => {
- await simulateLoading();
-
- return {
- nodes: organizations,
- };
- },
- },
- Mutation: {
- updateOrganization: async () => {
- // Simulate API loading
- await simulateLoading();
-
- return updateOrganizationResponse;
- },
- },
};
diff --git a/app/assets/javascripts/organizations/users/components/app.vue b/app/assets/javascripts/organizations/users/components/app.vue
index ae22bedd69a..065a1e004f2 100644
--- a/app/assets/javascripts/organizations/users/components/app.vue
+++ b/app/assets/javascripts/organizations/users/components/app.vue
@@ -1,10 +1,22 @@
<script>
import { __, s__ } from '~/locale';
import { createAlert } from '~/alert';
+import { ORGANIZATION_USERS_PER_PAGE } from '~/organizations/constants';
import organizationUsersQuery from '../graphql/organization_users.query.graphql';
+import UsersView from './users_view.vue';
+
+const defaultPagination = {
+ first: ORGANIZATION_USERS_PER_PAGE,
+ last: null,
+ before: '',
+ after: '',
+};
export default {
name: 'OrganizationsUsersApp',
+ components: {
+ UsersView,
+ },
i18n: {
users: __('Users'),
loadingPlaceholder: __('Loading'),
@@ -16,16 +28,31 @@ export default {
data() {
return {
users: [],
+ pagination: {
+ ...defaultPagination,
+ },
+ pageInfo: {},
};
},
apollo: {
users: {
query: organizationUsersQuery,
variables() {
- return { id: this.organizationGid };
+ return {
+ id: this.organizationGid,
+ first: this.pagination.first,
+ last: this.pagination.last,
+ before: this.pagination.before,
+ after: this.pagination.after,
+ };
},
update(data) {
- return data.organization.organizationUsers.nodes;
+ const { nodes, pageInfo } = data.organization.organizationUsers;
+ this.pageInfo = pageInfo;
+
+ return nodes.map(({ badges, user }) => {
+ return { ...user, badges, email: user.publicEmail };
+ });
},
error(error) {
createAlert({ message: this.$options.i18n.errorMessage, error, captureError: true });
@@ -37,15 +64,28 @@ export default {
return this.$apollo.queries.users.loading;
},
},
+ methods: {
+ handlePrevPage() {
+ this.pagination.before = this.pageInfo.startCursor;
+ this.pagination.after = '';
+ },
+ handleNextPage() {
+ this.pagination.before = '';
+ this.pagination.after = this.pageInfo.endCursor;
+ },
+ },
};
</script>
<template>
<section>
<h1 class="gl-my-4 gl-font-size-h-display">{{ $options.i18n.users }}</h1>
- <template v-if="loading">
- {{ $options.i18n.loadingPlaceholder }}
- </template>
- <div data-testid="organization-users">{{ users }}</div>
+ <users-view
+ :users="users"
+ :loading="loading"
+ :page-info="pageInfo"
+ @prev="handlePrevPage"
+ @next="handleNextPage"
+ />
</section>
</template>
diff --git a/app/assets/javascripts/organizations/users/components/users_view.vue b/app/assets/javascripts/organizations/users/components/users_view.vue
new file mode 100644
index 00000000000..c1f411fb958
--- /dev/null
+++ b/app/assets/javascripts/organizations/users/components/users_view.vue
@@ -0,0 +1,48 @@
+<script>
+import { GlLoadingIcon, GlKeysetPagination } from '@gitlab/ui';
+import UsersTable from '~/vue_shared/components/users_table/users_table.vue';
+
+export default {
+ name: 'UsersView',
+ components: {
+ GlLoadingIcon,
+ GlKeysetPagination,
+ UsersTable,
+ },
+ inject: ['paths'],
+ props: {
+ users: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <gl-loading-icon v-if="loading" class="gl-mt-5" size="md" />
+ <template v-else>
+ <users-table :users="users" :admin-user-path="paths.adminUser" />
+ <div class="gl-display-flex gl-justify-content-center">
+ <gl-keyset-pagination
+ v-bind="pageInfo"
+ :prev-text="__('Previous')"
+ :next-text="__('Next')"
+ @prev="$emit('prev')"
+ @next="$emit('next')"
+ />
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
index a0b2a639401..0b9b1314fa2 100644
--- a/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
+++ b/app/assets/javascripts/organizations/users/graphql/organization_users.query.graphql
@@ -1,7 +1,15 @@
-query getOrganizationUsers($id: OrganizationsOrganizationID!) {
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+
+query getOrganizationUsers(
+ $id: OrganizationsOrganizationID!
+ $first: Int
+ $last: Int
+ $before: String!
+ $after: String!
+) {
organization(id: $id) {
id
- organizationUsers {
+ organizationUsers(first: $first, last: $last, before: $before, after: $after) {
nodes {
badges {
text
@@ -10,8 +18,17 @@ query getOrganizationUsers($id: OrganizationsOrganizationID!) {
id
user {
id
+ username
+ avatarUrl
+ name
+ publicEmail
+ createdAt
+ lastActivityOn
}
}
+ pageInfo {
+ ...PageInfo
+ }
}
}
}
diff --git a/app/assets/javascripts/organizations/users/index.js b/app/assets/javascripts/organizations/users/index.js
index 76656243075..794ae9e70a6 100644
--- a/app/assets/javascripts/organizations/users/index.js
+++ b/app/assets/javascripts/organizations/users/index.js
@@ -13,7 +13,9 @@ export const initOrganizationsUsers = () => {
defaultClient: createDefaultClient(),
});
- const { organizationGid } = convertObjectPropsToCamelCase(el.dataset);
+ const { organizationGid, paths } = convertObjectPropsToCamelCase(JSON.parse(el.dataset.appData), {
+ deep: true,
+ });
return new Vue({
el,
@@ -21,6 +23,7 @@ export const initOrganizationsUsers = () => {
apolloProvider,
provide: {
organizationGid,
+ paths,
},
render(createElement) {
return createElement(OrganizationsUsersApp);
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 c8a4f32d5a7..3796c5440f7 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
@@ -119,9 +119,15 @@ export default {
tagLocation() {
return this.tag.path?.replace(`:${this.tag.name}`, '');
},
+ isEmptyRevision() {
+ return this.tag.revision === '';
+ },
isInvalidTag() {
return !this.tag.digest;
},
+ showConfigDigest() {
+ return !this.isInvalidTag && !this.isEmptyRevision;
+ },
},
};
</script>
@@ -235,7 +241,7 @@ export default {
/>
</details-row>
</template>
- <template v-if="!isInvalidTag" #details-configuration-digest>
+ <template v-if="showConfigDigest" #details-configuration-digest>
<details-row icon="cloud-gear" data-testid="configuration-detail">
<gl-sprintf :message="$options.i18n.CONFIGURATION_DETAILS_ROW_TEST">
<template #digest>
diff --git a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
index a821a2483cd..b1729f07861 100644
--- a/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
+++ b/app/assets/javascripts/packages_and_registries/infrastructure_registry/details/components/app.vue
@@ -126,7 +126,6 @@ export default {
attributes: {
variant: 'danger',
category: 'primary',
- 'data-qa-selector': 'delete_modal_button',
},
},
fileDeletePrimaryAction: {
@@ -158,7 +157,6 @@ export default {
class="js-delete-button"
variant="danger"
category="primary"
- data-qa-selector="delete_button"
>
{{ __('Delete') }}
</gl-button>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
index 498ddbae7b1..d71773adb9d 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/details/metadata/pypi.vue
@@ -8,6 +8,9 @@ import DetailsRow from '~/vue_shared/components/registry/details_row.vue';
export default {
i18n: {
requiredPython: s__('PackageRegistry|Required Python: %{pythonVersion}'),
+ summary: s__('PackageRegistry|Summary: %{summary}'),
+ authorEmail: s__('PackageRegistry|Author email: %{authorEmail}'),
+ keywords: s__('PackageRegistry|Keywords: %{keywords}'),
},
components: {
DetailsRow,
@@ -24,12 +27,33 @@ export default {
<template>
<div>
- <details-row icon="information-o" padding="gl-p-4" data-testid="pypi-required-python">
+ <details-row dashed icon="information-o" padding="gl-p-4" data-testid="pypi-required-python">
<gl-sprintf :message="$options.i18n.requiredPython">
<template #pythonVersion>
<strong>{{ packageMetadata.requiredPython }}</strong>
</template>
</gl-sprintf>
</details-row>
+ <details-row dashed icon="doc-text" padding="gl-p-4" data-testid="pypi-summary">
+ <gl-sprintf :message="$options.i18n.summary">
+ <template #summary>
+ <strong>{{ packageMetadata.summary }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row dashed icon="doc-text" padding="gl-p-4" data-testid="pypi-keywords">
+ <gl-sprintf :message="$options.i18n.keywords">
+ <template #keywords>
+ <strong>{{ packageMetadata.keywords }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
+ <details-row dashed icon="mail" padding="gl-p-4" data-testid="pypi-author-email">
+ <gl-sprintf :message="$options.i18n.authorEmail">
+ <template #authorEmail>
+ <strong>{{ packageMetadata.authorEmail }}</strong>
+ </template>
+ </gl-sprintf>
+ </details-row>
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
index 1020cd0c533..df50f5a52b4 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/components/list/package_search.vue
@@ -1,10 +1,13 @@
<script>
+import { GlFilteredSearchToken } from '@gitlab/ui';
import { sortableFields } from '~/packages_and_registries/package_registry/utils';
import {
FILTERED_SEARCH_TERM,
OPERATORS_IS,
TOKEN_TITLE_TYPE,
TOKEN_TYPE_TYPE,
+ TOKEN_TITLE_VERSION,
+ TOKEN_TYPE_VERSION,
} from '~/vue_shared/components/filtered_search_bar/constants';
import PersistedSearch from '~/packages_and_registries/shared/components/persisted_search.vue';
import { LIST_KEY_CREATED_AT } from '~/packages_and_registries/package_registry/constants';
@@ -21,6 +24,14 @@ export default {
token: PackageTypeToken,
operators: OPERATORS_IS,
},
+ {
+ type: TOKEN_TYPE_VERSION,
+ icon: 'doc-versions',
+ title: TOKEN_TITLE_VERSION,
+ unique: true,
+ token: GlFilteredSearchToken,
+ operators: OPERATORS_IS,
+ },
],
components: {
LocalStorageSync,
@@ -57,6 +68,7 @@ export default {
const parsed = {
packageName: '',
packageType: undefined,
+ packageVersion: '',
};
return filters.reduce((acc, filter) => {
@@ -67,6 +79,13 @@ export default {
};
}
+ if (filter.type === TOKEN_TYPE_VERSION && filter.value?.data) {
+ return {
+ ...acc,
+ packageVersion: filter.value.data.trim(),
+ };
+ }
+
if (filter.type === FILTERED_SEARCH_TERM) {
return {
...acc,
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql
index fc8b39b37ab..b95b5c2bc74 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_package_metadata.query.graphql
@@ -12,7 +12,10 @@ query getPackageMetadata($id: PackagesPackageID!) {
}
... on PypiMetadata {
id
+ authorEmail
+ keywords
requiredPython
+ summary
}
... on ConanMetadata {
id
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
index f25f24cbc5f..77f09e7b76b 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
+++ b/app/assets/javascripts/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql
@@ -9,6 +9,7 @@ query getPackages(
$groupSort: PackageGroupSort
$packageName: String
$packageType: PackageTypeEnum
+ $packageVersion: String
$first: Int
$last: Int
$after: String
@@ -20,6 +21,7 @@ query getPackages(
sort: $sort
packageName: $packageName
packageType: $packageType
+ packageVersion: $packageVersion
after: $after
before: $before
first: $first
@@ -43,6 +45,7 @@ query getPackages(
sort: $groupSort
packageName: $packageName
packageType: $packageType
+ packageVersion: $packageVersion
after: $after
before: $before
first: $first
diff --git a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
index 294c6baad1b..eb33c020f7d 100644
--- a/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
+++ b/app/assets/javascripts/packages_and_registries/package_registry/pages/list.vue
@@ -82,6 +82,7 @@ export default {
groupSort: this.isGroupPage ? this.sort : undefined,
packageName: this.filters?.packageName,
packageType: this.filters?.packageType,
+ packageVersion: this.filters?.packageVersion,
first: GRAPHQL_PAGE_SIZE,
...this.pageParams,
};
@@ -96,10 +97,10 @@ export default {
return this.packages?.count;
},
hasFilters() {
- return this.filters.packageName && this.filters.packageType;
+ return this.filters.packageName || this.filters.packageType || this.filters.packageVersion;
},
emptySearch() {
- return !this.filters.packageName && !this.filters.packageType;
+ return !this.filters.packageName && !this.filters.packageType && !this.filters.packageVersion;
},
emptyStateTitle() {
return this.emptySearch
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
index 59d4f5e24d0..9e6d55d71d3 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue
@@ -129,7 +129,7 @@ export default {
</script>
<template>
- <settings-block data-qa-selector="dependency_proxy_settings_content">
+ <settings-block data-testid="dependency-proxy-settings-content">
<template #title> {{ $options.i18n.DEPENDENCY_PROXY_HEADER }} </template>
<template #description> {{ $options.i18n.DEPENDENCY_PROXY_DESCRIPTION }} </template>
<template #default>
@@ -138,7 +138,6 @@ export default {
v-model="enabled"
:disabled="isLoading"
:label="$options.i18n.enabledProxyLabel"
- data-qa-selector="dependency_proxy_setting_toggle"
data-testid="dependency-proxy-setting-toggle"
>
<template v-if="enabled" #help>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
index e15f204dc6e..a773a64c4fc 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue
@@ -78,7 +78,6 @@ export default {
exception: 'mavenDuplicateExceptionRegex',
},
testid: 'maven-settings',
- dataQaSelector: 'allow_duplicates_toggle',
},
{
id: 'generic-duplicated-settings-regex-input',
@@ -154,7 +153,7 @@ export default {
</script>
<template>
- <settings-block data-qa-selector="package_registry_settings_content">
+ <settings-block data-testid="package-registry-settings-content">
<template #title> {{ $options.i18n.PACKAGE_SETTINGS_HEADER }}</template>
<template #description>
<span data-testid="description">
@@ -174,7 +173,7 @@ export default {
</template>
<template #cell(allowDuplicates)="{ item }">
<gl-toggle
- :data-qa-selector="item.dataQaSelector"
+ :data-testid="item.dataTestid"
:label="$options.i18n.DUPLICATES_TOGGLE_LABEL"
:value="item.duplicatesAllowed"
:disabled="isLoading"
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
index 4cc9cc190e8..06af69ff250 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue
@@ -6,16 +6,24 @@ import {
SHOW_SETUP_SUCCESS_ALERT,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '~/packages_and_registries/settings/project/constants';
-import ContainerExpirationPolicy from './container_expiration_policy.vue';
-import PackagesCleanupPolicy from './packages_cleanup_policy.vue';
+import ContainerExpirationPolicy from '~/packages_and_registries/settings/project/components/container_expiration_policy.vue';
+import PackagesCleanupPolicy from '~/packages_and_registries/settings/project/components/packages_cleanup_policy.vue';
export default {
components: {
ContainerExpirationPolicy,
+ DependencyProxyPackagesSettings: () =>
+ import(
+ 'ee_component/packages_and_registries/settings/project/components/dependency_proxy_packages_settings.vue'
+ ),
GlAlert,
PackagesCleanupPolicy,
},
- inject: ['showContainerRegistrySettings', 'showPackageRegistrySettings'],
+ inject: [
+ 'showContainerRegistrySettings',
+ 'showPackageRegistrySettings',
+ 'showDependencyProxySettings',
+ ],
i18n: {
UPDATE_SETTINGS_SUCCESS_MESSAGE,
},
@@ -54,5 +62,6 @@ export default {
</gl-alert>
<packages-cleanup-policy v-if="showPackageRegistrySettings" />
<container-expiration-policy v-if="showContainerRegistrySettings" />
+ <dependency-proxy-packages-settings v-if="showDependencyProxySettings" />
</div>
</template>
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
index 57c8d07e620..326265430d9 100644
--- a/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
+++ b/app/assets/javascripts/packages_and_registries/settings/project/registry_settings_bundle.js
@@ -23,6 +23,7 @@ export default () => {
helpPagePath,
showContainerRegistrySettings,
showPackageRegistrySettings,
+ showDependencyProxySettings,
} = el.dataset;
return new Vue({
el,
@@ -40,6 +41,7 @@ export default () => {
helpPagePath,
showContainerRegistrySettings: parseBoolean(showContainerRegistrySettings),
showPackageRegistrySettings: parseBoolean(showPackageRegistrySettings),
+ showDependencyProxySettings: parseBoolean(showDependencyProxySettings),
},
render(createElement) {
return createElement('registry-settings-app', {});
diff --git a/app/assets/javascripts/packages_and_registries/shared/utils.js b/app/assets/javascripts/packages_and_registries/shared/utils.js
index a19c8ed5866..e7606936e6b 100644
--- a/app/assets/javascripts/packages_and_registries/shared/utils.js
+++ b/app/assets/javascripts/packages_and_registries/shared/utils.js
@@ -10,13 +10,16 @@ export const searchArrayToFilterTokens = (search) =>
search.map((s) => keyValueToFilterToken(FILTERED_SEARCH_TERM, s));
export const extractFilterAndSorting = (queryObject) => {
- const { type, search, sort, orderBy } = queryObject;
+ const { type, search, version, sort, orderBy } = queryObject;
const filters = [];
const sorting = {};
if (type) {
filters.push(keyValueToFilterToken('type', type));
}
+ if (version) {
+ filters.push(keyValueToFilterToken('version', version));
+ }
if (search) {
filters.push(...searchArrayToFilterTokens(search));
}
diff --git a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue
index 2217792d7f3..9dfad16ae82 100644
--- a/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue
+++ b/app/assets/javascripts/pages/admin/application_settings/general/components/signup_checkbox.vue
@@ -23,7 +23,7 @@ export default {
type: Boolean,
required: true,
},
- dataQaSelector: {
+ dataTestId: {
type: String,
required: false,
default: '',
@@ -36,11 +36,7 @@ export default {
<div>
<input :name="name" type="hidden" :value="value ? '1' : '0'" data-testid="input" />
- <gl-form-checkbox
- :checked="value"
- :data-qa-selector="dataQaSelector"
- @input="$emit('input', $event)"
- >
+ <gl-form-checkbox :checked="value" :data-testid="dataTestId" @input="$emit('input', $event)">
<span data-testid="label">{{ label }}</span>
<template v-if="helpText" #help>
<span data-testid="helpText">{{ helpText }}</span>
diff --git a/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js b/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
new file mode 100644
index 00000000000..8a12e753847
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/application_settings/service_usage_data/index.js
@@ -0,0 +1,3 @@
+import initServiceUsageData from '~/admin/application_settings/setup_service_usage_data';
+
+initServiceUsageData();
diff --git a/app/assets/javascripts/pages/admin/deploy_keys/new/index.js b/app/assets/javascripts/pages/admin/deploy_keys/new/index.js
new file mode 100644
index 00000000000..a79542ee6e0
--- /dev/null
+++ b/app/assets/javascripts/pages/admin/deploy_keys/new/index.js
@@ -0,0 +1,3 @@
+import initDatePickers from '~/behaviors/date_picker';
+
+initDatePickers();
diff --git a/app/assets/javascripts/pages/clusters/agents/dashboard/index.js b/app/assets/javascripts/pages/clusters/agents/dashboard/index.js
new file mode 100644
index 00000000000..eebb674515b
--- /dev/null
+++ b/app/assets/javascripts/pages/clusters/agents/dashboard/index.js
@@ -0,0 +1,3 @@
+import { initKubernetesDashboard } from '~/kubernetes_dashboard/init_kubernetes_dashboard';
+
+initKubernetesDashboard();
diff --git a/app/assets/javascripts/pages/dashboard/issues/index.js b/app/assets/javascripts/pages/dashboard/issues/index.js
index 2ca11e96f69..963dc0c57da 100644
--- a/app/assets/javascripts/pages/dashboard/issues/index.js
+++ b/app/assets/javascripts/pages/dashboard/issues/index.js
@@ -1,10 +1,14 @@
-import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
+import { createFilteredSearchTokenKeys } from '~/filtered_search/issuable_filtered_search_token_keys';
import { mountIssuesDashboardApp } from '~/issues/dashboard';
import initManualOrdering from '~/issues/manual_ordering';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
+const IssuableFilteredSearchTokenKeys = createFilteredSearchTokenKeys({
+ disableReleaseFilter: true,
+});
+
initFilteredSearch({
page: FILTERED_SEARCH.ISSUES,
filteredSearchTokenKeys: IssuableFilteredSearchTokenKeys,
diff --git a/app/assets/javascripts/pages/dashboard/merge_requests/index.js b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
index a8c59ea6f3d..774e234a358 100644
--- a/app/assets/javascripts/pages/dashboard/merge_requests/index.js
+++ b/app/assets/javascripts/pages/dashboard/merge_requests/index.js
@@ -1,12 +1,20 @@
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 { createFilteredSearchTokenKeys } from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
import initFilteredSearch from '~/pages/search/init_filtered_search';
import { initNewResourceDropdown } from '~/vue_shared/components/new_resource_dropdown/init_new_resource_dropdown';
import { RESOURCE_TYPE_MERGE_REQUEST } from '~/vue_shared/components/new_resource_dropdown/constants';
import searchUserProjectsWithMergeRequestsEnabled from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_merge_requests_enabled.query.graphql';
-addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, true);
+const IssuableFilteredSearchTokenKeys = createFilteredSearchTokenKeys({
+ disableReleaseFilter: true,
+});
+
+addExtraTokensForMergeRequests(IssuableFilteredSearchTokenKeys, {
+ disableBranchFilter: true,
+ disableReleaseFilter: true,
+ disableEnvironmentFilter: true,
+});
initFilteredSearch({
page: FILTERED_SEARCH.MERGE_REQUESTS,
diff --git a/app/assets/javascripts/pages/groups/boards/index.js b/app/assets/javascripts/pages/groups/boards/index.js
index 23f5b083589..a591fed3d9b 100644
--- a/app/assets/javascripts/pages/groups/boards/index.js
+++ b/app/assets/javascripts/pages/groups/boards/index.js
@@ -1,5 +1,6 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
initBoards();
diff --git a/app/assets/javascripts/pages/groups/shared/group_details.js b/app/assets/javascripts/pages/groups/shared/group_details.js
index 5d9eafe5672..46040cd6706 100644
--- a/app/assets/javascripts/pages/groups/shared/group_details.js
+++ b/app/assets/javascripts/pages/groups/shared/group_details.js
@@ -1,10 +1,11 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initInviteMembersBanner from '~/groups/init_invite_members_banner';
import initNotificationsDropdown from '~/notifications';
import ProjectsList from '~/projects_list';
export default function initGroupDetails() {
- new ShortcutsNavigation(); // eslint-disable-line no-new
+ addShortcutsExtension(ShortcutsNavigation);
initNotificationsDropdown();
diff --git a/app/assets/javascripts/pages/groups/show/index.js b/app/assets/javascripts/pages/groups/show/index.js
index f6a4ca0f360..1c6c3c0c518 100644
--- a/app/assets/javascripts/pages/groups/show/index.js
+++ b/app/assets/javascripts/pages/groups/show/index.js
@@ -2,10 +2,12 @@ import leaveByUrl from '~/namespaces/leave_by_url';
import { initGroupOverviewTabs } from '~/groups/init_overview_tabs';
import { initGroupReadme } from '~/groups/init_group_readme';
import initReadMore from '~/read_more';
+import InitMoreActionsDropdown from '~/groups_projects/init_more_actions_dropdown';
import initGroupDetails from '../shared/group_details';
-leaveByUrl('group');
initGroupDetails();
initGroupOverviewTabs();
initReadMore();
initGroupReadme();
+InitMoreActionsDropdown();
+leaveByUrl('group');
diff --git a/app/assets/javascripts/pages/ide/index.js b/app/assets/javascripts/pages/ide/index/index.js
index 15933256e75..15933256e75 100644
--- a/app/assets/javascripts/pages/ide/index.js
+++ b/app/assets/javascripts/pages/ide/index/index.js
diff --git a/app/assets/javascripts/pages/ide/oauth_redirect/index.js b/app/assets/javascripts/pages/ide/oauth_redirect/index.js
new file mode 100644
index 00000000000..ee9233fab38
--- /dev/null
+++ b/app/assets/javascripts/pages/ide/oauth_redirect/index.js
@@ -0,0 +1,3 @@
+import { mountOAuthCallback } from '~/ide/mount_oauth_callback';
+
+mountOAuthCallback();
diff --git a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
index e912bfa4f92..1d54dad43a9 100644
--- a/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
+++ b/app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue
@@ -8,12 +8,13 @@ import {
GlTableLite,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
+import { isEqual } from 'lodash';
import { s__, __ } from '~/locale';
import { createAlert } from '~/alert';
import { parseIntPagination, normalizeHeaders } from '~/lib/utils/common_utils';
-import { joinPaths } from '~/lib/utils/url_utility';
-import { getBulkImportsHistory } from '~/rest_api';
+import { joinPaths, getParameterValues } from '~/lib/utils/url_utility';
+import { getBulkImportHistory, getBulkImportsHistory } from '~/rest_api';
import ImportStatus from '~/import_entities/import_groups/components/import_status.vue';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
@@ -92,7 +93,6 @@ export default {
tableCell({
key: 'status',
label: __('Status'),
- tdAttr: { 'data-qa-selector': 'import_status_indicator' },
}),
],
@@ -110,12 +110,18 @@ export default {
showDetailsLink() {
return this.glFeatures.bulkImportDetailsPage;
},
+
+ paginationConfigCopy() {
+ return { ...this.paginationConfig };
+ },
},
watch: {
- paginationConfig: {
- handler() {
- this.loadHistoryItems();
+ paginationConfigCopy: {
+ handler(newValue, oldValue) {
+ if (!isEqual(newValue, oldValue)) {
+ this.loadHistoryItems();
+ }
},
deep: true,
},
@@ -159,10 +165,19 @@ export default {
},
methods: {
+ fetchFn(params) {
+ const bulkImportId = getParameterValues('bulk_import_id')[0];
+
+ return bulkImportId
+ ? getBulkImportHistory(bulkImportId, params)
+ : getBulkImportsHistory(params);
+ },
+
async loadHistoryItems() {
try {
this.loading = true;
- const { data: historyItems, headers } = await getBulkImportsHistory({
+
+ const { data: historyItems, headers } = await this.fetchFn({
page: this.paginationConfig.page,
per_page: this.paginationConfig.perPage,
});
@@ -217,14 +232,11 @@ export default {
<template>
<div>
- <div
- class="gl-border-solid gl-border-gray-200 gl-border-0 gl-border-b-1 gl-display-flex gl-align-items-center"
- >
- <h1 class="gl-my-0 gl-py-4 gl-font-size-h1">
- <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6 gl-mb-2 gl-display-inline gl-mr-2" />
- {{ s__('BulkImport|GitLab Migration history') }}
- </h1>
- </div>
+ <h1 class="gl-font-size-h1 gl-my-0 gl-py-4 gl-display-flex gl-align-items-center gl-gap-3">
+ <img :src="$options.gitlabLogo" class="gl-w-6 gl-h-6" />
+ <span>{{ s__('BulkImport|Direct transfer history') }}</span>
+ </h1>
+
<gl-loading-icon v-if="loading" size="lg" class="gl-mt-5" />
<gl-empty-state
v-else-if="!hasHistoryItems"
diff --git a/app/assets/javascripts/pages/import/history/components/import_history_app.vue b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
index 9c0f937fe0e..a0ff3ded3f5 100644
--- a/app/assets/javascripts/pages/import/history/components/import_history_app.vue
+++ b/app/assets/javascripts/pages/import/history/components/import_history_app.vue
@@ -65,10 +65,10 @@ export default {
key: 'created_at',
label: __('Date'),
}),
+
tableCell({
key: 'status',
label: __('Status'),
- tdAttr: { 'data-qa-selector': 'import_status_indicator' },
}),
],
@@ -154,9 +154,7 @@ export default {
</gl-link>
<span v-else>{{ item.import_url }}</span>
</template>
- <span v-else>{{
- s__('BulkImport|Template / File-based import / GitLab Migration')
- }}</span>
+ <span v-else>{{ s__('BulkImport|Template / File-based import / Direct transfer') }}</span>
</template>
<template #cell(destination)="{ item }">
<gl-link :href="item.http_url_to_repo">
diff --git a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index 8fe822e4639..41952a33c05 100644
--- a/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -8,7 +8,7 @@ const skippable = twoFactorNode ? parseBoolean(twoFactorNode.dataset.twoFactorSk
if (skippable) {
const button = `<div class="gl-alert-actions">
- <a class="btn gl-button btn-md btn-confirm gl-alert-action" data-qa-selector="configure_it_later_button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>
+ <a class="btn gl-button btn-md btn-confirm gl-alert-action" data-testid="configure-it-later-button" data-method="patch" href="${twoFactorNode.dataset.two_factor_skip_url}">Configure it later</a>
</div>`;
const flashAlert = document.querySelector('.flash-alert');
if (flashAlert) {
diff --git a/app/assets/javascripts/pages/projects/activity/index.js b/app/assets/javascripts/pages/projects/activity/index.js
index 03fbad0f1ec..3138026e1db 100644
--- a/app/assets/javascripts/pages/projects/activity/index.js
+++ b/app/assets/javascripts/pages/projects/activity/index.js
@@ -1,5 +1,6 @@
import Activities from '~/activities';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
new Activities(); // eslint-disable-line no-new
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
diff --git a/app/assets/javascripts/pages/projects/artifacts/browse/index.js b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
index 60680ec7d1d..47cf348eb4d 100644
--- a/app/assets/javascripts/pages/projects/artifacts/browse/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/browse/index.js
@@ -1,5 +1,6 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BuildArtifacts from '~/build_artifacts';
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
new BuildArtifacts(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/artifacts/file/index.js b/app/assets/javascripts/pages/projects/artifacts/file/index.js
index 07ee4d686cc..3bc3b9dabbc 100644
--- a/app/assets/javascripts/pages/projects/artifacts/file/index.js
+++ b/app/assets/javascripts/pages/projects/artifacts/file/index.js
@@ -1,5 +1,6 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { BlobViewer } from '~/blob/viewer/index';
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
new BlobViewer(); // eslint-disable-line no-new
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index 23f5b083589..a591fed3d9b 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,5 +1,6 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initBoards from '~/boards';
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
initBoards();
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index c9f5895c7a3..d875f28433e 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -2,6 +2,7 @@
import $ from 'jquery';
import Vue from 'vue';
import loadAwardsHandler from '~/awards_handler';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Diff from '~/diff';
import { createAlert } from '~/alert';
@@ -20,7 +21,7 @@ import { initReportAbuse } from '~/projects/report_abuse';
initDiffStatsDropdown();
new ZenMode();
-new ShortcutsNavigation();
+addShortcutsExtension(ShortcutsNavigation);
initCommitBoxInfo();
diff --git a/app/assets/javascripts/pages/projects/commits/show/index.js b/app/assets/javascripts/pages/projects/commits/show/index.js
index f5ecf9be591..e3b22bbfee0 100644
--- a/app/assets/javascripts/pages/projects/commits/show/index.js
+++ b/app/assets/javascripts/pages/projects/commits/show/index.js
@@ -1,10 +1,11 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import CommitsList from '~/commits';
import GpgBadges from '~/gpg_badges';
import { mountCommits, initCommitsRefSwitcher } from '~/projects/commits';
new CommitsList(document.querySelector('.js-project-commits-show').dataset.commitsLimit); // eslint-disable-line no-new
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
GpgBadges.fetch();
mountCommits(document.getElementById('js-author-dropdown'));
initCommitsRefSwitcher();
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 22c21430e8b..4df84ac167c 100644
--- a/app/assets/javascripts/pages/projects/find_file/show/index.js
+++ b/app/assets/javascripts/pages/projects/find_file/show/index.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsFindFile from '~/behaviors/shortcuts/shortcuts_find_file';
import ProjectFindFile from '~/projects/project_find_file';
import InitBlobRefSwitcher from '../ref_switcher';
@@ -11,4 +12,4 @@ const projectFindFile = new ProjectFindFile($('.file-finder-holder'), {
refType: findElement.dataset.refType,
});
projectFindFile.load(findElement.dataset.fileFindUrl);
-new ShortcutsFindFile(projectFindFile); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsFindFile, projectFindFile);
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
index e3d50e900ca..bfa2f2cc14f 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue
@@ -423,7 +423,7 @@ export default {
>
<div>
<gl-icon
- data-qa-selector="fork_privacy_button"
+ data-testid="fork-privacy-button"
:name="icon"
:data-qa-privacy-level="`${value}`"
/>
@@ -440,8 +440,7 @@ export default {
category="primary"
variant="confirm"
class="js-no-auto-disable"
- data-testid="submit-button"
- data-qa-selector="fork_project_button"
+ data-testid="fork-project-button"
:loading="isSaving"
>
{{ s__('ForkProject|Fork project') }}
diff --git a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
index 84796954cf1..b4bb2176e26 100644
--- a/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
+++ b/app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue
@@ -90,8 +90,7 @@ export default {
}}</gl-button>
<gl-collapsible-listbox
class="gl-flex-grow-1"
- data-qa-selector="select_namespace_dropdown"
- data-testid="select_namespace_dropdown"
+ data-testid="select-namespace-dropdown"
:items="namespaceItems"
:header-text="__('Namespaces')"
:no-results-text="__('No matches found')"
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 1075241e172..dc00036864f 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,5 +1,6 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import Project from './project';
new Project(); // eslint-disable-line no-new
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
diff --git a/app/assets/javascripts/pages/projects/init_blob.js b/app/assets/javascripts/pages/projects/init_blob.js
index 244d1d5590e..6e3e1a35bd2 100644
--- a/app/assets/javascripts/pages/projects/init_blob.js
+++ b/app/assets/javascripts/pages/projects/init_blob.js
@@ -1,3 +1,4 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import BlobForkSuggestion from '~/blob/blob_fork_suggestion';
@@ -18,10 +19,8 @@ export default () => {
const fileBlobPermalinkUrl =
fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
- new ShortcutsNavigation(); // eslint-disable-line no-new
-
- // eslint-disable-next-line no-new
- new ShortcutsBlob({
+ addShortcutsExtension(ShortcutsNavigation);
+ addShortcutsExtension(ShortcutsBlob, {
fileBlobPermalinkUrl,
fileBlobPermalinkUrlElement,
});
diff --git a/app/assets/javascripts/pages/projects/issues/index/index.js b/app/assets/javascripts/pages/projects/issues/index/index.js
index b320d8a61c2..322eaa845ec 100644
--- a/app/assets/javascripts/pages/projects/issues/index/index.js
+++ b/app/assets/javascripts/pages/projects/issues/index/index.js
@@ -1,6 +1,7 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import { mountIssuesListApp, mountJiraIssuesListApp } from '~/issues/list';
mountIssuesListApp();
mountJiraIssuesListApp();
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
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 3ae8018714a..a37c18e41ab 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/index/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/index/index.js
@@ -1,4 +1,5 @@
import addExtraTokensForMergeRequests from 'ee_else_ce/filtered_search/add_extra_tokens_for_merge_requests';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import IssuableFilteredSearchTokenKeys from '~/filtered_search/issuable_filtered_search_token_keys';
import { FILTERED_SEARCH } from '~/filtered_search/constants';
@@ -16,7 +17,7 @@ initFilteredSearch({
useDefaultState: true,
});
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
initIssuableByEmail();
initCsvImportExportButtons();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
index 599fd225de9..0e66c3521dd 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request.js
@@ -3,12 +3,13 @@
import $ from 'jquery';
import IssuableForm from 'ee_else_ce/issuable/issuable_form';
import IssuableLabelSelector from '~/issuable/issuable_label_selector';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import LabelsSelect from '~/labels/labels_select';
import { mountMilestoneDropdown } from '~/sidebar/mount_sidebar';
export default () => {
- new ShortcutsNavigation();
+ addShortcutsExtension(ShortcutsNavigation);
new IssuableForm($('.merge-request-form'));
IssuableLabelSelector();
new LabelsSelect();
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
index af1635221ab..1cac330520f 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { s__ } from '~/locale';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsIssuable from '~/behaviors/shortcuts/shortcuts_issuable';
import { initPipelineCountListener } from '~/commit/pipelines/utils';
import { initIssuableSidebar } from '~/issuable';
@@ -18,7 +19,7 @@ import getStateQuery from './queries/get_state.query.graphql';
export default function initMergeRequestShow(store) {
new ZenMode(); // eslint-disable-line no-new
initPipelineCountListener(document.querySelector('#commit-pipeline-table-view'));
- new ShortcutsIssuable(true); // eslint-disable-line no-new
+ addShortcutsExtension(ShortcutsIssuable);
initSourcegraph();
initIssuableSidebar();
initAwardsApp(document.getElementById('js-vue-awards-block'));
diff --git a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
index 1a2b85d7e16..7202dcccd31 100644
--- a/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
+++ b/app/assets/javascripts/pages/projects/ml/model_versions/show/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { ShowMlModelVersion } from '~/ml/model_registry/apps';
-initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion);
+initSimpleApp('#js-mount-show-ml-model-version', ShowMlModelVersion, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js
index a669ea5baaf..58b703bdfda 100644
--- a/app/assets/javascripts/pages/projects/network/show/index.js
+++ b/app/assets/javascripts/pages/projects/network/show/index.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import Vue from 'vue';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNetwork from '~/behaviors/shortcuts/shortcuts_network';
import RefSelector from '~/ref/components/ref_selector.vue';
import Network from '../network';
@@ -44,6 +45,5 @@ initRefSwitcher();
commit_id: $('.network-graph').attr('data-commit-id'),
});
- // eslint-disable-next-line no-new
- new ShortcutsNetwork(networkGraph.branch_graph);
+ addShortcutsExtension(ShortcutsNetwork, networkGraph.branch_graph);
})();
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
index 9c4582ece21..ff2ece99f87 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue
@@ -2,7 +2,6 @@
import { GlFormRadio, GlFormRadioGroup, GlIcon, GlLink, GlTooltipDirective } from '@gitlab/ui';
import { getWeekdayNames } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
const KEY_EVERY_DAY = 'everyDay';
@@ -10,6 +9,12 @@ const KEY_EVERY_WEEK = 'everyWeek';
const KEY_EVERY_MONTH = 'everyMonth';
const KEY_CUSTOM = 'custom';
+const MINUTE = 60; // minute between 0-59
+const HOUR = 24; // hour between 0-23
+const WEEKDAY_INDEX = 7; // week index Sun-Sat
+const DAY = 29; // day between 0-28
+const getRandomCronValue = (max) => Math.floor(Math.random() * max);
+
export default {
components: {
GlFormRadio,
@@ -20,7 +25,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagMixin()],
props: {
initialCronInterval: {
type: String,
@@ -41,9 +45,10 @@ export default {
data() {
return {
isEditingCustom: false,
- randomHour: this.generateRandomHour(),
- randomWeekDayIndex: this.generateRandomWeekDayIndex(),
- randomDay: this.generateRandomDay(),
+ randomMinute: getRandomCronValue(MINUTE),
+ randomHour: getRandomCronValue(HOUR),
+ randomWeekDayIndex: getRandomCronValue(WEEKDAY_INDEX),
+ randomDay: getRandomCronValue(DAY),
inputNameAttribute: 'schedule[cron]',
radioValue: this.initialCronInterval ? KEY_CUSTOM : KEY_EVERY_DAY,
cronInterval: this.initialCronInterval,
@@ -53,19 +58,22 @@ export default {
computed: {
cronIntervalPresets() {
return {
- [KEY_EVERY_DAY]: `0 ${this.randomHour} * * *`,
- [KEY_EVERY_WEEK]: `0 ${this.randomHour} * * ${this.randomWeekDayIndex}`,
- [KEY_EVERY_MONTH]: `0 ${this.randomHour} ${this.randomDay} * *`,
+ [KEY_EVERY_DAY]: `${this.randomMinute} ${this.randomHour} * * *`,
+ [KEY_EVERY_WEEK]: `${this.randomMinute} ${this.randomHour} * * ${this.randomWeekDayIndex}`,
+ [KEY_EVERY_MONTH]: `${this.randomMinute} ${this.randomHour} ${this.randomDay} * *`,
};
},
+ formattedMinutes() {
+ return String(this.randomMinute).padStart(2, '0');
+ },
formattedTime() {
if (this.randomHour > 12) {
- return `${this.randomHour - 12}:00pm`;
+ return `${this.randomHour - 12}:${this.formattedMinutes}pm`;
}
if (this.randomHour === 12) {
- return `12:00pm`;
+ return `12:${this.formattedMinutes}pm`;
}
- return `${this.randomHour}:00am`;
+ return `${this.randomHour}:${this.formattedMinutes}am`;
},
radioOptions() {
return [
@@ -133,15 +141,6 @@ export default {
onCustomInput() {
this.radioValue = KEY_CUSTOM;
},
- generateRandomHour() {
- return Math.floor(Math.random() * 23);
- },
- generateRandomWeekDayIndex() {
- return Math.floor(Math.random() * 6);
- },
- generateRandomDay() {
- return Math.floor(Math.random() * 28);
- },
showDailyLimitMessage({ value }) {
return value === KEY_CUSTOM && this.dailyLimit;
},
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue b/app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue
index ed5ba3c2653..32775ac553c 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue
+++ b/app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue
@@ -1,50 +1,58 @@
<script>
-import { GlBadge, GlLink, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
-import { createAlert, VARIANT_INFO } from '~/alert';
+import { GlLink, GlLoadingIcon, GlModal, GlSprintf, GlToggle } from '@gitlab/ui';
+import { createAlert } from '~/alert';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
+import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
import getCiCatalogSettingsQuery from '../graphql/queries/get_ci_catalog_settings.query.graphql';
import catalogResourcesCreate from '../graphql/mutations/catalog_resources_create.mutation.graphql';
+import catalogResourcesDestroy from '../graphql/mutations/catalog_resources_destroy.mutation.graphql';
-export const i18n = {
- badgeText: __('Experiment'),
+const i18n = {
catalogResourceQueryError: s__(
'CiCatalog|There was a problem fetching the CI/CD Catalog setting.',
),
- catalogResourceMutationError: s__(
- 'CiCatalog|There was a problem marking the project as a CI/CD Catalog resource.',
+ setCatalogResourceMutationError: s__(
+ 'CiCatalog|Unable to set project as a CI/CD Catalog resource.',
+ ),
+ removeCatalogResourceMutationError: s__(
+ 'CiCatalog|Unable to remove project as a CI/CD Catalog resource.',
+ ),
+ setCatalogResourceMutationSuccess: s__('CiCatalog|This project is now a CI/CD Catalog resource.'),
+ removeCatalogResourceMutationSuccess: s__(
+ 'CiCatalog|This project is no longer a CI/CD Catalog resource.',
),
- catalogResourceMutationSuccess: s__('CiCatalog|This project is now a CI/CD Catalog resource.'),
ciCatalogLabel: s__('CiCatalog|CI/CD Catalog resource'),
ciCatalogHelpText: s__(
- 'CiCatalog|Mark project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}',
+ 'CiCatalog|Set project as a CI/CD Catalog resource. %{linkStart}What is the CI/CD Catalog?%{linkEnd}',
),
modal: {
actionPrimary: {
- text: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
+ text: s__('CiCatalog|Remove from the CI/CD catalog'),
},
actionCancel: {
text: __('Cancel'),
},
body: s__(
- 'CiCatalog|This project will be marked as a CI/CD Catalog resource and will be visible in the CI/CD Catalog. This action is not reversible.',
+ "CiCatalog|The project and any released versions will be removed from the CI/CD Catalog. If you re-enable this toggle, the project's existing releases are not re-added to the catalog. You must %{linkStart}create a new release%{linkEnd}.",
),
- title: s__('CiCatalog|Mark project as a CI/CD Catalog resource'),
+ title: s__('CiCatalog|Remove project from the CI/CD Catalog?'),
},
readMeHelpText: s__(
- 'CiCatalog|The project must contain a README.md file and a template.yml file. When enabled, the repository is available in the CI/CD Catalog.',
+ 'CiCatalog|The project will be findable in the CI/CD Catalog after the project has at least one release.',
),
};
-export const ciCatalogHelpPath = helpPagePath('ci/components/index', {
+const ciCatalogHelpPath = helpPagePath('ci/components/index', {
anchor: 'components-catalog',
});
+const releasesHelpPath = helpPagePath('user/project/releases/release_cicd_examples');
+
export default {
- i18n,
components: {
- GlBadge,
+ BetaBadge,
GlLink,
GlLoadingIcon,
GlModal,
@@ -59,7 +67,6 @@ export default {
},
data() {
return {
- ciCatalogHelpPath,
isCatalogResource: false,
showCatalogResourceModal: false,
};
@@ -81,19 +88,34 @@ export default {
},
},
computed: {
+ successMessage() {
+ return this.isCatalogResource
+ ? this.$options.i18n.setCatalogResourceMutationSuccess
+ : this.$options.i18n.removeCatalogResourceMutationSuccess;
+ },
+ errorMessage() {
+ return this.isCatalogResource
+ ? this.$options.i18n.removeCatalogResourceMutationError
+ : this.$options.i18n.setCatalogResourceMutationError;
+ },
isLoading() {
return this.$apollo.queries.isCatalogResource.loading;
},
},
methods: {
- async markProjectAsCatalogResource() {
+ async toggleCatalogResourceMutation({ isCreating }) {
+ this.showCatalogResourceModal = false;
+
+ const mutation = isCreating ? catalogResourcesCreate : catalogResourcesDestroy;
+ const mutationInput = isCreating ? 'catalogResourcesCreate' : 'catalogResourcesDestroy';
+
try {
const {
data: {
- catalogResourcesCreate: { errors },
+ [mutationInput]: { errors },
},
} = await this.$apollo.mutate({
- mutation: catalogResourcesCreate,
+ mutation,
variables: { input: { projectPath: this.fullPath } },
});
@@ -101,23 +123,30 @@ export default {
throw new Error(errors[0]);
}
- this.isCatalogResource = true;
- createAlert({
- message: this.$options.i18n.catalogResourceMutationSuccess,
- variant: VARIANT_INFO,
- });
+ this.isCatalogResource = !this.isCatalogResource;
+ this.$toast.show(this.successMessage);
} catch (error) {
- const message = error.message || this.$options.i18n.catalogResourceMutationError;
+ const message = error.message || this.errorMessage;
createAlert({ message });
}
},
- onCatalogResourceEnabledToggled() {
- this.showCatalogResourceModal = true;
- },
onModalCanceled() {
this.showCatalogResourceModal = false;
},
+ onToggleCatalogResource() {
+ if (this.isCatalogResource) {
+ this.showCatalogResourceModal = true;
+ } else {
+ this.toggleCatalogResourceMutation({ isCreating: true });
+ }
+ },
+ unlistCatalogResource() {
+ this.toggleCatalogResourceMutation({ isCreating: false });
+ },
},
+ i18n,
+ ciCatalogHelpPath,
+ releasesHelpPath,
};
</script>
@@ -125,40 +154,44 @@ export default {
<div>
<gl-loading-icon v-if="isLoading" />
<div v-else data-testid="ci-catalog-settings">
- <div>
- <label class="gl-mb-1 gl-mr-2">
+ <div class="gl-display-flex">
+ <label class="gl-mb-1 gl-mr-3">
{{ $options.i18n.ciCatalogLabel }}
</label>
- <gl-badge size="sm" variant="info"> {{ $options.i18n.badgeText }} </gl-badge>
+ <beta-badge size="sm" />
</div>
<gl-sprintf :message="$options.i18n.ciCatalogHelpText">
<template #link="{ content }">
- <gl-link :href="ciCatalogHelpPath" target="_blank">{{ content }}</gl-link>
+ <gl-link :href="$options.ciCatalogHelpPath" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<gl-toggle
class="gl-my-2"
- :disabled="isCatalogResource"
:value="isCatalogResource"
:label="$options.i18n.ciCatalogLabel"
label-position="hidden"
name="ci_resource_enabled"
- @change="onCatalogResourceEnabledToggled"
+ data-testid="catalog-resource-toggle"
+ @change="onToggleCatalogResource"
/>
<div class="gl-text-secondary">
{{ $options.i18n.readMeHelpText }}
</div>
<gl-modal
:visible="showCatalogResourceModal"
- modal-id="mark-as-catalog-resource"
+ modal-id="unlist-catalog-resource"
size="sm"
:title="$options.i18n.modal.title"
:action-cancel="$options.i18n.modal.actionCancel"
:action-primary="$options.i18n.modal.actionPrimary"
@canceled="onModalCanceled"
- @primary="markProjectAsCatalogResource"
+ @primary="unlistCatalogResource"
>
- {{ $options.i18n.modal.body }}
+ <gl-sprintf :message="$options.i18n.modal.body">
+ <template #link="{ content }">
+ <gl-link :href="$options.releasesHelpPath" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
</gl-modal>
</div>
</div>
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 6ff48b7de95..a7f685ea8a8 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
@@ -16,6 +16,7 @@ import {
CVE_ID_REQUEST_BUTTON_I18N,
featureAccessLevelDescriptions,
modelExperimentsHelpPath,
+ modelRegistryHelpPath,
} from '../constants';
import { toggleHiddenClassBySelector } from '../external';
import ProjectFeatureSetting from './project_feature_setting.vue';
@@ -63,6 +64,8 @@ export default {
modelExperimentsHelpText: s__(
'ProjectSettings|Track machine learning model experiments and artifacts.',
),
+ modelRegistryLabel: s__('ProjectSettings|Model registry'),
+ modelRegistryHelpText: s__('ProjectSettings|Manage machine learning models.'),
pagesLabel: s__('ProjectSettings|Pages'),
repositoryLabel: s__('ProjectSettings|Repository'),
requirementsLabel: s__('ProjectSettings|Requirements'),
@@ -83,7 +86,7 @@ export default {
VISIBILITY_LEVEL_INTERNAL_INTEGER,
VISIBILITY_LEVEL_PUBLIC_INTEGER,
modelExperimentsHelpPath,
-
+ modelRegistryHelpPath,
components: {
CiCatalogSettings,
ProjectFeatureSetting,
@@ -259,6 +262,7 @@ export default {
mergeRequestsAccessLevel: featureAccessLevel.EVERYONE,
packageRegistryAccessLevel: featureAccessLevel.EVERYONE,
modelExperimentsAccessLevel: featureAccessLevel.EVERYONE,
+ modelRegistryAccessLevel: featureAccessLevel.EVERYONE,
buildsAccessLevel: featureAccessLevel.EVERYONE,
wikiAccessLevel: featureAccessLevel.EVERYONE,
snippetsAccessLevel: featureAccessLevel.EVERYONE,
@@ -411,6 +415,10 @@ export default {
featureAccessLevel.PROJECT_MEMBERS,
this.modelExperimentsAccessLevel,
);
+ this.modelRegistryAccessLevel = Math.min(
+ featureAccessLevel.PROJECT_MEMBERS,
+ this.modelRegistryAccessLevel,
+ );
this.wikiAccessLevel = Math.min(featureAccessLevel.PROJECT_MEMBERS, this.wikiAccessLevel);
this.snippetsAccessLevel = Math.min(
featureAccessLevel.PROJECT_MEMBERS,
@@ -475,6 +483,8 @@ export default {
this.wikiAccessLevel = featureAccessLevel.EVERYONE;
if (this.modelExperimentsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.modelExperimentsAccessLevel = featureAccessLevel.EVERYONE;
+ if (this.modelRegistryAccessLevel > featureAccessLevel.NOT_ENABLED)
+ this.modelRegistryAccessLevel = featureAccessLevel.EVERYONE;
if (this.snippetsAccessLevel > featureAccessLevel.NOT_ENABLED)
this.snippetsAccessLevel = featureAccessLevel.EVERYONE;
if (this.pagesAccessLevel === featureAccessLevel.PROJECT_MEMBERS)
@@ -571,7 +581,7 @@ export default {
:disabled="!canChangeVisibilityLevel"
name="project[visibility_level]"
class="form-control select-control"
- data-qa-selector="project_visibility_dropdown"
+ data-testid="project-visibility-dropdown"
>
<option
:value="$options.VISIBILITY_LEVEL_PRIVATE_INTEGER"
@@ -914,6 +924,19 @@ export default {
/>
</project-setting-row>
<project-setting-row
+ ref="model-registry-settings"
+ :label="$options.i18n.modelRegistryLabel"
+ :help-text="$options.i18n.modelRegistryHelpText"
+ :help-path="$options.modelRegistryHelpPath"
+ >
+ <project-feature-setting
+ v-model="modelRegistryAccessLevel"
+ :label="$options.i18n.modelRegistryLabel"
+ :options="featureAccessLevelOptions"
+ name="project[project_feature_attributes][model_registry_access_level]"
+ />
+ </project-setting-row>
+ <project-setting-row
v-if="pagesAvailable && pagesAccessControlEnabled"
ref="pages-settings"
:help-path="pagesHelpPath"
@@ -1060,13 +1083,7 @@ export default {
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"
- >
+ <gl-button v-else type="submit" variant="confirm" data-testid="project-features-save-button">
{{ $options.i18n.confirmButtonText }}
</gl-button>
</div>
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/constants.js b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
index 522cc7cfc1a..125fc279240 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/constants.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/constants.js
@@ -48,3 +48,5 @@ export const CVE_ID_REQUEST_BUTTON_I18N = {
export const modelExperimentsHelpPath = helpPagePath(
'user/project/ml/experiment_tracking/index.md',
);
+
+export const modelRegistryHelpPath = helpPagePath('user/project/ml/model_registry/index.md');
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql b/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql
new file mode 100644
index 00000000000..fa42b081a5f
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql
@@ -0,0 +1,5 @@
+mutation catalogResourcesDestroy($input: CatalogResourcesDestroyInput!) {
+ catalogResourcesDestroy(input: $input) {
+ errors
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
index 8ceea37b701..e4a4e2c00eb 100644
--- a/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
+++ b/app/assets/javascripts/pages/projects/shared/web_ide_link/index.js
@@ -18,7 +18,7 @@ export default ({ el, router }) => {
const { projectPath, ref, isBlob, webIdeUrl, ...options } = convertObjectPropsToCamelCase(
JSON.parse(el.dataset.options),
);
- const { webIdePromoPopoverImg } = el.dataset;
+ const { webIdePromoPopoverImg, cssClasses } = el.dataset;
// eslint-disable-next-line no-new
new Vue({
@@ -40,6 +40,7 @@ export default ({ el, router }) => {
joinPaths('/', projectPath, 'edit', ref, '-', this.$route?.params.path || '', '/'),
),
projectPath,
+ cssClasses,
...options,
},
});
diff --git a/app/assets/javascripts/pages/projects/show/index.js b/app/assets/javascripts/pages/projects/show/index.js
index 98c58515d24..150c702f1fe 100644
--- a/app/assets/javascripts/pages/projects/show/index.js
+++ b/app/assets/javascripts/pages/projects/show/index.js
@@ -1,3 +1,4 @@
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import initClustersDeprecationAlert from '~/projects/clusters_deprecation_alert';
import leaveByUrl from '~/namespaces/leave_by_url';
@@ -8,6 +9,7 @@ import { initUploadFileTrigger } from '~/projects/upload_file';
import initReadMore from '~/read_more';
import initForksButton from '~/forks/init_forks_button';
import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
+import InitMoreActionsDropdown from '~/groups_projects/init_more_actions_dropdown';
// Project show page loads different overview content based on user preferences
if (document.getElementById('js-tree-list')) {
@@ -34,11 +36,9 @@ if (document.querySelector('.project-show-activity')) {
.catch(() => {});
}
-leaveByUrl('project');
-
initVueNotificationsDropdown();
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
initUploadFileTrigger();
initClustersDeprecationAlert();
@@ -61,3 +61,5 @@ if (document.querySelector('.js-autodevops-banner')) {
}
initForksButton();
+InitMoreActionsDropdown();
+leaveByUrl('project');
diff --git a/app/assets/javascripts/pages/projects/tree/show/index.js b/app/assets/javascripts/pages/projects/tree/show/index.js
index d87f8898c63..edecb798686 100644
--- a/app/assets/javascripts/pages/projects/tree/show/index.js
+++ b/app/assets/javascripts/pages/projects/tree/show/index.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import initTree from 'ee_else_ce/repository';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsNavigation from '~/behaviors/shortcuts/shortcuts_navigation';
import NewCommitForm from '~/new_commit_form';
import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
@@ -7,4 +8,4 @@ import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
new NewCommitForm($('.js-create-dir-form')); // eslint-disable-line no-new
initTree();
initAmbiguousRefModal();
-new ShortcutsNavigation(); // eslint-disable-line no-new
+addShortcutsExtension(ShortcutsNavigation);
diff --git a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js b/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
deleted file mode 100644
index 47aae36ecbb..00000000000
--- a/app/assets/javascripts/pages/shared/nav/sidebar_tracking.js
+++ /dev/null
@@ -1,44 +0,0 @@
-function onSidebarLinkClick() {
- const setDataTrackAction = (element, action) => {
- element.dataset.trackAction = action;
- };
-
- const setDataTrackExtra = (element, value) => {
- const SIDEBAR_COLLAPSED = 'Collapsed';
- const SIDEBAR_EXPANDED = 'Expanded';
- const sidebarCollapsed = document
- .querySelector('.nav-sidebar')
- .classList.contains('js-sidebar-collapsed')
- ? SIDEBAR_COLLAPSED
- : SIDEBAR_EXPANDED;
-
- element.dataset.trackExtra = JSON.stringify({
- sidebar_display: sidebarCollapsed,
- menu_display: value,
- });
- };
-
- const EXPANDED = 'Expanded';
- const FLY_OUT = 'Fly out';
- const CLICK_MENU_ACTION = 'click_menu';
- const CLICK_MENU_ITEM_ACTION = 'click_menu_item';
- const parentElement = this.parentNode;
- const subMenuList = parentElement.closest('.sidebar-sub-level-items');
-
- if (subMenuList) {
- const isFlyOut = subMenuList.classList.contains('fly-out-list') ? FLY_OUT : EXPANDED;
-
- setDataTrackExtra(parentElement, isFlyOut);
- setDataTrackAction(parentElement, CLICK_MENU_ITEM_ACTION);
- } else {
- const isFlyOut = parentElement.classList.contains('is-showing-fly-out') ? FLY_OUT : EXPANDED;
-
- setDataTrackExtra(parentElement, isFlyOut);
- setDataTrackAction(parentElement, CLICK_MENU_ACTION);
- }
-}
-export const initSidebarTracking = () => {
- document.querySelectorAll('.nav-sidebar li[data-track-label] > a').forEach((link) => {
- link.addEventListener('click', onSidebarLinkClick);
- });
-};
diff --git a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
index 3c070d2708d..de85420a976 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue
@@ -43,8 +43,7 @@ export default {
text: this.$options.i18n.deletePageText,
attributes: {
variant: 'danger',
- 'data-qa-selector': 'confirm_deletion_button',
- 'data-testid': 'confirm_deletion_button',
+ 'data-testid': 'confirm-deletion-button',
},
};
},
@@ -77,7 +76,7 @@ export default {
v-gl-modal="$options.modal.modalId"
category="secondary"
variant="danger"
- data-qa-selector="delete-button"
+ data-testid="delete-button"
>
{{ $options.i18n.deletePageText }}
</gl-button>
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
index 8491d667213..0b31c9b0e16 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_content.vue
@@ -88,7 +88,6 @@ export default {
v-else-if="!loadingContentFailed && !isLoadingContent"
ref="content"
v-safe-html="content"
- data-qa-selector="wiki_page_content"
data-testid="wiki-page-content"
class="js-wiki-page-content md"
></div>
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_export.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_export.vue
deleted file mode 100644
index 4d13f25c4cb..00000000000
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_export.vue
+++ /dev/null
@@ -1,40 +0,0 @@
-<script>
-import { GlDisclosureDropdown } from '@gitlab/ui';
-import { __ } from '~/locale';
-import printMarkdownDom from '~/lib/print_markdown_dom';
-
-export default {
- components: {
- GlDisclosureDropdown,
- },
- inject: ['target', 'title', 'stylesheet'],
- computed: {
- dropdownItems() {
- return [
- {
- text: __('Print as PDF'),
- action: this.print,
- },
- ];
- },
- },
- methods: {
- print() {
- printMarkdownDom({
- target: document.querySelector(this.target),
- title: this.title,
- stylesheet: this.stylesheet,
- });
- },
- },
-};
-</script>
-<template>
- <gl-disclosure-dropdown
- :items="dropdownItems"
- icon="ellipsis_v"
- category="tertiary"
- placement="right"
- no-caret
- />
-</template>
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 eaa99556994..855c7c4105f 100644
--- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue
@@ -187,6 +187,11 @@ export default {
return typeof this.drawioUrl === 'string' && this.drawioUrl.length > 0;
},
},
+ watch: {
+ title() {
+ this.updateCommitMessage();
+ },
+ },
mounted() {
if (!this.commitMessage) this.updateCommitMessage();
@@ -302,7 +307,7 @@ export default {
/>
<div class="row">
- <div class="col-sm-9">
+ <div class="col-12">
<gl-form-group :label="$options.i18n.title.label" label-for="wiki_title">
<template #description>
<gl-icon class="gl-mr-n1" name="bulb" />
@@ -321,7 +326,6 @@ export default {
:required="true"
:autofocus="!pageInfo.persisted"
:placeholder="$options.i18n.title.placeholder"
- @input="updateCommitMessage"
/>
</gl-form-group>
</div>
@@ -361,8 +365,8 @@ export default {
:drawio-enabled="drawioEnabled"
@contentEditor="notifyContentEditorActive"
@markdownField="notifyContentEditorInactive"
- @keydown.ctrl.enter="submitFormShortcut"
- @keydown.meta.enter="submitFormShortcut"
+ @keydown.ctrl.enter="submitFormWithShortcut"
+ @keydown.meta.enter="submitFormWithShortcut"
/>
<div class="form-text gl-text-gray-600">
<gl-sprintf
@@ -404,7 +408,7 @@ export default {
</div>
</div>
- <div class="form-actions">
+ <div class="gl-display-flex gl-gap-3" data-testid="wiki-form-actions">
<gl-button
category="primary"
variant="confirm"
@@ -416,7 +420,6 @@ export default {
<gl-button
data-testid="wiki-cancel-button"
:href="cancelFormPath"
- class="float-right"
@click="isFormDirty = false"
>
{{ $options.i18n.cancel }}</gl-button
diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_more_dropdown.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_more_dropdown.vue
new file mode 100644
index 00000000000..18b11d46bca
--- /dev/null
+++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_more_dropdown.vue
@@ -0,0 +1,58 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import printMarkdownDom from '~/lib/print_markdown_dom';
+
+export default {
+ components: {
+ GlDisclosureDropdown,
+ },
+ inject: ['print', 'history'],
+ computed: {
+ dropdownItems() {
+ const items = [];
+
+ if (this.history) {
+ items.push({
+ text: s__('Wiki|Page history'),
+ href: this.history,
+ extraAttrs: {
+ 'data-testid': 'page-history-button',
+ },
+ });
+ }
+
+ if (this.print) {
+ items.push({
+ text: __('Print as PDF'),
+ action: this.printPage,
+ extraAttrs: {
+ 'data-testid': 'page-print-button',
+ },
+ });
+ }
+
+ return items;
+ },
+ },
+ methods: {
+ printPage() {
+ printMarkdownDom({
+ target: document.querySelector(this.print.target),
+ title: this.print.title,
+ stylesheet: this.print.stylesheet,
+ });
+ },
+ },
+};
+</script>
+<template>
+ <gl-disclosure-dropdown
+ :items="dropdownItems"
+ icon="ellipsis_v"
+ category="tertiary"
+ placement="right"
+ no-caret
+ data-testid="wiki-more-dropdown"
+ />
+</template>
diff --git a/app/assets/javascripts/pages/shared/wikis/show.js b/app/assets/javascripts/pages/shared/wikis/show.js
index 9bc399d07b3..89ed395f742 100644
--- a/app/assets/javascripts/pages/shared/wikis/show.js
+++ b/app/assets/javascripts/pages/shared/wikis/show.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import Wikis from './wikis';
import WikiContent from './components/wiki_content.vue';
-import WikiExport from './components/wiki_export.vue';
+import WikiMoreDropdown from './components/wiki_more_dropdown.vue';
const mountWikiContentApp = () => {
const el = document.querySelector('.js-async-wiki-page-content');
@@ -25,17 +25,16 @@ const mountWikiExportApp = () => {
const el = document.querySelector('#js-export-actions');
if (!el) return false;
- const { target, title, stylesheet } = JSON.parse(el.dataset.options);
+ const { history, print } = JSON.parse(el.dataset.options);
return new Vue({
el,
provide: {
- target,
- title,
- stylesheet,
+ history,
+ print,
},
render(createElement) {
- return createElement(WikiExport);
+ return createElement(WikiMoreDropdown);
},
});
};
diff --git a/app/assets/javascripts/pages/shared/wikis/wikis.js b/app/assets/javascripts/pages/shared/wikis/wikis.js
index b32cc700e16..c98cda92a94 100644
--- a/app/assets/javascripts/pages/shared/wikis/wikis.js
+++ b/app/assets/javascripts/pages/shared/wikis/wikis.js
@@ -1,5 +1,6 @@
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import Tracking from '~/tracking';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsWiki from '~/behaviors/shortcuts/shortcuts_wiki';
const TRACKING_EVENT_NAME = 'view_wiki_page';
@@ -72,6 +73,6 @@ export default class Wikis {
}
static initShortcuts() {
- new ShortcutsWiki(); // eslint-disable-line no-new
+ addShortcutsExtension(ShortcutsWiki);
}
}
diff --git a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js b/app/assets/javascripts/pages/user_settings/personal_access_tokens/index.js
index c520042c172..c520042c172 100644
--- a/app/assets/javascripts/pages/profiles/personal_access_tokens/index.js
+++ b/app/assets/javascripts/pages/user_settings/personal_access_tokens/index.js
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index ab10283b3c4..f23b6d4596a 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -141,7 +141,7 @@ export default {
v-if="currentRequest.details && metricDetails"
:id="`peek-view-${metric}`"
class="gl-display-flex gl-align-items-baseline view"
- data-qa-selector="detailed_metric_content"
+ data-testid="detailed-metric-content"
>
<gl-button
v-gl-tooltip.viewport
diff --git a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
index c5f8fd1904f..9711610d0e2 100644
--- a/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
+++ b/app/assets/javascripts/performance_bar/components/performance_bar_app.vue
@@ -175,7 +175,7 @@ export default {
<div
v-if="currentRequest"
class="gl-display-flex container-fluid gl-overflow-x-auto"
- data-qa-selector="performance_bar"
+ data-testid="performance-bar"
>
<div class="gl-display-flex gl-flex-shrink-0 view-performance-container">
<div v-if="hasHost" id="peek-view-host" class="gl-display-flex gl-gap-2 view">
diff --git a/app/assets/javascripts/performance_bar/components/request_selector.vue b/app/assets/javascripts/performance_bar/components/request_selector.vue
index 2914b9762ac..b4f2140945f 100644
--- a/app/assets/javascripts/performance_bar/components/request_selector.vue
+++ b/app/assets/javascripts/performance_bar/components/request_selector.vue
@@ -28,13 +28,13 @@ export default {
};
</script>
<template>
- <div id="peek-request-selector" data-qa-selector="request_dropdown" class="view gl-mr-5">
+ <div id="peek-request-selector" data-testid="request-dropdown" class="view gl-mr-5">
<gl-form-select v-model="currentRequestId" class="gl-px-3! gl-py-2!">
<option
v-for="request in requests"
:key="request.id"
:value="request.id"
- data-qa-selector="request_dropdown_option"
+ data-testid="request-dropdown-option"
>
{{ request.displayName }}
</option>
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 71dc8c3d020..007b1454138 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -22,12 +22,17 @@ export default class PersistentUserCallout {
init() {
const followLink = this.container.querySelector('.js-follow-link');
+ const closeAndFollowLink = this.container.querySelector('.js-close-and-follow-link');
if (this.closeButtons.length) {
this.handleCloseButtonCallout();
} else if (followLink) {
this.handleFollowLinkCallout(followLink);
}
+
+ if (closeAndFollowLink) {
+ this.handleFollowLinkCallout(closeAndFollowLink);
+ }
}
handleCloseButtonCallout() {
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index bba8e1f7ba5..7420542a065 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -4,7 +4,6 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-recovery-settings-callout',
'.js-users-over-license-callout',
'.js-admin-licensed-user-count-threshold',
- '.js-buy-pipeline-minutes-notification-callout',
'.js-token-expiry-callout',
'.js-registration-enabled-callout',
'.js-new-user-signups-cap-reached',
@@ -25,6 +24,7 @@ const PERSISTENT_USER_CALLOUTS = [
'.js-branch-rules-info-callout',
'.js-new-nav-for-everyone-callout',
'.js-namespace-over-storage-users-combined-alert',
+ '.js-code-suggestions-ga-non-owner-alert',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/profile/account/components/delete_account_modal.vue b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
index 915f6578ac3..e9a67a401b8 100644
--- a/app/assets/javascripts/profile/account/components/delete_account_modal.vue
+++ b/app/assets/javascripts/profile/account/components/delete_account_modal.vue
@@ -42,7 +42,7 @@ export default {
text: __('Delete account'),
attributes: {
variant: 'danger',
- 'data-qa-selector': 'confirm_delete_account_button',
+ 'data-testid': 'confirm-delete-account-button',
category: 'primary',
disabled: !this.canSubmit,
},
@@ -128,7 +128,7 @@ Once you confirm %{deleteAccount}, it cannot be undone or recovered.`),
name="password"
class="form-control"
type="password"
- data-qa-selector="password_confirmation_field"
+ data-testid="password-confirmation-field"
aria-labelledby="input-label"
/>
<input
diff --git a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
index 815b8742500..eedb5d7764e 100644
--- a/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
+++ b/app/assets/javascripts/profile/edit/components/profile_edit_app.vue
@@ -3,7 +3,6 @@ import { nextTick } from 'vue';
import { GlForm, GlButton } from '@gitlab/ui';
import { VARIANT_DANGER, VARIANT_INFO, createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { readFileAsDataURL } from '~/lib/utils/file_utility';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils';
@@ -106,20 +105,12 @@ export default {
this.updateProfileSettings = false;
}
},
- async syncHeaderAvatars() {
- const dataURL = await readFileAsDataURL(this.avatarBlob);
-
- const elements = gon?.use_new_navigation
- ? ['[data-testid="user-dropdown"] .gl-avatar']
- : ['.header-user-avatar', '.js-sidebar-user-avatar'];
-
- elements.forEach((selector) => {
- const node = document.querySelector(selector);
- if (!node) return;
-
- node.setAttribute('src', dataURL);
- node.setAttribute('srcset', dataURL);
- });
+ syncHeaderAvatars() {
+ document.dispatchEvent(
+ new CustomEvent('userAvatar:update', {
+ detail: { url: URL.createObjectURL(this.avatarBlob) },
+ }),
+ );
},
onBlobChange(blob) {
this.avatarBlob = blob;
diff --git a/app/assets/javascripts/profile/edit/components/user_avatar.vue b/app/assets/javascripts/profile/edit/components/user_avatar.vue
index f0ff972336b..d69db9f3d6c 100644
--- a/app/assets/javascripts/profile/edit/components/user_avatar.vue
+++ b/app/assets/javascripts/profile/edit/components/user_avatar.vue
@@ -50,7 +50,7 @@ export default {
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image',
+ modalCropImg: document.querySelector('.modal-profile-crop-image'),
onBlobChange: this.onBlobChange,
};
// This has to be used with jQuery, considering migrate that from jQuery to Vue in the future.
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index ea1a5199ece..0c57dc3cbbe 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -1,7 +1,7 @@
-/* eslint-disable no-useless-escape, no-underscore-dangle, func-names, no-return-assign, consistent-return, class-methods-use-this */
+/* eslint-disable no-underscore-dangle, func-names */
import $ from 'jquery';
-import 'cropper';
+import Cropper from 'cropperjs';
import { isString } from 'lodash';
import { s__ } from '~/locale';
import { createAlert } from '~/alert';
@@ -9,7 +9,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
(() => {
// Matches everything but the file name
- const FILENAMEREGEX = /^.*[\\\/]/;
+ const FILENAMEREGEX = /^.*[\\/]/;
class GitLabCrop {
constructor(
@@ -33,7 +33,6 @@ import { loadCSSFile } from '../lib/utils/css_utils';
this.onModalShow = this.onModalShow.bind(this);
this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
- this.modalCropImg = isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
this.fileInput
.attr('name', `${this.fileInput.attr('name')}-trigger`)
.attr('id', `${this.fileInput.attr('id')}-trigger`);
@@ -53,9 +52,9 @@ import { loadCSSFile } from '../lib/utils/css_utils';
this.pickImageEl = this.getElement(pickImageEl);
this.modalCrop = isString(modalCrop) ? $(modalCrop) : modalCrop;
this.uploadImageBtn = isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
- this.modalCropImg = isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.onBlobChange = onBlobChange;
+ this.cropperInstance = null;
this.bindEvents();
}
@@ -78,7 +77,8 @@ import { loadCSSFile } from '../lib/utils/css_utils';
return _this.onActionBtnClick(btn);
});
this.onBlobChange(null);
- return (this.croppedImageBlob = null);
+ this.croppedImageBlob = null;
+ return null;
}
onPickImageClick() {
@@ -87,7 +87,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
onModalShow() {
const _this = this;
- return this.modalCropImg.cropper({
+ this.cropperInstance = new Cropper(this.modalCropImg, {
viewMode: 1,
center: false,
aspectRatio: 1,
@@ -104,11 +104,10 @@ import { loadCSSFile } from '../lib/utils/css_utils';
cropBoxResizable: false,
toggleDragModeOnDblclick: false,
built() {
- const $image = $(this);
- const container = $image.cropper('getContainerData');
+ const container = this.cropperInstance.getContainerData();
const { cropBoxWidth, cropBoxHeight } = _this;
- return $image.cropper('setCropBoxData', {
+ return this.cropperInstance.setCropBoxData({
width: cropBoxWidth,
height: cropBoxHeight,
left: (container.width - cropBoxWidth) / 2,
@@ -119,7 +118,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
}
onModalHide() {
- this.modalCropImg.attr('src', '').cropper('destroy');
+ this.cropperInstance.destroy();
const modalElement = document.querySelector('.modal-profile-crop');
if (modalElement) modalElement.remove();
}
@@ -134,9 +133,10 @@ import { loadCSSFile } from '../lib/utils/css_utils';
onActionBtnClick(btn) {
const data = $(btn).data();
- if (this.modalCropImg.data('cropper') && data.method) {
- return this.modalCropImg.cropper(data.method, data.option);
+ if (data.method) {
+ return this.cropperInstance[data.method](data.option);
}
+ return null;
}
onFileInputChange(e, input) {
@@ -146,7 +146,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
readFile(input) {
const reader = new FileReader();
reader.onload = () => {
- this.modalCropImg.attr('src', reader.result);
+ this.modalCropImg.setAttribute('src', reader.result);
import(/* webpackChunkName: 'bootstrapModal' */ 'bootstrap/js/dist/modal')
.then(() => {
this.modalCrop.modal('show');
@@ -162,7 +162,7 @@ import { loadCSSFile } from '../lib/utils/css_utils';
return reader.readAsDataURL(input.files[0]);
}
- dataURLtoBlob(dataURL) {
+ static dataURLtoBlob(dataURL) {
let i = 0;
let len = 0;
const binary = atob(dataURL.split(',')[1]);
@@ -184,14 +184,14 @@ import { loadCSSFile } from '../lib/utils/css_utils';
}
setBlob() {
- this.dataURL = this.modalCropImg
- .cropper('getCroppedCanvas', {
+ this.dataURL = this.cropperInstance
+ .getCroppedCanvas({
width: 200,
height: 200,
})
.toDataURL('image/png');
- this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
+ this.croppedImageBlob = GitLabCrop.dataURLtoBlob(this.dataURL);
this.onBlobChange(this.croppedImageBlob);
return this.croppedImageBlob;
}
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 2ccb360c7c1..16f0110a1af 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -23,7 +23,7 @@ export default class Profile {
modalCrop: '.modal-profile-crop',
pickImageEl: '.js-choose-user-avatar-button',
uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image',
+ modalCropImg: document.querySelector('.modal-profile-crop-image'),
};
this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
}
@@ -89,12 +89,9 @@ export default class Profile {
}
updateHeaderAvatar() {
- if (gon?.use_new_navigation) {
- $('[data-testid="user-dropdown"] .gl-avatar').attr('src', this.avatarGlCrop.dataURL);
- } else {
- $('.header-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- $('.js-sidebar-user-avatar').attr('src', this.avatarGlCrop.dataURL);
- }
+ const url = URL.createObjectURL(this.avatarGlCrop.getBlob());
+
+ document.dispatchEvent(new CustomEvent('userAvatar:update', { detail: { url } }));
}
setRepoRadio() {
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
index a4851b4fe4b..d2901500459 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_box_pipeline_status.vue
@@ -1,6 +1,6 @@
<script>
import { GlLoadingIcon } from '@gitlab/ui';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { createAlert } from '~/alert';
import { getQueryHeaders, toggleQueryPollingByVisibility } from '~/ci/pipeline_details/graph/utils';
import getLatestPipelineStatusQuery from '../graphql/queries/get_latest_pipeline_status.query.graphql';
diff --git a/app/assets/javascripts/projects/components/shared/delete_modal.vue b/app/assets/javascripts/projects/components/shared/delete_modal.vue
index 44e29d00d45..db2e283f9d2 100644
--- a/app/assets/javascripts/projects/components/shared/delete_modal.vue
+++ b/app/assets/javascripts/projects/components/shared/delete_modal.vue
@@ -74,7 +74,7 @@ export default {
attributes: {
variant: 'danger',
disabled: this.confirmDisabled,
- 'data-qa-selector': 'confirm_delete_button',
+ 'data-testid': 'confirm-delete-button',
},
},
cancel: {
@@ -147,7 +147,7 @@ export default {
v-model="userInput"
name="confirm_name_input"
type="text"
- data-qa-selector="confirm_name_field"
+ data-testid="confirm-name-field"
/>
<slot name="modal-footer"></slot>
</div>
diff --git a/app/assets/javascripts/projects/details/upload_button.vue b/app/assets/javascripts/projects/details/upload_button.vue
index e1c8c66a214..d19ec4bcab6 100644
--- a/app/assets/javascripts/projects/details/upload_button.vue
+++ b/app/assets/javascripts/projects/details/upload_button.vue
@@ -36,7 +36,10 @@ export default {
<span>
<gl-button
v-gl-modal="$options.uploadBlobModalId"
+ variant="link"
icon="upload"
+ class="stat-link gl-px-0!"
+ button-text-classes="gl-ml-2"
data-testid="upload-file-button"
>{{ __('Upload File') }}</gl-button
>
diff --git a/app/assets/javascripts/projects/new/components/app.vue b/app/assets/javascripts/projects/new/components/app.vue
index a841766a93c..7b55fe6c01c 100644
--- a/app/assets/javascripts/projects/new/components/app.vue
+++ b/app/assets/javascripts/projects/new/components/app.vue
@@ -2,7 +2,7 @@
import PROJECT_CREATE_FROM_TEMPLATE_SVG_URL from '@gitlab/svgs/dist/illustrations/project-create-from-template-sm.svg?url';
import PROJECT_CREATE_NEW_SVG_URL from '@gitlab/svgs/dist/illustrations/project-create-new-sm.svg?url';
import PROJECT_IMPORT_SVG_URL from '@gitlab/svgs/dist/illustrations/project-import-sm.svg?url';
-import PROJECT_RUN_CICD_PIPELINES_SVG_URL from '@gitlab/svgs/dist/illustrations/project-run-CICD-pipelines-sm.svg?url';
+import PROJECT_RUN_CICD_PIPELINES_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-devops-md.svg?url';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__ } from '~/locale';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
diff --git a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
index 06d96ef7bef..6422b4ac8d8 100644
--- a/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
+++ b/app/assets/javascripts/projects/pipelines/charts/components/pipeline_charts.vue
@@ -1,14 +1,14 @@
<script>
import { GlAlert, GlSkeletonLoader } from '@gitlab/ui';
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import { GlColumnChart, GlChartSeriesLabel } from '@gitlab/ui/dist/charts';
import dateFormat from '~/lib/dateformat';
import { getDateInPast } from '~/lib/utils/datetime_utility';
import { __, s__, sprintf } from '~/locale';
+import { dateFormats } from '~/analytics/shared/constants';
import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue';
import {
DEFAULT,
CHART_CONTAINER_HEIGHT,
- CHART_DATE_FORMAT,
INNER_CHART_HEIGHT,
ONE_WEEK_AGO_DAYS,
ONE_MONTH_AGO_DAYS,
@@ -51,6 +51,7 @@ export default {
components: {
GlAlert,
GlColumnChart,
+ GlChartSeriesLabel,
GlSkeletonLoader,
StatisticsList,
CiCdAnalyticsCharts,
@@ -67,6 +68,8 @@ export default {
failureType: null,
analytics: { ...defaultAnalyticsValues },
counts: { ...defaultCountValues },
+ tooltipTitle: '',
+ tooltipContent: [],
};
},
apollo: {
@@ -248,6 +251,15 @@ export default {
],
};
},
+ formatTooltipText({ value, seriesData }) {
+ this.tooltipTitle = value;
+ this.tooltipContent = seriesData.map(({ seriesId, seriesName, color, value: metric }) => ({
+ key: seriesId,
+ name: seriesName,
+ color,
+ value: metric[1],
+ }));
+ },
},
successColor: '#366800',
chartContainerHeight: CHART_CONTAINER_HEIGHT,
@@ -286,9 +298,9 @@ export default {
lastYear: __('Last year'),
},
get chartRanges() {
- const today = dateFormat(new Date(), CHART_DATE_FORMAT);
+ const today = dateFormat(new Date(), dateFormats.defaultDate);
const pastDate = (timeScale) =>
- dateFormat(getDateInPast(new Date(), timeScale), CHART_DATE_FORMAT);
+ dateFormat(getDateInPast(new Date(), timeScale), dateFormats.defaultDate);
return {
lastWeekRange: sprintf(__('%{oneWeekAgo} - %{today}'), {
oneWeekAgo: pastDate(ONE_WEEK_AGO_DAYS),
@@ -335,7 +347,25 @@ export default {
<template v-if="!loading">
<hr />
<h4 class="gl-my-4">{{ __('Pipelines charts') }}</h4>
- <ci-cd-analytics-charts :charts="areaCharts" :chart-options="$options.areaChartOptions" />
+ <ci-cd-analytics-charts
+ :charts="areaCharts"
+ :chart-options="$options.areaChartOptions"
+ :format-tooltip-text="formatTooltipText"
+ >
+ <template #tooltip-title>{{ tooltipTitle }}</template>
+ <template #tooltip-content>
+ <div
+ v-for="{ key, name, color, value } in tooltipContent"
+ :key="key"
+ class="gl-display-flex gl-justify-content-space-between"
+ >
+ <gl-chart-series-label class="gl-font-sm gl-mr-7" :color="color">
+ {{ name }}
+ </gl-chart-series-label>
+ <div class="gl-font-weight-bold">{{ value }}</div>
+ </div>
+ </template>
+ </ci-cd-analytics-charts>
</template>
</div>
</template>
diff --git a/app/assets/javascripts/projects/pipelines/charts/constants.js b/app/assets/javascripts/projects/pipelines/charts/constants.js
index dcec77ac6a4..e2fda0d9bf1 100644
--- a/app/assets/javascripts/projects/pipelines/charts/constants.js
+++ b/app/assets/javascripts/projects/pipelines/charts/constants.js
@@ -12,8 +12,6 @@ export const ONE_MONTH_AGO_DAYS = 31;
export const ONE_YEAR_AGO_DAYS = 365;
-export const CHART_DATE_FORMAT = 'dd mmm';
-
export const DEFAULT = 'default';
export const PARSE_FAILURE = 'parse_failure';
export const LOAD_ANALYTICS_FAILURE = 'load_analytics_failure';
diff --git a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
index f5fb72e84bc..d1c143b96f7 100644
--- a/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
+++ b/app/assets/javascripts/projects/settings/components/default_branch_selector.vue
@@ -8,6 +8,11 @@ export default {
RefSelector,
},
props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
persistedDefaultBranch: {
type: String,
required: true,
@@ -26,6 +31,7 @@ export default {
</script>
<template>
<ref-selector
+ :disabled="disabled"
:value="persistedDefaultBranch"
class="gl-w-full"
:project-id="projectId"
diff --git a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
index 611561e38f2..8e64d29e947 100644
--- a/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
+++ b/app/assets/javascripts/projects/settings/mount_default_branch_selector.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { parseBoolean } from '~/lib/utils/common_utils';
import DefaultBranchSelector from './components/default_branch_selector.vue';
export default (el) => {
@@ -6,13 +7,14 @@ export default (el) => {
return null;
}
- const { projectId, defaultBranch } = el.dataset;
+ const { projectId, defaultBranch, disabled } = el.dataset;
return new Vue({
el,
render(createElement) {
return createElement(DefaultBranchSelector, {
props: {
+ disabled: parseBoolean(disabled),
persistedDefaultBranch: defaultBranch,
projectId,
},
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
index 0a5fa288828..9aca74c9863 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/components/branch_rule.vue
@@ -12,8 +12,8 @@ export const i18n = {
statusChecks: s__('BranchRules|%{total} status %{subject}'),
approvalRules: s__('BranchRules|%{total} approval %{subject}'),
matchingBranches: s__('BranchRules|%{total} matching %{subject}'),
- pushAccessLevels: s__('BranchRules|Allowed to merge'),
- mergeAccessLevels: s__('BranchRules|Allowed to push and merge'),
+ pushAccessLevels: s__('BranchRules|Allowed to push and merge'),
+ mergeAccessLevels: s__('BranchRules|Allowed to merge'),
};
export default {
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
index f7a9949db4b..6d5443a5df0 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email.vue
@@ -25,6 +25,10 @@ export default {
I18N_STATE_VERIFICATION_FINISHED_TOGGLE_HELP,
I18N_RESET_BUTTON_LABEL,
props: {
+ incomingEmail: {
+ type: String,
+ required: true,
+ },
customEmail: {
type: String,
required: true,
@@ -128,7 +132,13 @@ export default {
<p class="gl-mb-0">
<strong>{{ errorLabel }}</strong>
</p>
- <p>{{ errorDescription }}</p>
+ <p>
+ <gl-sprintf :message="errorDescription">
+ <template #incomingEmail>
+ <code>{{ incomingEmail }}</code>
+ </template>
+ </gl-sprintf>
+ </p>
</template>
<p>{{ resetNote }}</p>
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
index f72aa19bdf2..2fe3ea4215a 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/custom_email_wrapper.vue
@@ -216,6 +216,7 @@ export default {
<custom-email
v-if="customEmail"
+ :incoming-email="incomingEmail"
:custom-email="customEmail"
:smtp-address="smtpAddress"
:verification-state="verificationState"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
index 6674937be67..a9dca81b9f4 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_root.vue
@@ -4,13 +4,12 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import axios from '~/lib/utils/axios_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import { __, sprintf } from '~/locale';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ServiceDeskSetting from './service_desk_setting.vue';
const CustomEmailWrapper = () => import('./custom_email_wrapper.vue');
export default {
- serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk.html', {
+ serviceDeskEmailHelpPath: helpPagePath('/user/project/service_desk/configure.html', {
anchor: 'use-an-additional-service-desk-alias-email',
}),
components: {
@@ -23,7 +22,6 @@ export default {
directives: {
SafeHtml,
},
- mixins: [glFeatureFlagsMixin()],
inject: {
initialIsEnabled: {
default: false,
@@ -55,6 +53,9 @@ export default {
projectKey: {
default: '',
},
+ reopenIssueOnExternalParticipantNote: {
+ default: false,
+ },
addExternalParticipantsFromCc: {
default: false,
},
@@ -81,7 +82,7 @@ export default {
},
computed: {
showCustomEmailWrapper() {
- return this.glFeatures.serviceDeskCustomEmail && this.isEnabled && this.isIssueTrackerEnabled;
+ return this.isEnabled && this.isIssueTrackerEnabled;
},
},
methods: {
@@ -117,6 +118,7 @@ export default {
fileTemplateProjectId,
outgoingName,
projectKey,
+ reopenIssueOnExternalParticipantNote,
addExternalParticipantsFromCc,
}) {
this.isTemplateSaving = true;
@@ -125,6 +127,7 @@ export default {
issue_template_key: selectedTemplate,
outgoing_name: outgoingName,
project_key: projectKey,
+ reopen_issue_on_external_participant_note: reopenIssueOnExternalParticipantNote,
add_external_participants_from_cc: addExternalParticipantsFromCc,
service_desk_enabled: this.isEnabled,
file_template_project_id: fileTemplateProjectId,
@@ -197,6 +200,7 @@ export default {
:initial-selected-file-template-project-id="selectedFileTemplateProjectId"
:initial-outgoing-name="outgoingName"
:initial-project-key="projectKey"
+ :initial-reopen-issue-on-external-participant-note="reopenIssueOnExternalParticipantNote"
:initial-add-external-participants-from-cc="addExternalParticipantsFromCc"
:templates="templates"
:is-template-saving="isTemplateSaving"
diff --git a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
index 5febb6ff0aa..2853a7d8d72 100644
--- a/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
+++ b/app/assets/javascripts/projects/settings_service_desk/components/service_desk_setting.vue
@@ -23,6 +23,12 @@ export default {
issueTrackerEnableMessage: __(
'To use Service Desk in this project, you must %{linkStart}activate the issue tracker%{linkEnd}.',
),
+ reopenIssueOnExternalParticipantNote: {
+ label: s__('ServiceDesk|Reopen issues when an external participant comments'),
+ help: s__(
+ 'ServiceDesk|This also adds an internal comment that mentions the assignees of the issue.',
+ ),
+ },
addExternalParticipantsFromCc: {
label: s__('ServiceDesk|Add external participants from the %{codeStart}Cc%{codeEnd} header'),
help: s__(
@@ -91,6 +97,11 @@ export default {
required: false,
default: '',
},
+ initialReopenIssueOnExternalParticipantNote: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
initialAddExternalParticipantsFromCc: {
type: Boolean,
required: false,
@@ -113,6 +124,7 @@ export default {
selectedFileTemplateProjectId: this.initialSelectedFileTemplateProjectId,
outgoingName: this.initialOutgoingName || __('GitLab Support Bot'),
projectKey: this.initialProjectKey,
+ reopenIssueOnExternalParticipantNote: this.initialReopenIssueOnExternalParticipantNote,
addExternalParticipantsFromCc: this.initialAddExternalParticipantsFromCc,
searchTerm: '',
projectKeyError: null,
@@ -132,12 +144,12 @@ export default {
return this.serviceDeskEmail && this.serviceDeskEmail !== this.incomingEmail;
},
emailSuffixHelpUrl() {
- return helpPagePath('user/project/service_desk.html', {
+ return helpPagePath('user/project/service_desk/configure.html', {
anchor: 'configure-a-suffix-for-service-desk-alias-email',
});
},
serviceDeskEmailAddressHelpUrl() {
- return helpPagePath('user/project/service_desk.html', {
+ return helpPagePath('user/project/service_desk/configure.html', {
anchor: 'use-an-additional-service-desk-alias-email',
});
},
@@ -156,6 +168,7 @@ export default {
selectedTemplate: this.selectedTemplate,
outgoingName: this.outgoingName,
projectKey: this.projectKey,
+ reopenIssueOnExternalParticipantNote: this.reopenIssueOnExternalParticipantNote,
addExternalParticipantsFromCc: this.addExternalParticipantsFromCc,
fileTemplateProjectId: this.selectedFileTemplateProjectId,
});
@@ -323,9 +336,22 @@ export default {
</gl-form-group>
<gl-form-checkbox
+ v-model="reopenIssueOnExternalParticipantNote"
+ :disabled="!isIssueTrackerEnabled"
+ data-testid="reopen-issue-on-external-participant-note"
+ >
+ {{ $options.i18n.reopenIssueOnExternalParticipantNote.label }}
+
+ <template #help>
+ {{ $options.i18n.reopenIssueOnExternalParticipantNote.help }}
+ </template>
+ </gl-form-checkbox>
+
+ <gl-form-checkbox
v-if="showAddExternalParticipantsFromCC"
v-model="addExternalParticipantsFromCc"
:disabled="!isIssueTrackerEnabled"
+ data-testid="add-external-participants-from-cc"
>
<gl-sprintf :message="$options.i18n.addExternalParticipantsFromCc.label">
<template #code="{ content }">
diff --git a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
index 8ac186e292c..a38da7add03 100644
--- a/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
+++ b/app/assets/javascripts/projects/settings_service_desk/custom_email_constants.js
@@ -112,7 +112,7 @@ export const I18N_ERROR_SMTP_HOST_ISSUE_DESC = s__(
);
export const I18N_ERROR_INVALID_CREDENTIALS_LABEL = s__('ServiceDesk|Invalid credentials');
export const I18N_ERROR_INVALID_CREDENTIALS_DESC = s__(
- 'ServiceDesk|The given credentials (username and password) were rejected by the SMTP server.',
+ 'ServiceDesk|The given credentials (username and password) were rejected by the SMTP server, or you need to explicitly set an authentication method.',
);
export const I18N_ERROR_MAIL_NOT_RECEIVED_IN_TIMEFRAME_LABEL = s__(
'ServiceDesk|Verification email not received within timeframe',
@@ -128,6 +128,16 @@ export const I18N_ERROR_INCORRECT_TOKEN_LABEL = s__('ServiceDesk|Incorrect verif
export const I18N_ERROR_INCORRECT_TOKEN_DESC = s__(
"ServiceDesk|The received email didn't contain the verification token that was sent to your email address.",
);
+export const I18N_ERROR_READ_TIMEOUT_LABEL = s__('ServiceDesk|Read timeout');
+export const I18N_ERROR_READ_TIMEOUT_DESC = s__(
+ 'ServiceDesk|The SMTP server did not respond in time.',
+);
+export const I18N_ERROR_INCORRECT_FORWARDING_TARGET_LABEL = s__(
+ 'ServiceDesk|Incorrect forwarding target',
+);
+export const I18N_ERROR_INCORRECT_FORWARDING_TARGET_DESC = s__(
+ 'ServiceDesk|Forward all emails to the custom email address to %{incomingEmail}.',
+);
export const I18N_VERIFICATION_ERRORS = {
smtp_host_issue: {
@@ -150,4 +160,12 @@ export const I18N_VERIFICATION_ERRORS = {
label: I18N_ERROR_INCORRECT_TOKEN_LABEL,
description: I18N_ERROR_INCORRECT_TOKEN_DESC,
},
+ read_timeout: {
+ label: I18N_ERROR_READ_TIMEOUT_LABEL,
+ description: I18N_ERROR_READ_TIMEOUT_DESC,
+ },
+ incorrect_forwarding_target: {
+ label: I18N_ERROR_INCORRECT_FORWARDING_TARGET_LABEL,
+ description: I18N_ERROR_INCORRECT_FORWARDING_TARGET_DESC,
+ },
};
diff --git a/app/assets/javascripts/projects/settings_service_desk/index.js b/app/assets/javascripts/projects/settings_service_desk/index.js
index ce223b349bf..a3c310ec501 100644
--- a/app/assets/javascripts/projects/settings_service_desk/index.js
+++ b/app/assets/javascripts/projects/settings_service_desk/index.js
@@ -21,6 +21,7 @@ export default () => {
incomingEmail,
outgoingName,
projectKey,
+ reopenIssueOnExternalParticipantNote,
addExternalParticipantsFromCc,
selectedTemplate,
selectedFileTemplateProjectId,
@@ -40,6 +41,7 @@ export default () => {
isIssueTrackerEnabled: parseBoolean(issueTrackerEnabled),
outgoingName,
projectKey,
+ reopenIssueOnExternalParticipantNote: parseBoolean(reopenIssueOnExternalParticipantNote),
addExternalParticipantsFromCc: parseBoolean(addExternalParticipantsFromCc),
selectedTemplate,
selectedFileTemplateProjectId: parseInt(selectedFileTemplateProjectId, 10) || null,
diff --git a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
index 074cddac422..7286f91d807 100644
--- a/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
+++ b/app/assets/javascripts/projects/tree/components/commit_pipeline_status.vue
@@ -4,7 +4,7 @@ import Visibility from 'visibilityjs';
import { createAlert } from '~/alert';
import Poll from '~/lib/utils/poll';
import { __, s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import CommitPipelineService from '../services/commit_pipeline_service';
export default {
@@ -92,7 +92,7 @@ export default {
</script>
<template>
<div class="gl-ml-5">
- <gl-loading-icon v-if="isLoading" size="lg" label="Loading pipeline status" />
+ <gl-loading-icon v-if="isLoading" size="sm" label="Loading pipeline status" />
<a v-else :href="ciStatus.details_path">
<ci-icon :status="ciStatus" :title="statusTitle" :aria-label="statusTitle" />
</a>
diff --git a/app/assets/javascripts/ref/components/ref_selector.vue b/app/assets/javascripts/ref/components/ref_selector.vue
index ed9fd521e67..0d6b19829f2 100644
--- a/app/assets/javascripts/ref/components/ref_selector.vue
+++ b/app/assets/javascripts/ref/components/ref_selector.vue
@@ -28,12 +28,17 @@ export default {
},
inheritAttrs: false,
props: {
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
enabledRefTypes: {
type: Array,
required: false,
default: () => ALL_REF_TYPES,
validator: (val) =>
- // It has to be an arrray
+ // It has to be an array
isArray(val) &&
// with at least one item
val.length > 0 &&
@@ -234,6 +239,10 @@ export default {
this.debouncedSearch();
},
selectRef(ref) {
+ if (this.disabled) {
+ return;
+ }
+
this.setSelectedRef(ref);
this.$emit('input', this.selectedRef);
},
@@ -262,6 +271,7 @@ export default {
:toggle-class="extendedToggleButtonClass"
:toggle-text="buttonText"
:icon="dropdownIcon"
+ :disabled="disabled"
v-bind="$attrs"
v-on="$listeners"
@hidden="$emit('hide')"
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index df9f333afe5..228007dd7d6 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -176,7 +176,10 @@ export default {
</p>
<form v-if="showForm" class="js-quick-submit" @submit.prevent="submitForm">
<tag-field />
- <gl-form-group :label="__('Release title')">
+ <gl-form-group
+ :label="__('Release title')"
+ :description="s__('Release|Leave blank to use the tag name as the release title.')"
+ >
<gl-form-input
id="release-title"
ref="releaseTitleInput"
@@ -194,12 +197,16 @@ export default {
:extra-links="milestoneComboboxExtraLinks"
/>
</gl-form-group>
- <gl-form-group :label="__('Release date')" label-for="release-released-at">
- <template #label-description>
+ <gl-form-group
+ :label="__('Release date')"
+ :label-description="__('The date when the release is ready.')"
+ label-for="release-released-at"
+ >
+ <template #description>
<gl-sprintf
:message="
__(
- 'The date when the release is ready. A release with a date in the future is labeled as an %{linkStart}Upcoming Release%{linkEnd}.',
+ 'A release with a date in the future is labeled as an %{linkStart}Upcoming Release%{linkEnd}.',
)
"
>
diff --git a/app/assets/javascripts/repository/commits_service.js b/app/assets/javascripts/repository/commits_service.js
index 1e0de045d39..44113788716 100644
--- a/app/assets/javascripts/repository/commits_service.js
+++ b/app/assets/javascripts/repository/commits_service.js
@@ -31,13 +31,14 @@ const fetchData = (projectPath, path, ref, offset, refType) => {
fetchedBatches.push(offset);
+ const encodePathFunc = gon.features.encodingLogsTree ? encodeURI : encodeURIComponent;
const url = joinPaths(
gon.relative_url_root || '/',
projectPath,
'/-/refs/',
- encodeURIComponent(ref),
+ encodePathFunc(ref),
'/logs_tree/',
- encodeURIComponent(removeLeadingSlash(path)),
+ encodePathFunc(removeLeadingSlash(path)),
);
return axios
diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue
index 97a1cbda5d0..4ec57676b79 100644
--- a/app/assets/javascripts/repository/components/blob_content_viewer.vue
+++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue
@@ -79,7 +79,7 @@ export default {
const urlHash = getLocationHash(); // If there is a code line hash in the URL we render with the simple viewer
const useSimpleViewer = usePlain || urlHash?.startsWith('L') || !this.hasRichViewer;
- this.initHighlightWorker(this.blobInfo);
+ this.initHighlightWorker(this.blobInfo, this.isUsingLfs);
this.switchViewer(useSimpleViewer ? SIMPLE_BLOB_VIEWER : RICH_BLOB_VIEWER); // By default, if present, use the rich viewer to render
},
error() {
@@ -204,7 +204,8 @@ export default {
return this.blobInfo.storedExternally && this.blobInfo.externalStorage === LFS_STORAGE;
},
isBlameEnabled() {
- return this.glFeatures.blobBlameInfo && this.blobInfo.language === 'json'; // This feature is currently scoped to JSON files
+ // Blame information within the blob viewer is not yet supported in our fallback (HAML) viewers
+ return this.glFeatures.blobBlameInfo && !this.useFallback;
},
},
watch: {
@@ -295,7 +296,14 @@ export default {
},
handleToggleBlame() {
this.switchViewer(SIMPLE_BLOB_VIEWER);
- this.showBlame = !this.showBlame;
+
+ if (this.$route?.query?.plain === '0') {
+ // If the user is not viewing plain code and clicks the blame button, we always want to show blame info
+ // For instance, when viewing the rendered version of a Markdown file
+ this.showBlame = true;
+ } else {
+ this.showBlame = !this.showBlame;
+ }
const blame = this.showBlame === true ? '1' : '0';
if (this.$route?.query?.blame === blame) return;
diff --git a/app/assets/javascripts/repository/components/blob_controls.vue b/app/assets/javascripts/repository/components/blob_controls.vue
index 4730c9575da..c64100f4f36 100644
--- a/app/assets/javascripts/repository/components/blob_controls.vue
+++ b/app/assets/javascripts/repository/components/blob_controls.vue
@@ -4,6 +4,7 @@ import { __ } from '~/locale';
import { createAlert } from '~/alert';
import getRefMixin from '~/repository/mixins/get_ref';
import initSourcegraph from '~/sourcegraph';
+import { addShortcutsExtension } from '~/behaviors/shortcuts';
import ShortcutsBlob from '~/behaviors/shortcuts/shortcuts_blob';
import BlobLinePermalinkUpdater from '~/blob/blob_line_permalink_updater';
import { updateElementsVisibility } from '../utils/dom';
@@ -105,8 +106,7 @@ export default {
);
const fileBlobPermalinkUrl =
fileBlobPermalinkUrlElement && fileBlobPermalinkUrlElement.getAttribute('href');
- // eslint-disable-next-line no-new
- new ShortcutsBlob({
+ addShortcutsExtension(ShortcutsBlob, {
fileBlobPermalinkUrl,
fileBlobPermalinkUrlElement,
});
@@ -124,7 +124,7 @@ export default {
</script>
<template>
- <div v-if="showBlobControls" class="gl-display-flex gl-gap-3">
+ <div v-if="showBlobControls" class="gl-display-flex gl-gap-3 gl-align-items-baseline">
<gl-button data-testid="find" :href="blobInfo.findFilePath" :class="$options.buttonClassList">
{{ $options.i18n.findFile }}
</gl-button>
diff --git a/app/assets/javascripts/repository/components/blob_viewers/index.js b/app/assets/javascripts/repository/components/blob_viewers/index.js
index d434700b29f..016f7f9fe43 100644
--- a/app/assets/javascripts/repository/components/blob_viewers/index.js
+++ b/app/assets/javascripts/repository/components/blob_viewers/index.js
@@ -1,4 +1,4 @@
-import { TEXT_FILE_TYPE, JSON_LANGUAGE } from '../../constants';
+import { TEXT_FILE_TYPE } from '../../constants';
export const viewers = {
csv: () => import('./csv_viewer.vue'),
@@ -17,12 +17,10 @@ export const viewers = {
geo_json: () => import('./geo_json/geo_json_viewer.vue'),
};
-export const loadViewer = (type, isUsingLfs, hljsWorkerEnabled, language) => {
+export const loadViewer = (type, isUsingLfs, hljsWorkerEnabled) => {
let viewer = viewers[type];
- if (hljsWorkerEnabled && language === JSON_LANGUAGE && type === TEXT_FILE_TYPE) {
- // The New Source Viewer currently only supports JSON files.
- // More language support will be added in: https://gitlab.com/gitlab-org/gitlab/-/issues/415753
+ if (hljsWorkerEnabled && type === TEXT_FILE_TYPE) {
viewer = () => import('~/vue_shared/components/source_viewer/source_viewer_new.vue');
}
diff --git a/app/assets/javascripts/repository/components/commit_info.vue b/app/assets/javascripts/repository/components/commit_info.vue
index b6674114a20..319ce2cea84 100644
--- a/app/assets/javascripts/repository/components/commit_info.vue
+++ b/app/assets/javascripts/repository/components/commit_info.vue
@@ -77,25 +77,32 @@ export default {
:size="32"
/>
<div class="commit-detail flex-list gl-display-flex gl-flex-grow-1 gl-min-w-0">
- <div class="commit-content gl-w-full gl-text-truncate" data-testid="commit-content">
- <gl-link
- v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
- :href="commit.webPath"
- :class="{ 'gl-font-style-italic': !commit.message }"
- class="commit-row-message item-title"
- />
- <gl-button
- v-if="commit.descriptionHtml"
- v-gl-tooltip
- :class="{ open: showDescription }"
- :title="$options.i18n.toggleCommitDescription"
- :aria-label="$options.i18n.toggleCommitDescription"
- :selected="showDescription"
- class="text-expander gl-vertical-align-bottom!"
- icon="ellipsis_h"
- @click="toggleShowDescription"
- />
- <div class="committer">
+ <div
+ class="commit-content gl-w-full gl-display-inline-flex gl-flex-wrap gl-align-items-baseline"
+ data-testid="commit-content"
+ >
+ <div
+ class="gl-flex-basis-full gl-display-inline-flex gl-align-items-center gl-column-gap-3"
+ >
+ <gl-link
+ v-safe-html:[$options.safeHtmlConfig]="commit.titleHtml"
+ :href="commit.webPath"
+ :class="{ 'gl-font-style-italic': !commit.message }"
+ class="commit-row-message item-title gl-line-clamp-1 gl-word-break-all!"
+ />
+ <gl-button
+ v-if="commit.descriptionHtml"
+ v-gl-tooltip
+ :class="{ open: showDescription }"
+ :title="$options.i18n.toggleCommitDescription"
+ :aria-label="$options.i18n.toggleCommitDescription"
+ :selected="showDescription"
+ class="text-expander gl-ml-0!"
+ icon="ellipsis_h"
+ @click="toggleShowDescription"
+ />
+ </div>
+ <div class="committer gl-flex-basis-full">
<gl-link
v-if="commit.author"
:href="commit.author.webPath"
diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue
index 079d4c522a8..8d4c4384e1d 100644
--- a/app/assets/javascripts/repository/components/delete_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue
@@ -269,6 +269,7 @@ export default {
:invalid-feedback="form.fields['commit_message'].feedback"
>
<gl-form-textarea
+ id="commit_message"
ref="message"
v-model="form.fields['commit_message'].value"
v-validation:[form.showValidation]
@@ -289,6 +290,7 @@ export default {
:invalid-feedback="form.fields['branch_name'].feedback"
>
<gl-form-input
+ id="branch_name"
v-model="form.fields['branch_name'].value"
v-validation:[form.showValidation]
:state="form.fields['branch_name'].state"
diff --git a/app/assets/javascripts/repository/components/last_commit.vue b/app/assets/javascripts/repository/components/last_commit.vue
index 7f7a76cd4aa..b85a4138ef1 100644
--- a/app/assets/javascripts/repository/components/last_commit.vue
+++ b/app/assets/javascripts/repository/components/last_commit.vue
@@ -3,7 +3,7 @@ import { GlTooltipDirective, GlButton, GlButtonGroup, GlLoadingIcon } from '@git
import SafeHtml from '~/vue_shared/directives/safe_html';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { sprintf, s__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
import getRefMixin from '../mixins/get_ref';
@@ -106,7 +106,7 @@ export default {
</script>
<template>
- <gl-loading-icon v-if="isLoading" size="lg" color="dark" class="m-auto" />
+ <gl-loading-icon v-if="isLoading" size="md" color="dark" class="m-auto" />
<commit-info v-else-if="commit" :commit="commit">
<div
class="commit-actions gl-display-flex gl-flex-align gl-align-items-center gl-flex-direction-row"
diff --git a/app/assets/javascripts/repository/components/table/index.vue b/app/assets/javascripts/repository/components/table/index.vue
index 3da7daa3eec..912cc4d2b1c 100644
--- a/app/assets/javascripts/repository/components/table/index.vue
+++ b/app/assets/javascripts/repository/components/table/index.vue
@@ -81,22 +81,27 @@ export default {
return ['', '/'].indexOf(this.path) === -1;
},
},
- watch: {
- $route: function routeChange() {
- this.$options.totalRowsLoaded = -1;
- },
- },
- totalRowsLoaded: -1,
methods: {
showMore() {
this.$emit('showMore');
},
- generateRowNumber(path, id, index) {
- const key = `${path}-${id}-${index}`;
+ generateRowNumber(entry, index) {
+ const { flatPath, id } = entry;
+ const key = `${flatPath}-${id}-${index}`;
+
+ // We adjust the offset that we request based on the type of entry
+ const numTrees = this.entries?.trees?.length || 0;
+ const numBlobs = this.entries?.blobs?.length || 0;
if (!this.rowNumbers[key] && this.rowNumbers[key] !== 0) {
- this.$options.totalRowsLoaded += 1;
- this.rowNumbers[key] = this.$options.totalRowsLoaded;
+ if (entry.type === 'commit') {
+ // submodules are rendered before blobs but are in the last pages the api response
+ this.rowNumbers[key] = numTrees + numBlobs + index;
+ } else if (entry.type === 'blob') {
+ this.rowNumbers[key] = numTrees + index;
+ } else {
+ this.rowNumbers[key] = index;
+ }
}
return this.rowNumbers[key];
@@ -145,7 +150,7 @@ export default {
:lfs-oid="entry.lfsOid"
:loading-path="loadingPath"
:total-entries="totalEntries"
- :row-number="generateRowNumber(entry.flatPath, entry.id, index)"
+ :row-number="generateRowNumber(entry, index)"
:commit-info="getCommit(entry.name)"
v-on="$listeners"
/>
diff --git a/app/assets/javascripts/repository/components/tree_content.vue b/app/assets/javascripts/repository/components/tree_content.vue
index dd2cfddc94e..49d9e09dde5 100644
--- a/app/assets/javascripts/repository/components/tree_content.vue
+++ b/app/assets/javascripts/repository/components/tree_content.vue
@@ -165,12 +165,8 @@ export default {
return;
}
- // Since a user could scroll either up or down, we want to support lazy loading in both directions
- this.loadCommitData(rowNumber);
-
- if (rowNumber - COMMIT_BATCH_SIZE >= 0) {
- this.loadCommitData(rowNumber - COMMIT_BATCH_SIZE);
- }
+ // Assume we are loading from the top and greedily choose offsets in multiples of COMMIT_BATCH_SIZE to minimize number of requests
+ this.loadCommitData(rowNumber - (rowNumber % COMMIT_BATCH_SIZE));
},
loadCommitData(rowNumber) {
loadCommits(this.projectPath, this.path, this.ref, rowNumber, this.refType)
diff --git a/app/assets/javascripts/repository/mixins/highlight_mixin.js b/app/assets/javascripts/repository/mixins/highlight_mixin.js
index 5b6f68681bb..422a84dff40 100644
--- a/app/assets/javascripts/repository/mixins/highlight_mixin.js
+++ b/app/assets/javascripts/repository/mixins/highlight_mixin.js
@@ -3,11 +3,14 @@ import {
EVENT_ACTION,
EVENT_LABEL_FALLBACK,
LINES_PER_CHUNK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
} from '~/vue_shared/components/source_viewer/constants';
import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
import LineHighlighter from '~/blob/line_highlighter';
import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
+import { TEXT_FILE_TYPE } from '../constants';
/*
* This mixin is intended to be used as an interface between our highlight worker and Vue components
@@ -27,8 +30,9 @@ export default {
this.track(EVENT_ACTION, { label, property: language });
},
isUnsupportedLanguage(language) {
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
const supportedLanguages = Object.keys(languageLoader);
- const isUnsupportedLanguage = !supportedLanguages.includes(language);
+ const isUnsupportedLanguage = !supportedLanguages.includes(mappedLanguage);
return LEGACY_FALLBACKS.includes(language) || isUnsupportedLanguage;
},
@@ -36,14 +40,29 @@ export default {
this.trackEvent(EVENT_LABEL_FALLBACK, language);
this?.onError();
},
- initHighlightWorker({ rawTextBlob, language, fileType }) {
- if (language !== 'json' || !this.glFeatures.highlightJsWorker) return;
+ async handleLFSBlob(externalStorageUrl, rawPath, language) {
+ await axios
+ .get(externalStorageUrl || rawPath)
+ .then(({ data }) => this.instructWorker(data, language))
+ .catch(() => this.$emit('error'));
+ },
+ initHighlightWorker(blob, isUsingLfs) {
+ const { rawTextBlob, language, fileType, externalStorageUrl, rawPath, simpleViewer } = blob;
+
+ if (!this.glFeatures.highlightJsWorker || simpleViewer?.fileType !== TEXT_FILE_TYPE) return;
if (this.isUnsupportedLanguage(language)) {
this.handleUnsupportedLanguage(language);
return;
}
+ this.highlightWorker.onmessage = this.handleWorkerMessage;
+
+ if (isUsingLfs) {
+ this.handleLFSBlob(externalStorageUrl, rawPath, language);
+ return;
+ }
+
/*
* We want to start rendering content as soon as possible, but highlighting large amounts of
* content can take long, so we render the content in phases:
@@ -64,8 +83,6 @@ export default {
this.chunks = splitIntoChunks(language, firstSeventyLines);
- this.highlightWorker.onmessage = this.handleWorkerMessage;
-
// Instruct the worker to highlight the first 70 lines ASAP, this improves perceived performance.
this.instructWorker(firstSeventyLines, language);
diff --git a/app/assets/javascripts/search/index.js b/app/assets/javascripts/search/index.js
index f83130213f2..5414e712912 100644
--- a/app/assets/javascripts/search/index.js
+++ b/app/assets/javascripts/search/index.js
@@ -1,22 +1,69 @@
import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { queryToObject } from '~/lib/utils/url_utility';
import syntaxHighlight from '~/syntax_highlight';
-import { initSidebar, sidebarInitState } from './sidebar';
+import { initSidebar } from './sidebar';
import { initSearchSort } from './sort';
import createStore from './store';
import { initTopbar } from './topbar';
import { initBlobRefSwitcher } from './under_topbar';
+const sidebarInitState = () => {
+ const el = document.getElementById('js-search-sidebar');
+ if (!el) return {};
+
+ const {
+ navigationJson,
+ searchType,
+ searchLevel,
+ groupInitialJson,
+ projectInitialJson,
+ } = el.dataset;
+
+ const navigationJsonParsed = JSON.parse(navigationJson);
+ const groupInitialJsonParsed = JSON.parse(groupInitialJson);
+ const projectInitialJsonParsed = JSON.parse(projectInitialJson);
+
+ return {
+ navigationJsonParsed,
+ searchType,
+ searchLevel,
+ groupInitialJsonParsed,
+ projectInitialJsonParsed,
+ };
+};
+
+const topBarInitState = () => {
+ const el = document.getElementById('js-search-topbar');
+
+ if (!el) {
+ return false;
+ }
+
+ const { defaultBranchName } = el.dataset;
+ return { defaultBranchName };
+};
+
export const initSearchApp = () => {
syntaxHighlight(document.querySelectorAll('.js-search-results'));
const query = queryToObject(window.location.search, { gatherArrays: true });
- const { navigationJsonParsed: navigation, searchType } = sidebarInitState() || {};
+ const {
+ navigationJsonParsed: navigation,
+ searchType,
+ searchLevel,
+ groupInitialJsonParsed: groupInitialJson,
+ projectInitialJsonParsed: projectInitialJson,
+ } = sidebarInitState() || {};
+
+ const { defaultBranchName } = topBarInitState() || {};
const store = createStore({
query,
navigation,
- useSidebarNavigation: gon.use_new_navigation,
searchType,
+ searchLevel,
+ groupInitialJson,
+ projectInitialJson,
+ defaultBranchName,
});
initTopbar(store);
diff --git a/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue b/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue
new file mode 100644
index 00000000000..cb017b6898b
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/all_scopes_start_filters.vue
@@ -0,0 +1,19 @@
+<script>
+import GroupFilter from './group_filter.vue';
+import ProjectFilter from './project_filter.vue';
+
+export default {
+ name: 'AllScopesStartFilters',
+ components: {
+ GroupFilter,
+ ProjectFilter,
+ },
+};
+</script>
+
+<template>
+ <div class="gl-px-5 gl-pt-6">
+ <group-filter class="gl-mb-5" />
+ <project-filter class="gl-mb-5" />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/app.vue b/app/assets/javascripts/search/sidebar/components/app.vue
index 86a5f5107f8..bbee0e441cc 100644
--- a/app/assets/javascripts/search/sidebar/components/app.vue
+++ b/app/assets/javascripts/search/sidebar/components/app.vue
@@ -2,9 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
-import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import SidebarPortal from '~/super_sidebar/components/sidebar_portal.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
@@ -27,6 +25,7 @@ import NotesFilters from './notes_filters.vue';
import CommitsFilters from './commits_filters.vue';
import MilestonesFilters from './milestones_filters.vue';
import WikiBlobsFilters from './wiki_blobs_filters.vue';
+import AllScopesStartFilters from './all_scopes_start_filters.vue';
export default {
name: 'GlobalSearchSidebar',
@@ -37,18 +36,23 @@ export default {
ProjectsFilters,
NotesFilters,
WikiBlobsFilters,
- ScopeLegacyNavigation,
ScopeSidebarNavigation,
SidebarPortal,
DomElementListener,
- SmallScreenDrawerNavigation,
CommitsFilters,
MilestonesFilters,
+ AllScopesStartFilters,
},
mixins: [glFeatureFlagsMixin()],
+ props: {
+ headerText: {
+ required: false,
+ type: String,
+ default: '',
+ },
+ },
computed: {
- // useSidebarNavigation refers to whether the new left sidebar navigation is enabled
- ...mapState(['useSidebarNavigation', 'searchType']),
+ ...mapState(['searchType']),
...mapGetters(['currentScope']),
showIssuesFilters() {
return this.currentScope === SCOPE_ISSUES;
@@ -72,16 +76,7 @@ export default {
return this.currentScope === SCOPE_MILESTONES;
},
showWikiBlobsFilters() {
- return (
- this.currentScope === SCOPE_WIKI_BLOBS &&
- this.glFeatures?.searchProjectWikisHideArchivedProjects
- );
- },
- showScopeNavigation() {
- // showScopeNavigation refers to whether the scope navigation should be shown
- // while the legacy navigation is being used and there are no search results
- // the scope navigation has to be hidden
- return Boolean(this.currentScope);
+ return this.currentScope === SCOPE_WIKI_BLOBS;
},
},
methods: {
@@ -93,9 +88,16 @@ export default {
</script>
<template>
- <section v-if="useSidebarNavigation">
+ <section>
<dom-element-listener selector="#js-open-mobile-filters" @click="toggleFiltersFromSidebar" />
<sidebar-portal>
+ <all-scopes-start-filters />
+ <div
+ v-if="headerText"
+ class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
+ >
+ {{ headerText }}
+ </div>
<scope-sidebar-navigation />
<issues-filters v-if="showIssuesFilters" />
<merge-requests-filters v-if="showMergeRequestFilters" />
@@ -107,32 +109,4 @@ export default {
<wiki-blobs-filters v-if="showWikiBlobsFilters" />
</sidebar-portal>
</section>
-
- <section
- v-else-if="showScopeNavigation"
- class="gl-display-flex gl-flex-direction-column gl-lg-mr-0 gl-md-mr-5 gl-lg-mb-6 gl-lg-mt-5"
- >
- <div class="search-sidebar gl-display-none gl-lg-display-block">
- <scope-legacy-navigation />
- <issues-filters v-if="showIssuesFilters" />
- <merge-requests-filters v-if="showMergeRequestFilters" />
- <blobs-filters v-if="showBlobFilters" />
- <projects-filters v-if="showProjectsFilters" />
- <notes-filters v-if="showNotesFilters" />
- <commits-filters v-if="showCommitsFilters" />
- <milestones-filters v-if="showMilestonesFilters" />
- <wiki-blobs-filters v-if="showWikiBlobsFilters" />
- </div>
- <small-screen-drawer-navigation class="gl-lg-display-none">
- <scope-legacy-navigation />
- <issues-filters v-if="showIssuesFilters" />
- <merge-requests-filters v-if="showMergeRequestFilters" />
- <blobs-filters v-if="showBlobFilters" />
- <projects-filters v-if="showProjectsFilters" />
- <notes-filters v-if="showNotesFilters" />
- <commits-filters v-if="showCommitsFilters" />
- <milestones-filters v-if="showMilestonesFilters" />
- <wiki-blobs-filters v-if="showWikiBlobsFilters" />
- </small-screen-drawer-navigation>
- </section>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
index b0e84beabc4..0308db17dc4 100644
--- a/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/archived_filter/index.vue
@@ -21,7 +21,7 @@ export default {
tooltip: s__('GlobalSearch|Include search results from archived projects'),
},
computed: {
- ...mapState(['urlQuery', 'useSidebarNavigation']),
+ ...mapState(['urlQuery']),
selectedFilter: {
get() {
return [parseBoolean(this.urlQuery?.include_archived)];
@@ -48,9 +48,9 @@ export default {
<template>
<gl-form-checkbox-group v-model="selectedFilter">
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ <div class="gl-mb-2 gl-font-weight-bold gl-font-sm" data-testid="archived-filter-title">
{{ $options.archivedFilterData.headerLabel }}
- </h5>
+ </div>
<gl-form-checkbox
class="gl-flex-grow-1 gl-display-inline-flex gl-justify-content-space-between gl-w-full"
:class="$options.LABEL_DEFAULT_CLASSES"
diff --git a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
index 0ed2c24efba..e282bacae31 100644
--- a/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/blobs_filters.vue
@@ -1,8 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters, mapState } from 'vuex';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { HR_DEFAULT_CLASSES } from '../constants';
import LanguageFilter from './language_filter/index.vue';
import ArchivedFilter from './archived_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -14,24 +10,12 @@ export default {
FiltersTemplate,
ArchivedFilter,
},
- mixins: [glFeatureFlagsMixin()],
- computed: {
- ...mapGetters(['currentScope']),
- ...mapState(['useSidebarNavigation', 'searchType']),
- showDivider() {
- return !this.useSidebarNavigation;
- },
- hrClasses() {
- return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
- },
- },
};
</script>
<template>
<filters-template>
<language-filter class="gl-mb-5" />
- <hr v-if="showDivider" :class="hrClasses" />
<archived-filter class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
index 176614be6da..4e91158fa36 100644
--- a/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/confidentiality_filter/index.vue
@@ -1,6 +1,4 @@
<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapState } from 'vuex';
import RadioFilter from '../radio_filter.vue';
import { confidentialFilterData } from './data';
@@ -9,9 +7,6 @@ export default {
components: {
RadioFilter,
},
- computed: {
- ...mapState(['useSidebarNavigation']),
- },
confidentialFilterData,
};
</script>
diff --git a/app/assets/javascripts/search/sidebar/components/filters_template.vue b/app/assets/javascripts/search/sidebar/components/filters_template.vue
index 0f68bf92048..2f40a430bfa 100644
--- a/app/assets/javascripts/search/sidebar/components/filters_template.vue
+++ b/app/assets/javascripts/search/sidebar/components/filters_template.vue
@@ -5,7 +5,6 @@ import { mapActions, mapState, mapGetters } from 'vuex';
import Tracking from '~/tracking';
import {
- HR_DEFAULT_CLASSES,
TRACKING_ACTION_CLICK,
TRACKING_LABEL_APPLY,
TRACKING_LABEL_RESET,
@@ -19,7 +18,7 @@ export default {
GlForm,
},
computed: {
- ...mapState(['sidebarDirty', 'useSidebarNavigation']),
+ ...mapState(['sidebarDirty']),
...mapGetters(['currentScope']),
},
methods: {
@@ -37,15 +36,16 @@ export default {
this.resetQuery();
},
},
- HR_DEFAULT_CLASSES,
};
</script>
<template>
- <gl-form class="issue-filters gl-px-5 gl-pt-0" @submit.prevent="applyQueryWithTracking">
- <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
+ <gl-form
+ class="issue-filters gl-px-5 gl-pt-0"
+ :aria-label="__('Search filters')"
+ @submit.prevent="applyQueryWithTracking"
+ >
<slot></slot>
- <hr v-if="!useSidebarNavigation" :class="$options.HR_DEFAULT_CLASSES" />
<div class="gl-display-flex gl-align-items-center gl-mt-4">
<gl-button category="primary" variant="confirm" type="submit" :disabled="!sidebarDirty">
{{ __('Apply') }}
diff --git a/app/assets/javascripts/search/topbar/components/group_filter.vue b/app/assets/javascripts/search/sidebar/components/group_filter.vue
index a177eb28991..20231cdda6a 100644
--- a/app/assets/javascripts/search/topbar/components/group_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/group_filter.vue
@@ -2,37 +2,49 @@
import { isEmpty } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
import SearchableDropdown from './searchable_dropdown.vue';
export default {
name: 'GroupFilter',
+ i18n: {
+ groupFieldLabel: s__('GlobalSearch|Group'),
+ },
components: {
SearchableDropdown,
},
- props: {
- initialData: {
- type: Object,
- required: false,
- default: () => ({}),
- },
+ data() {
+ return {
+ search: '',
+ labelId: 'group-filter-dropdown-id',
+ };
},
computed: {
- ...mapState(['query', 'groups', 'fetchingGroups']),
+ ...mapState(['query', 'groups', 'fetchingGroups', 'groupInitialJson', 'useSidebarNavigation']),
...mapGetters(['frequentGroups', 'currentScope']),
selectedGroup() {
- return isEmpty(this.initialData) ? ANY_OPTION : this.initialData;
+ return isEmpty(this.groupInitialJson) ? ANY_OPTION : this.groupInitialJson;
+ },
+ },
+ watch: {
+ search() {
+ this.debounceSearch();
},
},
created() {
// This tracks groups searched via the top nav search bar
- if (this.query.nav_source === 'navbar' && this.initialData?.id) {
- this.setFrequentGroup(this.initialData);
+ if (this.query.nav_source === 'navbar' && this.groupInitialJson?.id) {
+ this.setFrequentGroup(this.groupInitialJson);
}
},
methods: {
...mapActions(['fetchGroups', 'setFrequentGroup', 'loadFrequentGroups']),
+ firstLoad() {
+ this.loadFrequentGroups();
+ this.fetchGroups();
+ },
handleGroupChange(group) {
// If group.id is null we are clearing the filter and don't need to store that in LS.
if (group.id) {
@@ -54,17 +66,22 @@ export default {
</script>
<template>
- <searchable-dropdown
- data-testid="group-filter"
- :header-text="$options.GROUP_DATA.headerText"
- :name="$options.GROUP_DATA.name"
- :full-name="$options.GROUP_DATA.fullName"
- :loading="fetchingGroups"
- :selected-item="selectedGroup"
- :items="groups"
- :frequent-items="frequentGroups"
- @first-open="loadFrequentGroups"
- @search="fetchGroups"
- @change="handleGroupChange"
- />
+ <div>
+ <h5 :id="labelId" class="gl-mt-0 gl-mb-5 gl-font-sm">
+ {{ $options.i18n.groupFieldLabel }}
+ </h5>
+ <searchable-dropdown
+ data-testid="group-filter"
+ :header-text="$options.GROUP_DATA.headerText"
+ :name="$options.GROUP_DATA.name"
+ :loading="fetchingGroups"
+ :selected-item="selectedGroup"
+ :items="groups"
+ :frequent-items="frequentGroups"
+ :search-handler="fetchGroups"
+ :label-id="labelId"
+ @first-open="firstLoad"
+ @change="handleGroupChange"
+ />
+ </div>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/issues_filters.vue b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
index a77fb34cdba..d815b68b98a 100644
--- a/app/assets/javascripts/search/sidebar/components/issues_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/issues_filters.vue
@@ -2,7 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapState } from 'vuex';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { HR_DEFAULT_CLASSES, SEARCH_TYPE_ADVANCED } from '../constants';
+import { SEARCH_TYPE_ADVANCED } from '../constants';
import { confidentialFilterData } from './confidentiality_filter/data';
import { statusFilterData } from './status_filter/data';
import ConfidentialityFilter from './confidentiality_filter/index.vue';
@@ -26,7 +26,7 @@ export default {
mixins: [glFeatureFlagsMixin()],
computed: {
...mapGetters(['currentScope']),
- ...mapState(['useSidebarNavigation', 'searchType']),
+ ...mapState(['searchType']),
showConfidentialityFilter() {
return Object.values(confidentialFilterData.scopes).includes(this.currentScope);
},
@@ -36,19 +36,12 @@ export default {
showLabelFilter() {
return (
Object.values(labelFilterData.scopes).includes(this.currentScope) &&
- this.glFeatures.searchIssueLabelAggregation &&
this.searchType === SEARCH_TYPE_ADVANCED
);
},
showArchivedFilter() {
return archivedFilterData.scopes.includes(this.currentScope);
},
- showDivider() {
- return !this.useSidebarNavigation;
- },
- hrClasses() {
- return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
- },
},
};
</script>
@@ -56,11 +49,8 @@ export default {
<template>
<filters-template>
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
- <hr v-if="showConfidentialityFilter && showDivider" :class="hrClasses" />
<confidentiality-filter v-if="showConfidentialityFilter" class="gl-mb-5" />
- <hr v-if="showLabelFilter && showDivider" :class="hrClasses" />
<label-filter v-if="showLabelFilter" class="gl-mb-5" />
- <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
<archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
index 97583730958..106093b5ad1 100644
--- a/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/label_filter/index.vue
@@ -55,7 +55,7 @@ export default {
},
i18n: I18N,
computed: {
- ...mapState(['useSidebarNavigation', 'searchLabelString', 'query', 'urlQuery', 'aggregations']),
+ ...mapState(['searchLabelString', 'query', 'urlQuery', 'aggregations']),
...mapGetters([
'filteredLabels',
'filteredUnselectedLabels',
@@ -179,14 +179,10 @@ export default {
<template>
<div class="gl-pb-0 gl-md-pt-0 label-filter gl-relative">
- <h5
- class="gl-my-0"
- data-testid="label-filter-title"
- :class="{ 'gl-font-sm': useSidebarNavigation }"
- >
+ <div class="gl-mb-2 gl-font-weight-bold gl-font-sm" data-testid="label-filter-title">
{{ $options.labelFilterData.header }}
- </h5>
- <div class="gl-my-5">
+ </div>
+ <div>
<gl-label
v-for="label in unappliedNewLabels"
:key="label.key"
@@ -246,12 +242,7 @@ export default {
v-if="isFocused"
v-outside="closeDropdown"
data-testid="header-search-dropdown-menu"
- class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-w-full gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2"
- :class="{
- 'gl-max-w-none!': useSidebarNavigation,
- 'gl-min-w-full!': useSidebarNavigation,
- 'gl-w-full!': useSidebarNavigation,
- }"
+ class="header-search-dropdown-menu gl-overflow-y-auto gl-absolute gl-bg-white gl-border-1 gl-rounded-base gl-border-solid gl-border-gray-200 gl-shadow-x0-y2-b4-s0 gl-mt-3 gl-z-index-2 gl-w-full! gl-min-w-full! gl-max-w-none!"
>
<div class="header-search-dropdown-content gl-py-2">
<dropdown-keyboard-navigation
diff --git a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
index 784207cc702..d0c895530cd 100644
--- a/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/language_filter/index.vue
@@ -27,7 +27,7 @@ export default {
loadError: s__('GlobalSearch|Aggregations load error.'),
},
computed: {
- ...mapState(['aggregations', 'useSidebarNavigation']),
+ ...mapState(['aggregations']),
...mapGetters(['languageAggregationBuckets']),
hasBuckets() {
return this.languageAggregationBuckets.length > 0;
@@ -75,9 +75,9 @@ export default {
<template>
<div v-if="hasBuckets" class="language-filter-checkbox">
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ <div class="gl-mb-2 gl-font-weight-bold gl-font-sm">
{{ $options.languageFilterData.header }}
- </h5>
+ </div>
<div
v-if="!aggregations.error"
class="gl-overflow-x-hidden gl-overflow-y-auto"
diff --git a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
index f86906ebd26..18074db7603 100644
--- a/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
+++ b/app/assets/javascripts/search/sidebar/components/merge_requests_filters.vue
@@ -1,7 +1,6 @@
<script>
// eslint-disable-next-line no-restricted-imports
-import { mapGetters, mapState } from 'vuex';
-import { HR_DEFAULT_CLASSES } from '../constants';
+import { mapGetters } from 'vuex';
import { statusFilterData } from './status_filter/data';
import StatusFilter from './status_filter/index.vue';
import FiltersTemplate from './filters_template.vue';
@@ -17,19 +16,12 @@ export default {
},
computed: {
...mapGetters(['currentScope']),
- ...mapState(['useSidebarNavigation', 'searchType']),
showArchivedFilter() {
return archivedFilterData.scopes.includes(this.currentScope);
},
showStatusFilter() {
return Object.values(statusFilterData.scopes).includes(this.currentScope);
},
- showDivider() {
- return !this.useSidebarNavigation;
- },
- hrClasses() {
- return [...HR_DEFAULT_CLASSES, 'gl-display-none', 'gl-md-display-block'];
- },
},
};
</script>
@@ -37,7 +29,6 @@ export default {
<template>
<filters-template>
<status-filter v-if="showStatusFilter" class="gl-mb-5" />
- <hr v-if="showArchivedFilter && showDivider" :class="hrClasses" />
<archived-filter v-if="showArchivedFilter" class="gl-mb-5" />
</filters-template>
</template>
diff --git a/app/assets/javascripts/search/sidebar/components/project_filter.vue b/app/assets/javascripts/search/sidebar/components/project_filter.vue
new file mode 100644
index 00000000000..76983644e60
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/project_filter.vue
@@ -0,0 +1,94 @@
+<script>
+import { isEmpty } from 'lodash';
+// eslint-disable-next-line no-restricted-imports
+import { mapState, mapActions, mapGetters } from 'vuex';
+import { s__ } from '~/locale';
+import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '~/search/sidebar/constants';
+import SearchableDropdown from './searchable_dropdown.vue';
+
+export default {
+ name: 'ProjectFilter',
+ i18n: {
+ projectFieldLabel: s__('GlobalSearch|Project'),
+ },
+ components: {
+ SearchableDropdown,
+ },
+ data() {
+ return {
+ search: '',
+ labelId: 'projects-filter-dropdown-id',
+ };
+ },
+ computed: {
+ ...mapState([
+ 'query',
+ 'projects',
+ 'fetchingProjects',
+ 'projectInitialJson',
+ 'useSidebarNavigation',
+ ]),
+ ...mapGetters(['frequentProjects', 'currentScope']),
+ selectedProject() {
+ return isEmpty(this.projectInitialJson) ? ANY_OPTION : this.projectInitialJson;
+ },
+ },
+ watch: {
+ search() {
+ this.debounceSearch();
+ },
+ },
+ created() {
+ // This tracks projects searched via the top nav search bar
+ if (this.query.nav_source === 'navbar' && this.projectInitialJson?.id) {
+ this.setFrequentProject(this.projectInitialJson);
+ }
+ },
+ methods: {
+ ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
+ firstLoad() {
+ this.loadFrequentProjects();
+ this.fetchProjects();
+ },
+ handleProjectChange(project) {
+ // If project.id is null we are clearing the filter and don't need to store that in LS.
+ if (project.id) {
+ this.setFrequentProject(project);
+ }
+
+ // This determines if we need to update the group filter or not
+ const queryParams = {
+ ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
+ [PROJECT_DATA.queryParam]: project.id,
+ nav_source: null,
+ scope: this.currentScope,
+ };
+
+ visitUrl(setUrlParams(queryParams));
+ },
+ },
+ PROJECT_DATA,
+};
+</script>
+
+<template>
+ <div>
+ <h5 :id="labelId" class="gl-mt-0 gl-mb-5 gl-font-sm">
+ {{ $options.i18n.projectFieldLabel }}
+ </h5>
+ <searchable-dropdown
+ data-testid="project-filter"
+ :header-text="$options.PROJECT_DATA.headerText"
+ :name="$options.PROJECT_DATA.name"
+ :loading="fetchingProjects"
+ :selected-item="selectedProject"
+ :items="projects"
+ :frequent-items="frequentProjects"
+ :search-handler="fetchProjects"
+ :label-id="labelId"
+ @first-open="firstLoad"
+ @change="handleProjectChange"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/radio_filter.vue b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
index a1eb5ccecd8..f961bfea608 100644
--- a/app/assets/javascripts/search/sidebar/components/radio_filter.vue
+++ b/app/assets/javascripts/search/sidebar/components/radio_filter.vue
@@ -17,7 +17,7 @@ export default {
},
},
computed: {
- ...mapState(['query', 'useSidebarNavigation']),
+ ...mapState(['query']),
...mapGetters(['currentScope']),
ANY() {
return this.filterData.filters.ANY;
@@ -57,9 +57,9 @@ export default {
<template>
<div>
- <h5 class="gl-mt-0 gl-mb-5" :class="{ 'gl-font-sm': useSidebarNavigation }">
+ <div class="gl-mb-2 gl-font-weight-bold gl-font-sm">
{{ filterData.header }}
- </h5>
+ </div>
<gl-form-radio-group v-model="selectedFilter">
<gl-form-radio v-for="f in filtersArray" :key="f.value" :value="f.value">
{{ radioLabel(f) }}
diff --git a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
deleted file mode 100644
index a4c1119736f..00000000000
--- a/app/assets/javascripts/search/sidebar/components/scope_legacy_navigation.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { GlNav, GlNavItem, GlIcon } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState } from 'vuex';
-import { s__ } from '~/locale';
-import Tracking from '~/tracking';
-import { formatSearchResultCount, addCountOverLimit } from '~/search/store/utils';
-import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants';
-import { slugifyWithUnderscore } from '../../../lib/utils/text_utility';
-
-export default {
- name: 'ScopeLegacyNavigation',
- i18n: {
- countOverLimitLabel: s__('GlobalSearch|Result count is over limit.'),
- },
- components: {
- GlNav,
- GlNavItem,
- GlIcon,
- },
- mixins: [Tracking.mixin()],
- computed: {
- ...mapState(['navigation', 'urlQuery']),
- },
- created() {
- if (this.urlQuery?.search) {
- this.fetchSidebarCount();
- }
- },
- methods: {
- ...mapActions(['fetchSidebarCount']),
- showFormatedCount(countString) {
- return formatSearchResultCount(countString);
- },
- isCountOverLimit(countString) {
- return Boolean(addCountOverLimit(countString));
- },
- handleClick(scope) {
- this.track('click_menu_item', { label: `vertical_navigation_${scope}` });
- },
- linkClasses(isHighlighted) {
- return [...this.$options.NAV_LINK_DEFAULT_CLASSES, { 'gl-font-weight-bold': isHighlighted }];
- },
- countClasses(isHighlighted) {
- return [
- ...this.$options.NAV_LINK_COUNT_DEFAULT_CLASSES,
- isHighlighted ? 'gl-text-gray-900' : 'gl-text-gray-500',
- ];
- },
- qaSelectorValue(item) {
- return `${slugifyWithUnderscore(item.label)}_tab`;
- },
- },
- NAV_LINK_DEFAULT_CLASSES,
- NAV_LINK_COUNT_DEFAULT_CLASSES,
-};
-</script>
-
-<template>
- <nav data-testid="search-filter" class="gl-border-none">
- <gl-nav vertical pills>
- <gl-nav-item
- v-for="(item, scope) in navigation"
- :key="scope"
- :link-classes="linkClasses(item.active)"
- class="gl-mb-1"
- :href="item.link"
- :active="item.active"
- :data-qa-selector="qaSelectorValue(item)"
- :data-testid="qaSelectorValue(item)"
- @click="handleClick(scope)"
- ><span data-testid="label">{{ item.label }}</span
- ><span v-if="item.count" data-testid="count" :class="countClasses(item.active)">
- {{ showFormatedCount(item.count)
- }}<gl-icon
- v-if="isCountOverLimit(item.count)"
- name="plus"
- :aria-label="$options.i18n.countOverLimitLabel"
- :size="8"
- />
- </span>
- </gl-nav-item>
- </gl-nav>
- </nav>
-</template>
diff --git a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
index f30618ad9b7..874803a720d 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
@@ -2,6 +2,7 @@
// eslint-disable-next-line no-restricted-imports
import { mapActions, mapState, mapGetters } from 'vuex';
import { s__ } from '~/locale';
+import eventHub from '~/super_sidebar/event_hub';
import NavItem from '~/super_sidebar/components/nav_item.vue';
import { NAV_LINK_DEFAULT_CLASSES, NAV_LINK_COUNT_DEFAULT_CLASSES } from '../constants';
@@ -18,6 +19,8 @@ export default {
...mapGetters(['navigationItems']),
},
created() {
+ eventHub.$emit('toggle-menu-header', false);
+
if (this.urlQuery?.search) {
this.fetchSidebarCount();
}
diff --git a/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
new file mode 100644
index 00000000000..c1f0bfc59f3
--- /dev/null
+++ b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
@@ -0,0 +1,222 @@
+<script>
+import { GlCollapsibleListbox, GlAvatar } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import SafeHtml from '~/vue_shared/directives/safe_html';
+import highlight from '~/lib/utils/highlight';
+import { truncateNamespace } from '~/lib/utils/text_utility';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { __, s__, n__ } from '~/locale';
+import { ANY_OPTION } from '../constants';
+
+export default {
+ name: 'SearchableDropdown',
+ components: {
+ GlAvatar,
+ GlCollapsibleListbox,
+ },
+ directives: {
+ SafeHtml,
+ },
+ i18n: {
+ frequentlySearched: __('Frequently searched'),
+ availableGroups: s__('GlobalSearch|All available groups'),
+ nothingFound: s__('GlobalSearch|Nothing found…'),
+ reset: s__('GlobalSearch|Reset'),
+ itemsFound(count) {
+ return n__('%d item found', '%d items found', count);
+ },
+ },
+ props: {
+ headerText: {
+ type: String,
+ required: false,
+ default: "__('Filter')",
+ },
+ name: {
+ type: String,
+ required: false,
+ default: 'name',
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ selectedItem: {
+ type: Object,
+ required: true,
+ },
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ frequentItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ searchHandler: {
+ type: Function,
+ required: true,
+ },
+ labelId: {
+ type: String,
+ required: false,
+ default: 'labelId',
+ },
+ },
+ data() {
+ return {
+ searchText: '',
+ hasBeenOpened: false,
+ showableItems: [],
+ searchInProgress: false,
+ };
+ },
+ watch: {
+ items() {
+ if (this.searchText === '') {
+ this.showableItems = this.defaultItems();
+ }
+ },
+ },
+ created() {
+ this.showableItems = this.defaultItems();
+ },
+ methods: {
+ defaultItems() {
+ const frequentItems = this.convertItemsFormat([...this.frequentItems]);
+ const nonFrequentItems = this.convertItemsFormat([
+ ...this.uniqueItems(this.items, this.frequentItems),
+ ]);
+
+ return [
+ {
+ text: '',
+ options: [
+ {
+ value: ANY_OPTION.name,
+ text: ANY_OPTION.name,
+ ...ANY_OPTION,
+ },
+ ],
+ },
+ {
+ text: this.$options.i18n.frequentlySearched,
+ options: frequentItems,
+ },
+ {
+ text: this.$options.i18n.availableGroups,
+ options: nonFrequentItems,
+ },
+ ].filter((group) => {
+ return group.options.length > 0;
+ });
+ },
+ search(search) {
+ this.searchText = search;
+ this.searchInProgress = true;
+
+ if (search !== '') {
+ debounce(() => {
+ this.searchHandler(this.searchText);
+ this.showableItems = this.convertItemsFormat([...this.items]);
+ }, DEFAULT_DEBOUNCE_AND_THROTTLE_MS)();
+
+ return;
+ }
+
+ this.showableItems = this.defaultItems();
+ },
+ openDropdown() {
+ if (!this.hasBeenOpened) {
+ this.hasBeenOpened = true;
+ this.$emit('first-open');
+ }
+ },
+ resetDropdown() {
+ this.$emit('change', ANY_OPTION);
+ },
+ convertItemsFormat(items) {
+ return items.map((item) => ({ value: item.id, text: item.full_name, ...item }));
+ },
+ truncatedNamespace(item) {
+ const itemDuplicat = { ...item };
+ const namespaceWithFallback = itemDuplicat.name_with_namespace
+ ? itemDuplicat.name_with_namespace
+ : itemDuplicat.full_name;
+
+ return truncateNamespace(namespaceWithFallback);
+ },
+ highlightedItemName(item) {
+ return highlight(item.name, item.searchText);
+ },
+ onSelectGroup(selected) {
+ if (selected === ANY_OPTION.name) {
+ this.$emit('change', ANY_OPTION);
+ return;
+ }
+
+ const flatShowableItems = [...this.frequentItems, ...this.items];
+ const newSelectedItem = flatShowableItems.find((item) => item.id === selected);
+ this.$emit('change', newSelectedItem);
+ },
+ uniqueItems(allItems, frequentItems) {
+ return allItems.filter((item) => {
+ const itemNotIdentical = frequentItems.some((fitem) => fitem.id === item.id);
+ return Boolean(!itemNotIdentical);
+ });
+ },
+ },
+ ANY_OPTION,
+ AVATAR_SHAPE_OPTION_RECT,
+};
+</script>
+
+<template>
+ <gl-collapsible-listbox
+ :items="showableItems"
+ :header-text="headerText"
+ :toggle-text="selectedItem[name]"
+ :no-results-text="$options.i18n.nothingFound"
+ :selected="selectedItem.id"
+ :searching="loading"
+ :reset-button-label="$options.i18n.reset"
+ :toggle-aria-labelled-by="labelId"
+ searchable
+ block
+ @shown="openDropdown"
+ @search="search"
+ @select="onSelectGroup"
+ @reset="resetDropdown"
+ >
+ <template #search-summary-sr-only>
+ {{ $options.i18n.itemsFound(showableItems.length) }}
+ </template>
+ <template #list-item="{ item }">
+ <div class="gl-display-flex gl-align-items-center">
+ <gl-avatar
+ :src="item.avatar_url"
+ :entity-id="item.id"
+ :entity-name="item.name"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="32"
+ class="gl-mr-3"
+ aria-hidden="true"
+ />
+ <div class="gl-display-flex gl-flex-direction-column">
+ <span
+ v-safe-html="highlightedItemName(item)"
+ class="gl-font-weight-bold gl-white-space-nowrap"
+ data-testid="item-title"
+ ></span>
+ <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">
+ {{ truncatedNamespace(item) }}</span
+ >
+ </div>
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+</template>
diff --git a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue b/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
deleted file mode 100644
index e966b8d877e..00000000000
--- a/app/assets/javascripts/search/sidebar/components/small_screen_drawer_navigation.vue
+++ /dev/null
@@ -1,61 +0,0 @@
-<script>
-import { GlDrawer } from '@gitlab/ui';
-import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
-import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
-import DomElementListener from '~/vue_shared/components/dom_element_listener.vue';
-import { s__ } from '~/locale';
-
-export default {
- name: 'SmallScreenDrawerNavigation',
- components: {
- GlDrawer,
- DomElementListener,
- },
- i18n: {
- smallScreenFiltersDrawerHeader: s__('GlobalSearch|Filters'),
- },
- data() {
- return {
- openSmallScreenFilters: false,
- };
- },
- computed: {
- getDrawerHeaderHeight() {
- if (!this.openSmallScreenFilters) return '0';
- return getContentWrapperHeight();
- },
- },
- methods: {
- closeSmallScreenFilters() {
- this.openSmallScreenFilters = false;
- },
- toggleSmallScreenFilters() {
- this.openSmallScreenFilters = !this.openSmallScreenFilters;
- },
- },
- DRAWER_Z_INDEX,
-};
-</script>
-<template>
- <dom-element-listener selector="#js-open-mobile-filters" @click="toggleSmallScreenFilters">
- <gl-drawer
- :header-height="getDrawerHeaderHeight"
- :z-index="$options.DRAWER_Z_INDEX"
- variant="sidebar"
- class="small-screen-drawer-navigation"
- :open="openSmallScreenFilters"
- @close="closeSmallScreenFilters"
- >
- <template #title>
- <h2 class="gl-my-0 gl-font-size-h2 gl-line-height-24">
- {{ $options.i18n.smallScreenFiltersDrawerHeader }}
- </h2>
- </template>
- <template #default>
- <div>
- <slot></slot>
- </div>
- </template>
- </gl-drawer>
- </dom-element-listener>
-</template>
diff --git a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
index a5f717dcf06..cbc1a26f1ae 100644
--- a/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
+++ b/app/assets/javascripts/search/sidebar/components/status_filter/index.vue
@@ -1,5 +1,4 @@
<script>
-import { HR_DEFAULT_CLASSES } from '../../constants';
import RadioFilter from '../radio_filter.vue';
import { statusFilterData } from './data';
@@ -9,7 +8,6 @@ export default {
RadioFilter,
},
statusFilterData,
- HR_DEFAULT_CLASSES,
};
</script>
diff --git a/app/assets/javascripts/search/sidebar/constants/index.js b/app/assets/javascripts/search/sidebar/constants/index.js
index 1559155a941..e3b0db670b5 100644
--- a/app/assets/javascripts/search/sidebar/constants/index.js
+++ b/app/assets/javascripts/search/sidebar/constants/index.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export const SCOPE_ISSUES = 'issues';
export const SCOPE_MERGE_REQUESTS = 'merge_requests';
export const SCOPE_BLOB = 'blobs';
@@ -18,8 +20,6 @@ export const NAV_LINK_DEFAULT_CLASSES = [
'gl-justify-content-space-between',
];
export const NAV_LINK_COUNT_DEFAULT_CLASSES = ['gl-font-sm', 'gl-font-weight-normal'];
-export const HR_DEFAULT_CLASSES = ['hr-x', 'gl-border-gray-100'];
-export const ONLY_SHOW_MD = ['gl-display-none', 'gl-md-display-block'];
export const TRACKING_ACTION_CLICK = 'search:filters:click';
export const TRACKING_LABEL_APPLY = 'Apply Filters';
@@ -28,3 +28,23 @@ export const TRACKING_LABEL_RESET = 'Reset Filters';
export const SEARCH_TYPE_BASIC = 'basic';
export const SEARCH_TYPE_ADVANCED = 'advanced';
export const SEARCH_TYPE_ZOEKT = 'zoekt';
+
+export const ANY_OPTION = {
+ id: null,
+ name: __('Any'),
+ name_with_namespace: __('Any'),
+};
+
+export const GROUP_DATA = {
+ headerText: __('Filter results by group'),
+ queryParam: 'group_id',
+ name: 'name',
+ fullName: 'full_name',
+};
+
+export const PROJECT_DATA = {
+ headerText: __('Filter results by project'),
+ queryParam: 'project_id',
+ name: 'name',
+ fullName: 'name_with_namespace',
+};
diff --git a/app/assets/javascripts/search/sidebar/index.js b/app/assets/javascripts/search/sidebar/index.js
index 3a699355dc9..9a7472ccad3 100644
--- a/app/assets/javascripts/search/sidebar/index.js
+++ b/app/assets/javascripts/search/sidebar/index.js
@@ -4,27 +4,23 @@ import GlobalSearchSidebar from './components/app.vue';
Vue.use(Translate);
-export const sidebarInitState = () => {
- const el = document.getElementById('js-search-sidebar');
- if (!el) return {};
-
- const { navigationJson, searchType } = el.dataset;
-
- const navigationJsonParsed = JSON.parse(navigationJson);
-
- return { navigationJsonParsed, searchType };
-};
-
export const initSidebar = (store) => {
const el = document.getElementById('js-search-sidebar');
+ const hederEl = document.getElementById('super-sidebar-context-header');
+ const headerText = hederEl.innerText;
if (!el) return false;
return new Vue({
el,
+ name: 'GlobalSearchSidebar',
store,
render(createElement) {
- return createElement(GlobalSearchSidebar);
+ return createElement(GlobalSearchSidebar, {
+ props: {
+ headerText,
+ },
+ });
},
});
};
diff --git a/app/assets/javascripts/search/store/constants.js b/app/assets/javascripts/search/store/constants.js
index ad47cd975f8..1d6720b5936 100644
--- a/app/assets/javascripts/search/store/constants.js
+++ b/app/assets/javascripts/search/store/constants.js
@@ -39,3 +39,7 @@ export const ICON_MAP = {
export const ZOEKT_SEARCH_TYPE = 'zoekt';
export const ADVANCED_SEARCH_TYPE = 'advanced';
export const BASIC_SEARCH_TYPE = 'basic';
+
+export const SEARCH_LEVEL_GLOBAL = 'global';
+export const SEARCH_LEVEL_PROJECT = 'project';
+export const SEARCH_LEVEL_GROUP = 'group';
diff --git a/app/assets/javascripts/search/store/mutations.js b/app/assets/javascripts/search/store/mutations.js
index b248681f053..7627b2e0e08 100644
--- a/app/assets/javascripts/search/store/mutations.js
+++ b/app/assets/javascripts/search/store/mutations.js
@@ -6,7 +6,7 @@ export default {
},
[types.RECEIVE_GROUPS_SUCCESS](state, data) {
state.fetchingGroups = false;
- state.groups = data;
+ state.groups = [...data];
},
[types.RECEIVE_GROUPS_ERROR](state) {
state.fetchingGroups = false;
@@ -17,7 +17,7 @@ export default {
},
[types.RECEIVE_PROJECTS_SUCCESS](state, data) {
state.fetchingProjects = false;
- state.projects = data;
+ state.projects = [...data];
},
[types.RECEIVE_PROJECTS_ERROR](state) {
state.fetchingProjects = false;
diff --git a/app/assets/javascripts/search/store/state.js b/app/assets/javascripts/search/store/state.js
index b4cd2af65ba..279ba467bba 100644
--- a/app/assets/javascripts/search/store/state.js
+++ b/app/assets/javascripts/search/store/state.js
@@ -1,7 +1,15 @@
import { cloneDeep } from 'lodash';
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from './constants';
-const createState = ({ query, navigation, useSidebarNavigation, searchType }) => ({
+const createState = ({
+ query,
+ navigation,
+ searchType,
+ searchLevel,
+ groupInitialJson,
+ projectInitialJson,
+ defaultBranchName,
+}) => ({
urlQuery: cloneDeep(query),
query,
groups: [],
@@ -14,7 +22,6 @@ const createState = ({ query, navigation, useSidebarNavigation, searchType }) =>
},
sidebarDirty: false,
navigation,
- useSidebarNavigation,
aggregations: {
error: false,
fetching: false,
@@ -22,6 +29,10 @@ const createState = ({ query, navigation, useSidebarNavigation, searchType }) =>
},
searchLabelString: '',
searchType,
+ searchLevel,
+ groupInitialJson,
+ projectInitialJson,
+ defaultBranchName,
});
export default createState;
diff --git a/app/assets/javascripts/search/topbar/components/app.vue b/app/assets/javascripts/search/topbar/components/app.vue
index 49e66492519..da189fe9f06 100644
--- a/app/assets/javascripts/search/topbar/components/app.vue
+++ b/app/assets/javascripts/search/topbar/components/app.vue
@@ -1,56 +1,30 @@
<script>
-import { GlSearchBoxByClick, GlButton } from '@gitlab/ui';
+import { GlSearchBoxByType, GlButton } from '@gitlab/ui';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapActions } from 'vuex';
import { s__ } from '~/locale';
-import { parseBoolean } from '~/lib/utils/common_utils';
import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
import { ZOEKT_SEARCH_TYPE, ADVANCED_SEARCH_TYPE } from '~/search/store/constants';
import { SYNTAX_OPTIONS_ADVANCED_DOCUMENT, SYNTAX_OPTIONS_ZOEKT_DOCUMENT } from '../constants';
-import GroupFilter from './group_filter.vue';
-import ProjectFilter from './project_filter.vue';
+import SearchTypeIndicator from './search_type_indicator.vue';
export default {
name: 'GlobalSearchTopbar',
i18n: {
searchPlaceholder: s__(`GlobalSearch|Search for projects, issues, etc.`),
searchLabel: s__(`GlobalSearch|What are you searching for?`),
- documentFetchErrorMessage: s__(
- 'GlobalSearch|There was an error fetching the "Syntax Options" document.',
- ),
- searchFieldLabel: s__('GlobalSearch|What are you searching for?'),
syntaxOptionsLabel: s__('GlobalSearch|Syntax options'),
groupFieldLabel: s__('GlobalSearch|Group'),
projectFieldLabel: s__('GlobalSearch|Project'),
- searchButtonLabel: s__('GlobalSearch|Search'),
- closeButtonLabel: s__('GlobalSearch|Close'),
},
components: {
GlButton,
- GlSearchBoxByClick,
- GroupFilter,
- ProjectFilter,
+ GlSearchBoxByType,
MarkdownDrawer,
- },
- props: {
- groupInitialJson: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- projectInitialJson: {
- type: Object,
- required: false,
- default: () => ({}),
- },
- defaultBranchName: {
- type: String,
- required: false,
- default: '',
- },
+ SearchTypeIndicator,
},
computed: {
- ...mapState(['query', 'searchType']),
+ ...mapState(['query', 'searchType', 'defaultBranchName']),
search: {
get() {
return this.query ? this.query.search : '';
@@ -59,9 +33,6 @@ export default {
this.setQuery({ key: 'search', value });
},
},
- showFilters() {
- return !parseBoolean(this.query.snippets);
- },
showSyntaxOptions() {
return (
(this.searchType === ZOEKT_SEARCH_TYPE || this.searchType === ADVANCED_SEARCH_TYPE) &&
@@ -90,46 +61,40 @@ export default {
</script>
<template>
- <section class="gl-p-5 gl-bg-gray-10 gl-border-b gl-border-t">
+ <section>
+ <div
+ class="gl-lg-display-flex gl-flex-direction-row gl-py-5"
+ :class="{
+ 'gl-justify-content-space-between': showSyntaxOptions,
+ 'gl-justify-content-end': !showSyntaxOptions,
+ }"
+ >
+ <template v-if="showSyntaxOptions">
+ <div>
+ <gl-button
+ category="tertiary"
+ variant="link"
+ size="small"
+ button-text-classes="gl-font-sm!"
+ @click="onToggleDrawer"
+ >{{ $options.i18n.syntaxOptionsLabel }}
+ </gl-button>
+ </div>
+ <markdown-drawer ref="markdownDrawer" :document-path="documentBasedOnSearchType" />
+ </template>
+ <search-type-indicator />
+ </div>
<div class="search-page-form gl-lg-display-flex gl-flex-direction-column">
- <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-end">
- <div class="gl-flex-grow-1 gl-mb-4 gl-lg-mb-0 gl-lg-mr-2">
- <div
- class="gl-display-flex gl-flex-direction-row gl-justify-content-space-between gl-mb-0 gl-md-mb-4"
- >
- <label class="gl-mb-1 gl-md-pb-2">{{ $options.i18n.searchLabel }}</label>
- <template v-if="showSyntaxOptions">
- <gl-button
- category="tertiary"
- variant="link"
- size="small"
- button-text-classes="gl-font-sm!"
- @click="onToggleDrawer"
- >{{ $options.i18n.syntaxOptionsLabel }}
- </gl-button>
- <markdown-drawer ref="markdownDrawer" :document-path="documentBasedOnSearchType" />
- </template>
- </div>
- <gl-search-box-by-click
+ <div class="gl-lg-display-flex gl-flex-direction-row gl-align-items-flex-start">
+ <div class="gl-flex-grow-1 gl-pb-8 gl-lg-mb-0 gl-lg-mr-2">
+ <gl-search-box-by-type
id="dashboard_search"
v-model="search"
name="search"
:placeholder="$options.i18n.searchPlaceholder"
- @submit="applyQuery"
+ @keydown.enter.stop.prevent="applyQuery"
/>
</div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-mx-3">
- <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{
- $options.i18n.groupFieldLabel
- }}</label>
- <group-filter :initial-data="groupInitialJson" />
- </div>
- <div v-if="showFilters" class="gl-mb-4 gl-lg-mb-0 gl-lg-ml-3">
- <label class="gl-display-block gl-mb-1 gl-md-pb-2">{{
- $options.i18n.projectFieldLabel
- }}</label>
- <project-filter :initial-data="projectInitialJson" />
- </div>
</div>
</div>
</section>
diff --git a/app/assets/javascripts/search/topbar/components/project_filter.vue b/app/assets/javascripts/search/topbar/components/project_filter.vue
deleted file mode 100644
index c8190b4002d..00000000000
--- a/app/assets/javascripts/search/topbar/components/project_filter.vue
+++ /dev/null
@@ -1,70 +0,0 @@
-<script>
-// eslint-disable-next-line no-restricted-imports
-import { mapState, mapActions, mapGetters } from 'vuex';
-import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
-import { ANY_OPTION, GROUP_DATA, PROJECT_DATA } from '../constants';
-import SearchableDropdown from './searchable_dropdown.vue';
-
-export default {
- name: 'ProjectFilter',
- components: {
- SearchableDropdown,
- },
- props: {
- initialData: {
- type: Object,
- required: false,
- default: () => null,
- },
- },
- computed: {
- ...mapState(['query', 'projects', 'fetchingProjects']),
- ...mapGetters(['frequentProjects', 'currentScope']),
- selectedProject() {
- return this.initialData ? this.initialData : ANY_OPTION;
- },
- },
- created() {
- // This tracks projects searched via the top nav search bar
- if (this.query.nav_source === 'navbar' && this.initialData?.id) {
- this.setFrequentProject(this.initialData);
- }
- },
- methods: {
- ...mapActions(['fetchProjects', 'setFrequentProject', 'loadFrequentProjects']),
- handleProjectChange(project) {
- // If project.id is null we are clearing the filter and don't need to store that in LS.
- if (project.id) {
- this.setFrequentProject(project);
- }
-
- // This determines if we need to update the group filter or not
- const queryParams = {
- ...(project.namespace?.id && { [GROUP_DATA.queryParam]: project.namespace.id }),
- [PROJECT_DATA.queryParam]: project.id,
- nav_source: null,
- scope: this.currentScope,
- };
-
- visitUrl(setUrlParams(queryParams));
- },
- },
- PROJECT_DATA,
-};
-</script>
-
-<template>
- <searchable-dropdown
- data-testid="project-filter"
- :header-text="$options.PROJECT_DATA.headerText"
- :name="$options.PROJECT_DATA.name"
- :full-name="$options.PROJECT_DATA.fullName"
- :loading="fetchingProjects"
- :selected-item="selectedProject"
- :items="projects"
- :frequent-items="frequentProjects"
- @first-open="loadFrequentProjects"
- @search="fetchProjects"
- @change="handleProjectChange"
- />
-</template>
diff --git a/app/assets/javascripts/search/topbar/components/search_type_indicator.vue b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
new file mode 100644
index 00000000000..362139bf64d
--- /dev/null
+++ b/app/assets/javascripts/search/topbar/components/search_type_indicator.vue
@@ -0,0 +1,120 @@
+<script>
+// eslint-disable-next-line no-restricted-imports
+import { mapState } from 'vuex';
+import { GlSprintf, GlLink, GlTooltipDirective } from '@gitlab/ui';
+import { s__, __ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import {
+ ZOEKT_SEARCH_TYPE,
+ ADVANCED_SEARCH_TYPE,
+ BASIC_SEARCH_TYPE,
+ SEARCH_LEVEL_PROJECT,
+} from '~/search/store/constants';
+import {
+ ZOEKT_HELP_PAGE,
+ ADVANCED_SEARCH_HELP_PAGE,
+ ADVANCED_SEARCH_SYNTAX_HELP_ANCHOR,
+ ZOEKT_HELP_PAGE_SYNTAX_ANCHOR,
+} from '../constants';
+
+export default {
+ name: 'SearchTypeIndicator',
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ i18n: {
+ zoekt_enabled: s__(
+ 'GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is enabled',
+ ),
+ zoekt_disabled: s__(
+ 'GlobalSearch|%{linkStart}Exact code search (powered by Zoekt)%{linkEnd} is disabled since %{ref_elem} is not the default branch. %{docs_link}',
+ ),
+ advanced_enabled: __('%{linkStart}Advanced search%{linkEnd} is enabled.'),
+ advanced_disabled: __(
+ '%{linkStart}Advanced search%{linkEnd} is disabled since %{ref_elem} is not the default branch. %{docs_link}',
+ ),
+ more: __('Learn more.'),
+ },
+ components: {
+ GlSprintf,
+ GlLink,
+ },
+ computed: {
+ ...mapState(['searchType', 'defaultBranchName', 'query', 'searchLevel']),
+ zoektHelpUrl() {
+ return helpPagePath(ZOEKT_HELP_PAGE);
+ },
+ zoektSyntaxHelpUrl() {
+ return helpPagePath(ZOEKT_HELP_PAGE, {
+ anchor: ZOEKT_HELP_PAGE_SYNTAX_ANCHOR,
+ });
+ },
+ advancedSearchHelpUrl() {
+ return helpPagePath(ADVANCED_SEARCH_HELP_PAGE);
+ },
+ advancedSearchSyntaxHelpUrl() {
+ return helpPagePath(ADVANCED_SEARCH_HELP_PAGE, {
+ anchor: ADVANCED_SEARCH_SYNTAX_HELP_ANCHOR,
+ });
+ },
+ isZoekt() {
+ return this.searchType === ZOEKT_SEARCH_TYPE;
+ },
+ isAdvancedSearch() {
+ return this.searchType === ADVANCED_SEARCH_TYPE;
+ },
+ isEnabled() {
+ if (this.searchLevel !== SEARCH_LEVEL_PROJECT) {
+ return true;
+ }
+
+ return !this.query.repository_ref || this.query.repository_ref === this.defaultBranchName;
+ },
+ isBasicSearch() {
+ return this.searchType === BASIC_SEARCH_TYPE;
+ },
+ disabledMessage() {
+ return this.isZoekt
+ ? this.$options.i18n.zoekt_disabled
+ : this.$options.i18n.advanced_disabled;
+ },
+ helpUrl() {
+ return this.isZoekt ? this.zoektHelpUrl : this.advancedSearchHelpUrl;
+ },
+ enabledMessage() {
+ return this.isZoekt ? this.$options.i18n.zoekt_enabled : this.$options.i18n.advanced_enabled;
+ },
+ syntaxHelpUrl() {
+ return this.isZoekt ? this.zoektSyntaxHelpUrl : this.advancedSearchSyntaxHelpUrl;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-text-gray-600">
+ <div v-if="isBasicSearch" data-testid="basic">&nbsp;</div>
+ <div v-else-if="isEnabled" :data-testid="`${searchType}-enabled`">
+ <gl-sprintf :message="enabledMessage">
+ <template #link="{ content }">
+ <gl-link :href="helpUrl" target="_blank" data-testid="docs-link">{{ content }} </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ <div v-else :data-testid="`${searchType}-disabled`">
+ <gl-sprintf :message="disabledMessage">
+ <template #link="{ content }">
+ <gl-link :href="helpUrl" target="_blank" data-testid="docs-link">{{ content }} </gl-link>
+ </template>
+ <template #ref_elem>
+ <code v-gl-tooltip :title="query.repository_ref">{{ query.repository_ref }}</code>
+ </template>
+ <template #docs_link>
+ <gl-link :href="syntaxHelpUrl" target="_blank" data-testid="syntax-docs-link"
+ >{{ $options.i18n.more }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
deleted file mode 100644
index ff639d538b3..00000000000
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown.vue
+++ /dev/null
@@ -1,195 +0,0 @@
-<script>
-import {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlIcon,
- GlButton,
- GlSkeletonLoader,
- GlTooltipDirective,
-} from '@gitlab/ui';
-import { __ } from '~/locale';
-import { ANY_OPTION } from '../constants';
-import SearchableDropdownItem from './searchable_dropdown_item.vue';
-
-export default {
- i18n: {
- clearLabel: __('Clear'),
- frequentlySearched: __('Frequently searched'),
- },
- name: 'SearchableDropdown',
- components: {
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlLoadingIcon,
- GlIcon,
- GlButton,
- GlSkeletonLoader,
- SearchableDropdownItem,
- },
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- props: {
- headerText: {
- type: String,
- required: false,
- default: "__('Filter')",
- },
- name: {
- type: String,
- required: false,
- default: 'name',
- },
- fullName: {
- type: String,
- required: false,
- default: 'name',
- },
- loading: {
- type: Boolean,
- required: false,
- default: false,
- },
- selectedItem: {
- type: Object,
- required: true,
- },
- items: {
- type: Array,
- required: false,
- default: () => [],
- },
- frequentItems: {
- type: Array,
- required: false,
- default: () => [],
- },
- },
- data() {
- return {
- searchText: '',
- hasBeenOpened: false,
- };
- },
- computed: {
- showFrequentItems() {
- return !this.searchText && this.frequentItems.length > 0;
- },
- },
- methods: {
- isSelected(selected) {
- return selected.id === this.selectedItem.id;
- },
- openDropdown() {
- if (!this.hasBeenOpened) {
- this.hasBeenOpened = true;
- this.$emit('first-open');
- }
-
- this.$emit('search', this.searchText);
- },
- resetDropdown() {
- this.$emit('change', ANY_OPTION);
- },
- updateDropdown(item) {
- this.$emit('change', item);
- },
- },
- ANY_OPTION,
-};
-</script>
-
-<template>
- <gl-dropdown
- class="gl-w-full"
- menu-class="global-search-dropdown-menu"
- toggle-class="gl-text-truncate"
- :header-text="headerText"
- :right="true"
- @show="openDropdown"
- @shown="$refs.searchBox.focusInput()"
- >
- <template #button-content>
- <span class="dropdown-toggle-text gl-flex-grow-1 gl-text-truncate">
- {{ selectedItem[name] }}
- </span>
- <gl-loading-icon v-if="loading" size="sm" inline class="gl-mr-3" />
- <gl-button
- v-if="!isSelected($options.ANY_OPTION)"
- v-gl-tooltip
- name="clear"
- category="tertiary"
- :title="$options.i18n.clearLabel"
- :aria-label="$options.i18n.clearLabel"
- class="gl-p-0! gl-mr-2"
- @keydown.enter.stop="resetDropdown"
- @click.stop="resetDropdown"
- >
- <gl-icon name="clear" />
- </gl-button>
- <gl-icon name="chevron-down" />
- </template>
- <div class="gl-sticky gl-top-0 gl-z-index-1 gl-bg-white">
- <gl-search-box-by-type
- ref="searchBox"
- v-model="searchText"
- class="gl-m-3"
- :debounce="500"
- @input="openDropdown"
- />
- <gl-dropdown-item
- class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2! gl-mb-2"
- is-check-item
- :is-checked="isSelected($options.ANY_OPTION)"
- is-check-centered
- @click="updateDropdown($options.ANY_OPTION)"
- >
- <span data-testid="item-title">{{ $options.ANY_OPTION.name }}</span>
- </gl-dropdown-item>
- </div>
- <div
- v-if="showFrequentItems"
- class="gl-border-b-solid gl-border-b-gray-100 gl-border-b-1 gl-pb-2 gl-mb-2"
- >
- <gl-dropdown-section-header>{{
- $options.i18n.frequentlySearched
- }}</gl-dropdown-section-header>
- <searchable-dropdown-item
- v-for="item in frequentItems"
- :key="item.id"
- :item="item"
- :selected-item="selectedItem"
- :search-text="searchText"
- :name="name"
- :full-name="fullName"
- data-testid="frequent-items"
- @change="updateDropdown"
- />
- </div>
- <div v-if="!loading">
- <searchable-dropdown-item
- v-for="item in items"
- :key="item.id"
- :item="item"
- :selected-item="selectedItem"
- :search-text="searchText"
- :name="name"
- :full-name="fullName"
- data-testid="searchable-items"
- @change="updateDropdown"
- />
- </div>
- <div v-if="loading" class="gl-mx-4 gl-mt-3">
- <gl-skeleton-loader :height="100">
- <rect y="0" width="90%" height="20" rx="4" />
- <rect y="40" width="70%" height="20" rx="4" />
- <rect y="80" width="80%" height="20" rx="4" />
- </gl-skeleton-loader>
- </div>
- </gl-dropdown>
-</template>
diff --git a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue b/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
deleted file mode 100644
index c1e33df3c42..00000000000
--- a/app/assets/javascripts/search/topbar/components/searchable_dropdown_item.vue
+++ /dev/null
@@ -1,78 +0,0 @@
-<script>
-import { GlDropdownItem, GlAvatar } from '@gitlab/ui';
-import SafeHtml from '~/vue_shared/directives/safe_html';
-import highlight from '~/lib/utils/highlight';
-import { truncateNamespace } from '~/lib/utils/text_utility';
-import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
-
-export default {
- name: 'SearchableDropdownItem',
- components: {
- GlDropdownItem,
- GlAvatar,
- },
- directives: {
- SafeHtml,
- },
- props: {
- item: {
- type: Object,
- required: true,
- },
- selectedItem: {
- type: Object,
- required: true,
- },
- searchText: {
- type: String,
- required: false,
- default: '',
- },
- name: {
- type: String,
- required: true,
- },
- fullName: {
- type: String,
- required: true,
- },
- },
- computed: {
- isSelected() {
- return this.item.id === this.selectedItem.id;
- },
- truncatedNamespace() {
- return truncateNamespace(this.item[this.fullName]);
- },
- highlightedItemName() {
- return highlight(this.item[this.name], this.searchText);
- },
- },
- AVATAR_SHAPE_OPTION_RECT,
-};
-</script>
-
-<template>
- <gl-dropdown-item
- is-check-item
- :is-checked="isSelected"
- is-check-centered
- @click="$emit('change', item)"
- >
- <div class="gl-display-flex gl-align-items-center">
- <gl-avatar
- :src="item.avatar_url"
- :entity-id="item.id"
- :entity-name="item[name]"
- :shape="$options.AVATAR_SHAPE_OPTION_RECT"
- :size="32"
- />
- <div class="gl-display-flex gl-flex-direction-column">
- <span v-safe-html="highlightedItemName" data-testid="item-title"></span>
- <span class="gl-font-sm gl-text-gray-700" data-testid="item-namespace">{{
- truncatedNamespace
- }}</span>
- </div>
- </div>
- </gl-dropdown-item>
-</template>
diff --git a/app/assets/javascripts/search/topbar/constants.js b/app/assets/javascripts/search/topbar/constants.js
index 1ad40fbe3db..2bd0a4d2c66 100644
--- a/app/assets/javascripts/search/topbar/constants.js
+++ b/app/assets/javascripts/search/topbar/constants.js
@@ -1,24 +1,8 @@
-import { __ } from '~/locale';
-
-export const ANY_OPTION = Object.freeze({
- id: null,
- name: __('Any'),
- name_with_namespace: __('Any'),
-});
-
-export const GROUP_DATA = {
- headerText: __('Filter results by group'),
- queryParam: 'group_id',
- name: 'name',
- fullName: 'full_name',
-};
-
-export const PROJECT_DATA = {
- headerText: __('Filter results by project'),
- queryParam: 'project_id',
- name: 'name',
- fullName: 'name_with_namespace',
-};
-
export const SYNTAX_OPTIONS_ADVANCED_DOCUMENT = 'drawers/drawers/advanced_search_syntax.md';
export const SYNTAX_OPTIONS_ZOEKT_DOCUMENT = 'drawers/drawers/exact_code_search_syntax.md';
+
+export const ZOEKT_HELP_PAGE = 'user/search/exact_code_search';
+export const ADVANCED_SEARCH_HELP_PAGE = 'user/search/advanced_search';
+
+export const ADVANCED_SEARCH_SYNTAX_HELP_ANCHOR = 'use-the-advanced-search-syntax';
+export const ZOEKT_HELP_PAGE_SYNTAX_ANCHOR = 'syntax';
diff --git a/app/assets/javascripts/search/topbar/index.js b/app/assets/javascripts/search/topbar/index.js
index aad7445ebdc..b88dc8ed516 100644
--- a/app/assets/javascripts/search/topbar/index.js
+++ b/app/assets/javascripts/search/topbar/index.js
@@ -11,22 +11,12 @@ export const initTopbar = (store) => {
return false;
}
- const { groupInitialJson, projectInitialJson, defaultBranchName } = el.dataset;
-
- const groupInitialJsonParsed = JSON.parse(groupInitialJson);
- const projectInitialJsonParsed = JSON.parse(projectInitialJson);
-
return new Vue({
el,
+ name: 'GlobalSearchTopbar',
store,
render(createElement) {
- return createElement(GlobalSearchTopbar, {
- props: {
- groupInitialJson: groupInitialJsonParsed,
- projectInitialJson: projectInitialJsonParsed,
- defaultBranchName,
- },
- });
+ return createElement(GlobalSearchTopbar);
},
});
};
diff --git a/app/assets/javascripts/search/under_topbar/index.js b/app/assets/javascripts/search/under_topbar/index.js
index 8e50c6655dd..0be803d68fd 100644
--- a/app/assets/javascripts/search/under_topbar/index.js
+++ b/app/assets/javascripts/search/under_topbar/index.js
@@ -14,6 +14,7 @@ export const initBlobRefSwitcher = () => {
return new Vue({
el,
+ name: 'GlobalSearchUnderTopbar',
render(createElement) {
return createElement(RefSelector, {
props: {
diff --git a/app/assets/javascripts/security_configuration/components/app.vue b/app/assets/javascripts/security_configuration/components/app.vue
index 32d46a0d4af..4a4c91c6ba7 100644
--- a/app/assets/javascripts/security_configuration/components/app.vue
+++ b/app/assets/javascripts/security_configuration/components/app.vue
@@ -1,43 +1,21 @@
<script>
import { GlTab, GlTabs, GlSprintf, GlLink, GlAlert } from '@gitlab/ui';
import Api from '~/api';
-import { __, s__ } from '~/locale';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
import SectionLayout from '~/vue_shared/security_configuration/components/section_layout.vue';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT } from '~/tracking/constants';
-import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
-import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import {
AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY,
TAB_VULNERABILITY_MANAGEMENT_INDEX,
-} from './constants';
+ i18n,
+} from '../constants';
+import AutoDevOpsAlert from './auto_dev_ops_alert.vue';
+import AutoDevOpsEnabledAlert from './auto_dev_ops_enabled_alert.vue';
import FeatureCard from './feature_card.vue';
import TrainingProviderList from './training_provider_list.vue';
-export const i18n = {
- configurationHistory: s__('SecurityConfiguration|Configuration history'),
- securityTesting: s__('SecurityConfiguration|Security testing'),
- latestPipelineDescription: s__(
- `SecurityConfiguration|The status of the tools only applies to the
- default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.`,
- ),
- description: s__(
- `SecurityConfiguration|Once you've enabled a scan for the default branch,
- any subsequent feature branch you create will include the scan. An enabled
- scanner will not be reflected as such until the pipeline has been
- successfully executed and it has generated valid artifacts.`,
- ),
- 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.',
- ),
- securityTrainingDoc: s__('SecurityConfiguration|Learn more about vulnerability training'),
-};
-
export default {
i18n,
components: {
diff --git a/app/assets/javascripts/security_configuration/components/constants.js b/app/assets/javascripts/security_configuration/components/constants.js
deleted file mode 100644
index da213b0ed43..00000000000
--- a/app/assets/javascripts/security_configuration/components/constants.js
+++ /dev/null
@@ -1,332 +0,0 @@
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { __, s__ } from '~/locale';
-import ContinuousVulnerabilityScan from '~/security_configuration/components/continuous_vulnerability_scan.vue';
-
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SAST_IAC,
- REPORT_TYPE_DAST,
- REPORT_TYPE_DAST_PROFILES,
- REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
- REPORT_TYPE_SECRET_DETECTION,
- REPORT_TYPE_DEPENDENCY_SCANNING,
- REPORT_TYPE_CONTAINER_SCANNING,
- REPORT_TYPE_COVERAGE_FUZZING,
- REPORT_TYPE_CORPUS_MANAGEMENT,
- REPORT_TYPE_API_FUZZING,
-} from '~/vue_shared/security_reports/constants';
-
-import kontraLogo from 'images/vulnerability/kontra-logo.svg?raw';
-import scwLogo from 'images/vulnerability/scw-logo.svg?raw';
-import secureflagLogo from 'images/vulnerability/secureflag-logo.svg?raw';
-import configureSastMutation from '../graphql/configure_sast.mutation.graphql';
-import configureSastIacMutation from '../graphql/configure_iac.mutation.graphql';
-import configureSecretDetectionMutation from '../graphql/configure_secret_detection.mutation.graphql';
-
-/**
- * Translations & helpPagePaths for Security Configuration Page
- * Make sure to add new scanner translations to the SCANNER_NAMES_MAP below.
- */
-
-export const SAST_NAME = __('Static Application Security Testing (SAST)');
-export const SAST_SHORT_NAME = s__('ciReport|SAST');
-export const SAST_DESCRIPTION = __('Analyze your source code for known vulnerabilities.');
-export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
-export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', {
- anchor: 'configuration',
-});
-
-export const SAST_IAC_NAME = __('Infrastructure as Code (IaC) Scanning');
-export const SAST_IAC_SHORT_NAME = s__('ciReport|SAST IaC');
-export const SAST_IAC_DESCRIPTION = __(
- 'Analyze your infrastructure as code configuration files for known vulnerabilities.',
-);
-export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index');
-export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
- 'user/application_security/iac_scanning/index',
- {
- anchor: 'configuration',
- },
-);
-
-export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
-export const DAST_SHORT_NAME = s__('ciReport|DAST');
-export const DAST_DESCRIPTION = s__(
- 'ciReport|Analyze a deployed version of your web application for known vulnerabilities by examining it from the outside in. DAST works by simulating external attacks on your application while it is running.',
-);
-export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
-export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/dast/index', {
- anchor: 'enable-automatic-dast-run',
-});
-export const DAST_BADGE_TEXT = __('Available on demand');
-export const DAST_BADGE_TOOLTIP = __(
- 'On-demand scans run outside of the DevOps cycle and find vulnerabilities in your projects',
-);
-
-export const DAST_PROFILES_NAME = __('DAST profiles');
-export const DAST_PROFILES_DESCRIPTION = s__(
- 'SecurityConfiguration|Manage profiles for use by DAST scans.',
-);
-export const DAST_PROFILES_CONFIG_TEXT = s__('SecurityConfiguration|Manage profiles');
-
-export const BAS_BADGE_TEXT = s__('SecurityConfiguration|Incubating feature');
-export const BAS_BADGE_TOOLTIP = s__(
- 'SecurityConfiguration|Breach and Attack Simulation is an incubating feature extending existing security testing by simulating adversary activity.',
-);
-export const BAS_DESCRIPTION = s__(
- 'SecurityConfiguration|Simulate breach and attack scenarios against your running application by attempting to detect and exploit known vulnerabilities.',
-);
-export const BAS_HELP_PATH = helpPagePath(
- 'user/application_security/breach_and_attack_simulation/index',
-);
-export const BAS_NAME = s__('SecurityConfiguration|Breach and Attack Simulation (BAS)');
-export const BAS_SHORT_NAME = s__('SecurityConfiguration|BAS');
-
-export const BAS_DAST_FEATURE_FLAG_DESCRIPTION = s__(
- 'SecurityConfiguration|Enable incubating Breach and Attack Simulation focused features such as callback attacks in your DAST scans.',
-);
-export const BAS_DAST_FEATURE_FLAG_HELP_PATH = helpPagePath(
- 'user/application_security/breach_and_attack_simulation/index',
- { anchor: 'extend-dynamic-application-security-testing-dast' },
-);
-export const BAS_DAST_FEATURE_FLAG_NAME = s__(
- 'SecurityConfiguration|Out-of-Band Application Security Testing (OAST)',
-);
-
-export const SECRET_DETECTION_NAME = __('Secret Detection');
-export const SECRET_DETECTION_DESCRIPTION = __(
- 'Analyze your source code and git history for secrets.',
-);
-export const SECRET_DETECTION_HELP_PATH = helpPagePath(
- 'user/application_security/secret_detection/index',
-);
-export const SECRET_DETECTION_CONFIG_HELP_PATH = helpPagePath(
- 'user/application_security/secret_detection/index',
- { anchor: 'configuration' },
-);
-
-export const DEPENDENCY_SCANNING_NAME = __('Dependency Scanning');
-export const DEPENDENCY_SCANNING_DESCRIPTION = __(
- 'Analyze your dependencies for known vulnerabilities.',
-);
-export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
- 'user/application_security/dependency_scanning/index',
-);
-export const DEPENDENCY_SCANNING_CONFIG_HELP_PATH = helpPagePath(
- 'user/application_security/dependency_scanning/index',
- { anchor: 'configuration' },
-);
-
-export const CONTAINER_SCANNING_NAME = __('Container Scanning');
-export const CONTAINER_SCANNING_DESCRIPTION = __(
- 'Check your Docker images for known vulnerabilities.',
-);
-export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
- 'user/application_security/container_scanning/index',
-);
-export const CONTAINER_SCANNING_CONFIG_HELP_PATH = helpPagePath(
- 'user/application_security/container_scanning/index',
- { anchor: 'configuration' },
-);
-
-export const COVERAGE_FUZZING_NAME = __('Coverage Fuzzing');
-export const COVERAGE_FUZZING_DESCRIPTION = __(
- 'Find bugs in your code with coverage-guided fuzzing.',
-);
-export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
- 'user/application_security/coverage_fuzzing/index',
-);
-export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath(
- 'user/application_security/coverage_fuzzing/index',
- { anchor: 'enable-coverage-guided-fuzz-testing' },
-);
-
-export const CORPUS_MANAGEMENT_NAME = __('Corpus Management');
-export const CORPUS_MANAGEMENT_DESCRIPTION = s__(
- 'SecurityConfiguration|Manage corpus files used as seed inputs with coverage-guided fuzzing.',
-);
-export const CORPUS_MANAGEMENT_CONFIG_TEXT = s__('SecurityConfiguration|Manage corpus');
-
-export const API_FUZZING_NAME = __('API Fuzzing');
-export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.');
-export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index');
-
-export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
-
-export const SCANNER_NAMES_MAP = {
- SAST: SAST_SHORT_NAME,
- SAST_IAC: SAST_IAC_NAME,
- DAST: DAST_SHORT_NAME,
- API_FUZZING: API_FUZZING_NAME,
- CONTAINER_SCANNING: CONTAINER_SCANNING_NAME,
- COVERAGE_FUZZING: COVERAGE_FUZZING_NAME,
- SECRET_DETECTION: SECRET_DETECTION_NAME,
- DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
- BREACH_AND_ATTACK_SIMULATION: BAS_NAME,
- CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
- GENERIC: s__('ciReport|Manually added'),
-};
-
-export const securityFeatures = [
- {
- name: SAST_NAME,
- shortName: SAST_SHORT_NAME,
- description: SAST_DESCRIPTION,
- helpPath: SAST_HELP_PATH,
- configurationHelpPath: SAST_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SAST,
- },
- {
- name: SAST_IAC_NAME,
- shortName: SAST_IAC_SHORT_NAME,
- description: SAST_IAC_DESCRIPTION,
- helpPath: SAST_IAC_HELP_PATH,
- configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SAST_IAC,
- },
- {
- badge: {
- text: DAST_BADGE_TEXT,
- tooltipText: DAST_BADGE_TOOLTIP,
- variant: 'info',
- },
- secondary: {
- type: REPORT_TYPE_DAST_PROFILES,
- name: DAST_PROFILES_NAME,
- description: DAST_PROFILES_DESCRIPTION,
- configurationText: DAST_PROFILES_CONFIG_TEXT,
- },
- name: DAST_NAME,
- shortName: DAST_SHORT_NAME,
- description: DAST_DESCRIPTION,
- helpPath: DAST_HELP_PATH,
- configurationHelpPath: DAST_CONFIG_HELP_PATH,
- type: REPORT_TYPE_DAST,
- anchor: 'dast',
- },
- {
- name: DEPENDENCY_SCANNING_NAME,
- description: DEPENDENCY_SCANNING_DESCRIPTION,
- helpPath: DEPENDENCY_SCANNING_HELP_PATH,
- configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
- type: REPORT_TYPE_DEPENDENCY_SCANNING,
- anchor: 'dependency-scanning',
- slotComponent: ContinuousVulnerabilityScan,
- },
- {
- name: CONTAINER_SCANNING_NAME,
- description: CONTAINER_SCANNING_DESCRIPTION,
- helpPath: CONTAINER_SCANNING_HELP_PATH,
- configurationHelpPath: CONTAINER_SCANNING_CONFIG_HELP_PATH,
- type: REPORT_TYPE_CONTAINER_SCANNING,
- },
- {
- name: SECRET_DETECTION_NAME,
- description: SECRET_DETECTION_DESCRIPTION,
- helpPath: SECRET_DETECTION_HELP_PATH,
- configurationHelpPath: SECRET_DETECTION_CONFIG_HELP_PATH,
- type: REPORT_TYPE_SECRET_DETECTION,
- },
- {
- name: API_FUZZING_NAME,
- description: API_FUZZING_DESCRIPTION,
- helpPath: API_FUZZING_HELP_PATH,
- type: REPORT_TYPE_API_FUZZING,
- },
- {
- name: COVERAGE_FUZZING_NAME,
- description: COVERAGE_FUZZING_DESCRIPTION,
- helpPath: COVERAGE_FUZZING_HELP_PATH,
- configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH,
- type: REPORT_TYPE_COVERAGE_FUZZING,
- secondary: {
- type: REPORT_TYPE_CORPUS_MANAGEMENT,
- name: CORPUS_MANAGEMENT_NAME,
- description: CORPUS_MANAGEMENT_DESCRIPTION,
- configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT,
- },
- },
- {
- anchor: 'bas',
- badge: {
- alwaysDisplay: true,
- text: BAS_BADGE_TEXT,
- tooltipText: BAS_BADGE_TOOLTIP,
- variant: 'info',
- },
- description: BAS_DESCRIPTION,
- name: BAS_NAME,
- helpPath: BAS_HELP_PATH,
- secondary: {
- configurationHelpPath: BAS_DAST_FEATURE_FLAG_HELP_PATH,
- description: BAS_DAST_FEATURE_FLAG_DESCRIPTION,
- name: BAS_DAST_FEATURE_FLAG_NAME,
- },
- shortName: BAS_SHORT_NAME,
- type: REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
- },
-];
-
-export const featureToMutationMap = {
- [REPORT_TYPE_SAST]: {
- mutationId: 'configureSast',
- getMutationPayload: (projectPath) => ({
- mutation: configureSastMutation,
- variables: {
- input: {
- projectPath,
- configuration: { global: [], pipeline: [], analyzers: [] },
- },
- },
- }),
- },
- [REPORT_TYPE_SAST_IAC]: {
- mutationId: 'configureSastIac',
- getMutationPayload: (projectPath) => ({
- mutation: configureSastIacMutation,
- variables: {
- input: {
- projectPath,
- },
- },
- }),
- },
- [REPORT_TYPE_SECRET_DETECTION]: {
- mutationId: 'configureSecretDetection',
- getMutationPayload: (projectPath) => ({
- mutation: configureSecretDetectionMutation,
- variables: {
- input: {
- projectPath,
- },
- },
- }),
- },
-};
-
-export const AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY =
- 'security_configuration_auto_devops_enabled_dismissed_projects';
-
-// Fetch the svg path from the GraphQL query once this issue is resolved
-// https://gitlab.com/gitlab-org/gitlab/-/issues/346899
-export const TEMP_PROVIDER_LOGOS = {
- Kontra: {
- svg: kontraLogo,
- },
- [__('Secure Code Warrior')]: {
- svg: scwLogo,
- },
- SecureFlag: {
- svg: secureflagLogo,
- },
-};
-
-// Use the `url` field from the GraphQL query once this issue is resolved
-// https://gitlab.com/gitlab-org/gitlab/-/issues/356129
-export const TEMP_PROVIDER_URLS = {
- Kontra: 'https://application.security/',
- [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
- SecureFlag: 'https://www.secureflag.com/',
-};
-
-export const TAB_VULNERABILITY_MANAGEMENT_INDEX = 1;
diff --git a/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue b/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
deleted file mode 100644
index 61cbde2107c..00000000000
--- a/app/assets/javascripts/security_configuration/components/continuous_vulnerability_scan.vue
+++ /dev/null
@@ -1,127 +0,0 @@
-<script>
-import { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert } from '@gitlab/ui';
-import ProjectSetContinuousVulnerabilityScanning from '~/security_configuration/graphql/project_set_continuous_vulnerability_scanning.graphql';
-import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { __, s__ } from '~/locale';
-import { helpPagePath } from '~/helpers/help_page_helper';
-
-export default {
- name: 'ContinuousVulnerabilityscan',
- components: { GlBadge, GlIcon, GlToggle, GlLink, GlSprintf, GlAlert },
- mixins: [glFeatureFlagsMixin()],
- inject: ['continuousVulnerabilityScansEnabled', 'projectFullPath'],
- i18n: {
- badgeLabel: __('Experiment'),
- title: s__('CVS|Continuous Vulnerability Scan'),
- description: s__(
- 'CVS|Detect vulnerabilities outside a pipeline as new data is added to the GitLab Advisory Database.',
- ),
- learnMore: __('Learn more'),
- testingAgreementMessage: s__(
- 'CVS|By enabling this feature, you accept the %{linkStart}Testing Terms of Use%{linkEnd}',
- ),
- },
- props: {
- feature: {
- type: Object,
- required: true,
- },
- },
- data() {
- return {
- toggleValue: this.continuousVulnerabilityScansEnabled,
- errorMessage: '',
- isAlertDismissed: false,
- };
- },
- computed: {
- isFeatureConfigured() {
- return this.feature.available && this.feature.configured;
- },
- shouldShowAlert() {
- return this.errorMessage && !this.isAlertDismissed;
- },
- },
- methods: {
- reportError(error) {
- this.errorMessage = error;
- this.isAlertDismissed = false;
- },
- async toggleCVS(checked) {
- try {
- const { data } = await this.$apollo.mutate({
- mutation: ProjectSetContinuousVulnerabilityScanning,
- variables: {
- input: {
- projectPath: this.projectFullPath,
- enable: checked,
- },
- },
- });
-
- const { errors } = data.projectSetContinuousVulnerabilityScanning;
-
- if (errors.length > 0) {
- this.reportError(errors[0].message);
- }
- if (data.projectSetContinuousVulnerabilityScanning !== null) {
- this.toggleValue = checked;
- }
- } catch (error) {
- this.reportError(error);
- }
- },
- },
- CVSHelpPagePath: helpPagePath(
- 'user/application_security/continuous_vulnerability_scanning/index',
- ),
- experimentHelpPagePath: helpPagePath('policy/experiment-beta-support', { anchor: 'experiment' }),
-};
-</script>
-
-<template>
- <div v-if="glFeatures.dependencyScanningOnAdvisoryIngestion">
- <h4 class="gl-font-base gl-m-0 gl-mt-6">
- {{ $options.i18n.title }}
- <gl-badge
- ref="badge"
- :href="$options.experimentHelpPagePath"
- target="_blank"
- size="sm"
- variant="neutral"
- class="gl-cursor-pointer"
- >{{ $options.i18n.badgeLabel }}</gl-badge
- >
- </h4>
- <gl-alert
- v-if="shouldShowAlert"
- class="gl-mb-5 gl-mt-2"
- variant="danger"
- @dismiss="isAlertDismissed = true"
- >{{ errorMessage }}</gl-alert
- >
- <gl-toggle
- class="gl-mt-5"
- :disabled="!isFeatureConfigured"
- :value="toggleValue"
- :label="s__('CVS|Toggle CVS')"
- label-position="hidden"
- @change="toggleCVS"
- />
-
- <p class="gl-mb-0 gl-mt-5">
- {{ $options.i18n.description }}
- <gl-link :href="$options.CVSHelpPagePath" target="_blank">{{
- $options.i18n.learnMore
- }}</gl-link>
- <br />
- <gl-sprintf :message="$options.i18n.testingAgreementMessage">
- <template #link="{ content }">
- <gl-link href="https://about.gitlab.com/handbook/legal/testing-agreement" target="_blank">
- {{ content }} <gl-icon name="external-link" />
- </gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
-</template>
diff --git a/app/assets/javascripts/security_configuration/components/feature_card.vue b/app/assets/javascripts/security_configuration/components/feature_card.vue
index 395bdad5dcc..2100da78219 100644
--- a/app/assets/javascripts/security_configuration/components/feature_card.vue
+++ b/app/assets/javascripts/security_configuration/components/feature_card.vue
@@ -73,9 +73,6 @@ export default {
hasSecondary() {
return Boolean(this.feature.secondary);
},
- hasSlotComponent() {
- return Boolean(this.feature.slotComponent);
- },
// This condition is a temporary hack to not display any wrong information
// until this BE Bug is fixed: https://gitlab.com/gitlab-org/gitlab/-/issues/350307.
// More Information: https://gitlab.com/gitlab-org/gitlab/-/issues/350307#note_825447417
@@ -221,9 +218,5 @@ export default {
{{ $options.i18n.configurationGuide }}
</gl-button>
</div>
-
- <div v-if="hasSlotComponent">
- <component :is="feature.slotComponent" :feature="feature" />
- </div>
</gl-card>
</template>
diff --git a/app/assets/javascripts/security_configuration/components/training_provider_list.vue b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
index d424ec6dfeb..ae2894e25a2 100644
--- a/app/assets/javascripts/security_configuration/components/training_provider_list.vue
+++ b/app/assets/javascripts/security_configuration/components/training_provider_list.vue
@@ -18,6 +18,8 @@ import {
TRACK_TOGGLE_TRAINING_PROVIDER_LABEL,
TRACK_PROVIDER_LEARN_MORE_CLICK_ACTION,
TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL,
+ TEMP_PROVIDER_LOGOS,
+ TEMP_PROVIDER_URLS,
} from '~/security_configuration/constants';
import dismissUserCalloutMutation from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import securityTrainingProvidersQuery from '~/security_configuration/graphql/security_training_providers.query.graphql';
@@ -26,7 +28,6 @@ import {
updateSecurityTrainingCache,
updateSecurityTrainingOptimisticResponse,
} from '~/security_configuration/graphql/cache_utils';
-import { TEMP_PROVIDER_LOGOS, TEMP_PROVIDER_URLS } from './constants';
const i18n = {
providerQueryErrorMessage: __(
diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js
index 14eb10ac2aa..94bcf81a3eb 100644
--- a/app/assets/javascripts/security_configuration/constants.js
+++ b/app/assets/javascripts/security_configuration/constants.js
@@ -1,3 +1,334 @@
+import kontraLogo from 'images/vulnerability/kontra-logo.svg?raw';
+import scwLogo from 'images/vulnerability/scw-logo.svg?raw';
+import secureflagLogo from 'images/vulnerability/secureflag-logo.svg?raw';
+import { __, s__ } from '~/locale';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+import {
+ REPORT_TYPE_SAST,
+ REPORT_TYPE_SAST_IAC,
+ REPORT_TYPE_DAST,
+ REPORT_TYPE_DAST_PROFILES,
+ REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
+ REPORT_TYPE_SECRET_DETECTION,
+ REPORT_TYPE_DEPENDENCY_SCANNING,
+ REPORT_TYPE_CONTAINER_SCANNING,
+ REPORT_TYPE_COVERAGE_FUZZING,
+ REPORT_TYPE_CORPUS_MANAGEMENT,
+ REPORT_TYPE_API_FUZZING,
+} from '~/vue_shared/security_reports/constants';
+
+import configureSastMutation from './graphql/configure_sast.mutation.graphql';
+import configureSastIacMutation from './graphql/configure_iac.mutation.graphql';
+import configureSecretDetectionMutation from './graphql/configure_secret_detection.mutation.graphql';
+
+/**
+ * Translations & helpPagePaths for Security Configuration Page
+ * Make sure to add new scanner translations to the SCANNER_NAMES_MAP below.
+ */
+
+export const SAST_NAME = __('Static Application Security Testing (SAST)');
+export const SAST_SHORT_NAME = s__('ciReport|SAST');
+export const SAST_DESCRIPTION = __('Analyze your source code for known vulnerabilities.');
+export const SAST_HELP_PATH = helpPagePath('user/application_security/sast/index');
+export const SAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/sast/index', {
+ anchor: 'configuration',
+});
+
+export const SAST_IAC_NAME = __('Infrastructure as Code (IaC) Scanning');
+export const SAST_IAC_SHORT_NAME = s__('ciReport|SAST IaC');
+export const SAST_IAC_DESCRIPTION = __(
+ 'Analyze your infrastructure as code configuration files for known vulnerabilities.',
+);
+export const SAST_IAC_HELP_PATH = helpPagePath('user/application_security/iac_scanning/index');
+export const SAST_IAC_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/iac_scanning/index',
+ {
+ anchor: 'configuration',
+ },
+);
+
+export const DAST_NAME = __('Dynamic Application Security Testing (DAST)');
+export const DAST_SHORT_NAME = s__('ciReport|DAST');
+export const DAST_DESCRIPTION = s__(
+ 'ciReport|Analyze a deployed version of your web application for known vulnerabilities by examining it from the outside in. DAST works by simulating external attacks on your application while it is running.',
+);
+export const DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
+export const DAST_CONFIG_HELP_PATH = helpPagePath('user/application_security/dast/index', {
+ anchor: 'enable-automatic-dast-run',
+});
+export const DAST_BADGE_TEXT = __('Available on demand');
+export const DAST_BADGE_TOOLTIP = __(
+ 'On-demand scans run outside of the DevOps cycle and find vulnerabilities in your projects',
+);
+
+export const DAST_PROFILES_NAME = __('DAST profiles');
+export const DAST_PROFILES_DESCRIPTION = s__(
+ 'SecurityConfiguration|Manage profiles for use by DAST scans.',
+);
+export const DAST_PROFILES_CONFIG_TEXT = s__('SecurityConfiguration|Manage profiles');
+
+export const BAS_BADGE_TEXT = s__('SecurityConfiguration|Incubating feature');
+export const BAS_BADGE_TOOLTIP = s__(
+ 'SecurityConfiguration|Breach and Attack Simulation is an incubating feature extending existing security testing by simulating adversary activity.',
+);
+export const BAS_DESCRIPTION = s__(
+ 'SecurityConfiguration|Simulate breach and attack scenarios against your running application by attempting to detect and exploit known vulnerabilities.',
+);
+export const BAS_HELP_PATH = helpPagePath(
+ 'user/application_security/breach_and_attack_simulation/index',
+);
+export const BAS_NAME = s__('SecurityConfiguration|Breach and Attack Simulation (BAS)');
+export const BAS_SHORT_NAME = s__('SecurityConfiguration|BAS');
+
+export const BAS_DAST_FEATURE_FLAG_DESCRIPTION = s__(
+ 'SecurityConfiguration|Enable incubating Breach and Attack Simulation focused features such as callback attacks in your DAST scans.',
+);
+export const BAS_DAST_FEATURE_FLAG_HELP_PATH = helpPagePath(
+ 'user/application_security/breach_and_attack_simulation/index',
+ { anchor: 'extend-dynamic-application-security-testing-dast' },
+);
+export const BAS_DAST_FEATURE_FLAG_NAME = s__(
+ 'SecurityConfiguration|Out-of-Band Application Security Testing (OAST)',
+);
+
+export const SECRET_DETECTION_NAME = __('Secret Detection');
+export const SECRET_DETECTION_DESCRIPTION = __(
+ 'Analyze your source code and git history for secrets.',
+);
+export const SECRET_DETECTION_HELP_PATH = helpPagePath(
+ 'user/application_security/secret_detection/index',
+);
+export const SECRET_DETECTION_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/secret_detection/index',
+ { anchor: 'configuration' },
+);
+
+export const DEPENDENCY_SCANNING_NAME = __('Dependency Scanning');
+export const DEPENDENCY_SCANNING_DESCRIPTION = __(
+ 'Analyze your dependencies for known vulnerabilities.',
+);
+export const DEPENDENCY_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/dependency_scanning/index',
+);
+export const DEPENDENCY_SCANNING_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/dependency_scanning/index',
+ { anchor: 'configuration' },
+);
+
+export const CONTAINER_SCANNING_NAME = __('Container Scanning');
+export const CONTAINER_SCANNING_DESCRIPTION = __(
+ 'Check your Docker images for known vulnerabilities.',
+);
+export const CONTAINER_SCANNING_HELP_PATH = helpPagePath(
+ 'user/application_security/container_scanning/index',
+);
+export const CONTAINER_SCANNING_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/container_scanning/index',
+ { anchor: 'configuration' },
+);
+
+export const COVERAGE_FUZZING_NAME = __('Coverage Fuzzing');
+export const COVERAGE_FUZZING_DESCRIPTION = __(
+ 'Find bugs in your code with coverage-guided fuzzing.',
+);
+export const COVERAGE_FUZZING_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+);
+export const COVERAGE_FUZZING_CONFIG_HELP_PATH = helpPagePath(
+ 'user/application_security/coverage_fuzzing/index',
+ { anchor: 'enable-coverage-guided-fuzz-testing' },
+);
+
+export const CORPUS_MANAGEMENT_NAME = __('Corpus Management');
+export const CORPUS_MANAGEMENT_DESCRIPTION = s__(
+ 'SecurityConfiguration|Manage corpus files used as seed inputs with coverage-guided fuzzing.',
+);
+export const CORPUS_MANAGEMENT_CONFIG_TEXT = s__('SecurityConfiguration|Manage corpus');
+
+export const API_FUZZING_NAME = __('API Fuzzing');
+export const API_FUZZING_DESCRIPTION = __('Find bugs in your code with API fuzzing.');
+export const API_FUZZING_HELP_PATH = helpPagePath('user/application_security/api_fuzzing/index');
+
+export const CLUSTER_IMAGE_SCANNING_NAME = s__('ciReport|Cluster Image Scanning');
+
+export const SCANNER_NAMES_MAP = {
+ SAST: SAST_SHORT_NAME,
+ SAST_IAC: SAST_IAC_NAME,
+ DAST: DAST_SHORT_NAME,
+ API_FUZZING: API_FUZZING_NAME,
+ CONTAINER_SCANNING: CONTAINER_SCANNING_NAME,
+ COVERAGE_FUZZING: COVERAGE_FUZZING_NAME,
+ SECRET_DETECTION: SECRET_DETECTION_NAME,
+ DEPENDENCY_SCANNING: DEPENDENCY_SCANNING_NAME,
+ BREACH_AND_ATTACK_SIMULATION: BAS_NAME,
+ CLUSTER_IMAGE_SCANNING: CLUSTER_IMAGE_SCANNING_NAME,
+ GENERIC: s__('ciReport|Manually added'),
+};
+
+export const securityFeatures = [
+ {
+ name: SAST_NAME,
+ shortName: SAST_SHORT_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ configurationHelpPath: SAST_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ },
+ {
+ name: SAST_IAC_NAME,
+ shortName: SAST_IAC_SHORT_NAME,
+ description: SAST_IAC_DESCRIPTION,
+ helpPath: SAST_IAC_HELP_PATH,
+ configurationHelpPath: SAST_IAC_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST_IAC,
+ },
+ {
+ badge: {
+ text: DAST_BADGE_TEXT,
+ tooltipText: DAST_BADGE_TOOLTIP,
+ variant: 'info',
+ },
+ secondary: {
+ type: REPORT_TYPE_DAST_PROFILES,
+ name: DAST_PROFILES_NAME,
+ description: DAST_PROFILES_DESCRIPTION,
+ configurationText: DAST_PROFILES_CONFIG_TEXT,
+ },
+ name: DAST_NAME,
+ shortName: DAST_SHORT_NAME,
+ description: DAST_DESCRIPTION,
+ helpPath: DAST_HELP_PATH,
+ configurationHelpPath: DAST_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_DAST,
+ anchor: 'dast',
+ },
+ {
+ name: DEPENDENCY_SCANNING_NAME,
+ description: DEPENDENCY_SCANNING_DESCRIPTION,
+ helpPath: DEPENDENCY_SCANNING_HELP_PATH,
+ configurationHelpPath: DEPENDENCY_SCANNING_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_DEPENDENCY_SCANNING,
+ anchor: 'dependency-scanning',
+ },
+ {
+ name: CONTAINER_SCANNING_NAME,
+ description: CONTAINER_SCANNING_DESCRIPTION,
+ helpPath: CONTAINER_SCANNING_HELP_PATH,
+ configurationHelpPath: CONTAINER_SCANNING_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_CONTAINER_SCANNING,
+ },
+ {
+ name: SECRET_DETECTION_NAME,
+ description: SECRET_DETECTION_DESCRIPTION,
+ helpPath: SECRET_DETECTION_HELP_PATH,
+ configurationHelpPath: SECRET_DETECTION_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SECRET_DETECTION,
+ },
+ {
+ name: API_FUZZING_NAME,
+ description: API_FUZZING_DESCRIPTION,
+ helpPath: API_FUZZING_HELP_PATH,
+ type: REPORT_TYPE_API_FUZZING,
+ },
+ {
+ name: COVERAGE_FUZZING_NAME,
+ description: COVERAGE_FUZZING_DESCRIPTION,
+ helpPath: COVERAGE_FUZZING_HELP_PATH,
+ configurationHelpPath: COVERAGE_FUZZING_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_COVERAGE_FUZZING,
+ secondary: {
+ type: REPORT_TYPE_CORPUS_MANAGEMENT,
+ name: CORPUS_MANAGEMENT_NAME,
+ description: CORPUS_MANAGEMENT_DESCRIPTION,
+ configurationText: CORPUS_MANAGEMENT_CONFIG_TEXT,
+ },
+ },
+ {
+ anchor: 'bas',
+ badge: {
+ alwaysDisplay: true,
+ text: BAS_BADGE_TEXT,
+ tooltipText: BAS_BADGE_TOOLTIP,
+ variant: 'info',
+ },
+ description: BAS_DESCRIPTION,
+ name: BAS_NAME,
+ helpPath: BAS_HELP_PATH,
+ secondary: {
+ configurationHelpPath: BAS_DAST_FEATURE_FLAG_HELP_PATH,
+ description: BAS_DAST_FEATURE_FLAG_DESCRIPTION,
+ name: BAS_DAST_FEATURE_FLAG_NAME,
+ },
+ shortName: BAS_SHORT_NAME,
+ type: REPORT_TYPE_BREACH_AND_ATTACK_SIMULATION,
+ },
+];
+
+export const featureToMutationMap = {
+ [REPORT_TYPE_SAST]: {
+ mutationId: 'configureSast',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastMutation,
+ variables: {
+ input: {
+ projectPath,
+ configuration: { global: [], pipeline: [], analyzers: [] },
+ },
+ },
+ }),
+ },
+ [REPORT_TYPE_SAST_IAC]: {
+ mutationId: 'configureSastIac',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSastIacMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ }),
+ },
+ [REPORT_TYPE_SECRET_DETECTION]: {
+ mutationId: 'configureSecretDetection',
+ getMutationPayload: (projectPath) => ({
+ mutation: configureSecretDetectionMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ }),
+ },
+};
+
+export const AUTO_DEVOPS_ENABLED_ALERT_DISMISSED_STORAGE_KEY =
+ 'security_configuration_auto_devops_enabled_dismissed_projects';
+
+// Fetch the svg path from the GraphQL query once this issue is resolved
+// https://gitlab.com/gitlab-org/gitlab/-/issues/346899
+export const TEMP_PROVIDER_LOGOS = {
+ Kontra: {
+ svg: kontraLogo,
+ },
+ [__('Secure Code Warrior')]: {
+ svg: scwLogo,
+ },
+ SecureFlag: {
+ svg: secureflagLogo,
+ },
+};
+
+// Use the `url` field from the GraphQL query once this issue is resolved
+// https://gitlab.com/gitlab-org/gitlab/-/issues/356129
+export const TEMP_PROVIDER_URLS = {
+ Kontra: 'https://application.security/',
+ [__('Secure Code Warrior')]: 'https://www.securecodewarrior.com/',
+ SecureFlag: 'https://www.secureflag.com/',
+};
+
+export const TAB_VULNERABILITY_MANAGEMENT_INDEX = 1;
+
export const TRACK_TOGGLE_TRAINING_PROVIDER_ACTION = 'toggle_security_training_provider';
export const TRACK_TOGGLE_TRAINING_PROVIDER_LABEL = 'update_security_training_provider';
export const TRACK_CLICK_TRAINING_LINK_ACTION = 'click_security_training_link';
@@ -6,3 +337,25 @@ export const TRACK_PROVIDER_LEARN_MORE_CLICK_LABEL = 'security_training_provider
export const TRACK_TRAINING_LOADED_ACTION = 'security_training_link_loaded';
export const TRACK_PROMOTION_BANNER_CTA_CLICK_ACTION = 'click_button';
export const TRACK_PROMOTION_BANNER_CTA_CLICK_LABEL = 'security_training_promotion_cta';
+
+export const i18n = {
+ configurationHistory: s__('SecurityConfiguration|Configuration history'),
+ securityTesting: s__('SecurityConfiguration|Security testing'),
+ latestPipelineDescription: s__(
+ `SecurityConfiguration|The status of the tools only applies to the
+ default branch and is based on the %{linkStart}latest pipeline%{linkEnd}.`,
+ ),
+ description: s__(
+ `SecurityConfiguration|Once you've enabled a scan for the default branch,
+ any subsequent feature branch you create will include the scan. An enabled
+ scanner will not be reflected as such until the pipeline has been
+ successfully executed and it has generated valid artifacts.`,
+ ),
+ 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. Please note that security training is not accessible in an environment that is offline.',
+ ),
+ securityTrainingDoc: s__('SecurityConfiguration|Learn more about vulnerability training'),
+};
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 4b498091134..8086b200891 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -3,7 +3,7 @@ import VueApollo from 'vue-apollo';
import createDefaultClient from '~/lib/graphql';
import { parseBooleanDataAttributes } from '~/lib/utils/dom_utils';
import SecurityConfigurationApp from './components/app.vue';
-import { securityFeatures } from './components/constants';
+import { securityFeatures } from './constants';
import { augmentFeatures } from './utils';
export const initSecurityConfiguration = (el) => {
@@ -26,7 +26,6 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
- continuousVulnerabilityScansEnabled,
} = el.dataset;
const { augmentedSecurityFeatures } = augmentFeatures(
@@ -44,7 +43,6 @@ export const initSecurityConfiguration = (el) => {
autoDevopsHelpPagePath,
autoDevopsPath,
vulnerabilityTrainingDocsPath,
- continuousVulnerabilityScansEnabled,
},
render(createElement) {
return createElement(SecurityConfigurationApp, {
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index 7f0caf1af46..59b49cb3820 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,5 +1,5 @@
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { SCANNER_NAMES_MAP } from '~/security_configuration/components/constants';
+import { SCANNER_NAMES_MAP } from '~/security_configuration/constants';
import { REPORT_TYPE_DAST } from '~/vue_shared/security_reports/constants';
/**
diff --git a/app/assets/javascripts/set_status_modal/constants.js b/app/assets/javascripts/set_status_modal/constants.js
index 53e64db1497..86e24c29775 100644
--- a/app/assets/javascripts/set_status_modal/constants.js
+++ b/app/assets/javascripts/set_status_modal/constants.js
@@ -12,3 +12,5 @@ export const AVAILABILITY_STATUS = {
BUSY: 'busy',
NOT_SET: 'not_set',
};
+
+export const SET_STATUS_MODAL_ID = 'set-user-status-modal';
diff --git a/app/assets/javascripts/set_status_modal/set_status_form.vue b/app/assets/javascripts/set_status_modal/set_status_form.vue
index 60ed0d073fe..b6d609ab1fa 100644
--- a/app/assets/javascripts/set_status_modal/set_status_form.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_form.vue
@@ -14,7 +14,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
import * as Emoji from '~/emoji';
import { s__ } from '~/locale';
-import { formatDate, newDate, nSecondsAfter, isToday } from '~/lib/utils/datetime_utility';
+import { newDate, nSecondsAfter, isToday, localeDateFormat } from '~/lib/utils/datetime_utility';
import { TIME_RANGES_WITH_NEVER, AVAILABILITY_STATUS, NEVER_TIME_RANGE } from './constants';
export default {
@@ -148,10 +148,10 @@ export default {
},
formatClearStatusAfterDate(date) {
if (isToday(date)) {
- return formatDate(date, 'h:MMtt');
+ return localeDateFormat.asTime.format(date);
}
- return formatDate(date, 'mmm d, yyyy h:MMtt');
+ return localeDateFormat.asDateTime.format(date);
},
},
TIME_RANGES_WITH_NEVER,
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 270d7f0d182..7f229e5c5ed 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
@@ -2,17 +2,18 @@
import { GlToast, GlTooltipDirective, GlModal } from '@gitlab/ui';
import Vue from 'vue';
import { createAlert } from '~/alert';
-import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
+import { BV_HIDE_MODAL } from '~/lib/utils/constants';
import { s__ } from '~/locale';
import { updateUserStatus } from '~/rest_api';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isUserBusy, computedClearStatusAfterValue } from './utils';
-import { AVAILABILITY_STATUS } from './constants';
+import { AVAILABILITY_STATUS, SET_STATUS_MODAL_ID } from './constants';
import SetStatusForm from './set_status_form.vue';
Vue.use(GlToast);
export default {
+ SET_STATUS_MODAL_ID,
components: {
GlModal,
SetStatusForm,
@@ -29,11 +30,13 @@ export default {
},
currentEmoji: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
currentMessage: {
type: String,
- required: true,
+ required: false,
+ default: '',
},
currentAvailability: {
type: String,
@@ -51,7 +54,6 @@ export default {
defaultEmojiTag: '',
emoji: this.currentEmoji,
message: this.currentMessage,
- modalId: 'set-user-status-modal',
availability: isUserBusy(this.currentAvailability),
clearStatusAfter: null,
};
@@ -65,11 +67,11 @@ export default {
},
},
mounted() {
- this.$root.$emit(BV_SHOW_MODAL, this.modalId);
+ this.$emit('mounted');
},
methods: {
closeModal() {
- this.$root.$emit(BV_HIDE_MODAL, this.modalId);
+ this.$root.$emit(BV_HIDE_MODAL, SET_STATUS_MODAL_ID);
},
removeStatus() {
this.availability = false;
@@ -132,7 +134,7 @@ export default {
<template>
<gl-modal
:title="s__('SetStatusModal|Set a status')"
- :modal-id="modalId"
+ :modal-id="$options.SET_STATUS_MODAL_ID"
:action-primary="$options.actionPrimary"
:action-secondary="$options.actionSecondary"
modal-class="set-user-status-modal"
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
index 0563ed8394c..897cd3583c8 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.vue
@@ -1,5 +1,4 @@
<script>
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
@@ -104,8 +103,6 @@ export default {
.then(() => {
this.loading = false;
this.store.resetChanging();
-
- refreshUserMergeRequestCounts();
})
.catch(() => {
this.loading = false;
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
index 50fcd3c9350..478d261d06c 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_vue/dropdown_title.vue
@@ -37,7 +37,6 @@ export default {
category="tertiary"
size="small"
class="float-right js-sidebar-dropdown-toggle gl-mr-n2"
- data-qa-selector="labels_edit_button"
@click="toggleDropdownContents"
>
{{ __('Edit') }}
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
index f2ce02526e7..e3be7d549ab 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_value.vue
@@ -27,6 +27,11 @@ export default {
type: Boolean,
required: true,
},
+ supportsLockOnMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
labelsFilterBasePath: {
type: String,
required: true,
@@ -67,6 +72,12 @@ export default {
scopedLabel(label) {
return this.allowScopedLabels && isScopedLabel(label);
},
+ isLabelLocked(label) {
+ return label.lockOnMerge && this.supportsLockOnMerge;
+ },
+ showCloseButton(label) {
+ return this.allowLabelRemove && !this.isLabelLocked(label);
+ },
removeLabel(labelId) {
this.$emit('onLabelRemove', labelId);
},
@@ -115,7 +126,7 @@ export default {
:background-color="label.color"
:target="labelFilterUrl(label)"
:scoped="scopedLabel(label)"
- :show-close-button="allowLabelRemove"
+ :show-close-button="showCloseButton(label)"
:disabled="disableLabels"
tooltip-placement="top"
@close="removeLabel(label.id)"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
index a3bacc4a674..93c3dce4308 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/embedded_labels_list.vue
@@ -22,6 +22,11 @@ export default {
type: Boolean,
required: true,
},
+ supportsLockOnMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
labelsFilterBasePath: {
type: String,
required: true,
@@ -42,9 +47,17 @@ export default {
return `${basePath}?${filterParam}[]=${encodeURIComponent(title)}`;
},
- showScopedLabel(label) {
+ scopedLabel(label) {
return this.allowScopedLabels && isScopedLabel(label);
},
+ isLabelLocked(label) {
+ // These particular labels were initialized from HAML data, so the attributes are
+ // in snake case instead of camel case
+ return label.lock_on_merge && this.supportsLockOnMerge;
+ },
+ showCloseButton(label) {
+ return this.allowLabelRemove && !this.isLabelLocked(label);
+ },
removeLabel(labelId) {
this.$emit('onLabelRemove', labelId);
},
@@ -63,8 +76,8 @@ export default {
:description="label.description"
:background-color="label.color"
:target="buildFilterUrl(label)"
- :scoped="showScopedLabel(label)"
- :show-close-button="allowLabelRemove"
+ :scoped="scopedLabel(label)"
+ :show-close-button="showCloseButton(label)"
:disabled="disabled"
tooltip-placement="top"
@close="removeLabel(label.id)"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
index e0cdfd91658..5280054f0cc 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql
@@ -5,9 +5,11 @@ query mergeRequestLabels($fullPath: ID!, $iid: String!) {
id
issuable: mergeRequest(iid: $iid) {
id
+ supportsLockOnMerge
labels {
nodes {
...Label
+ lockOnMerge
}
}
}
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
index ac52e4dbf3f..e0d7400f7a6 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue
@@ -101,6 +101,11 @@ export default {
type: String,
required: true,
},
+ issuableSupportsLockOnMerge: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
workspaceType: {
type: String,
required: true,
@@ -151,6 +156,9 @@ export default {
isLabelListEnabled() {
return this.showEmbeddedLabelsList && isDropdownVariantEmbedded(this.variant);
},
+ isLockOnMergeSupported() {
+ return this.issuableSupportsLockOnMerge || this.issuable?.supportsLockOnMerge;
+ },
},
apollo: {
issuable: {
@@ -376,6 +384,7 @@ export default {
:disable-labels="labelsSelectInProgress"
:selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
+ :supports-lock-on-merge="isLockOnMergeSupported"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="handleLabelRemove"
@@ -389,6 +398,7 @@ export default {
:disable-labels="labelsSelectInProgress"
:selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
+ :supports-lock-on-merge="isLockOnMergeSupported"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
class="gl-mb-2"
@@ -440,6 +450,7 @@ export default {
:disabled="labelsSelectInProgress"
:selected-labels="issuableLabels"
:allow-label-remove="allowLabelRemove"
+ :supports-lock-on-merge="isLockOnMergeSupported"
:labels-filter-base-path="labelsFilterBasePath"
:labels-filter-param="labelsFilterParam"
@onLabelRemove="handleLabelRemove"
diff --git a/app/assets/javascripts/sidebar/components/participants/participants.vue b/app/assets/javascripts/sidebar/components/participants/participants.vue
index 99d36a61632..251a038f7ee 100644
--- a/app/assets/javascripts/sidebar/components/participants/participants.vue
+++ b/app/assets/javascripts/sidebar/components/participants/participants.vue
@@ -110,12 +110,12 @@ export default {
</div>
<div
v-if="showParticipantLabel"
- class="title hide-collapsed gl-mb-2! gl-line-height-20 gl-font-weight-bold"
+ class="title hide-collapsed gl-line-height-20 gl-font-weight-bold gl-mb-0!"
>
<gl-loading-icon v-if="loading" size="sm" :inline="true" />
{{ participantLabel }}
</div>
- <div class="hide-collapsed gl-display-flex gl-flex-wrap">
+ <div class="hide-collapsed gl-display-flex gl-flex-wrap gl-mt-2 gl-mb-n3">
<div
v-for="participant in visibleParticipants"
:key="participant.id"
diff --git a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
index 9c23f239b4c..1d1dbd51756 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/sidebar_reviewers.vue
@@ -2,11 +2,12 @@
// NOTE! For the first iteration, we are simply copying the implementation of Assignees
// It will soon be overhauled in Issue https://gitlab.com/gitlab-org/gitlab/-/issues/233736
import Vue from 'vue';
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/alert';
import { TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch';
import eventHub from '../../event_hub';
import getMergeRequestReviewersQuery from '../../queries/get_merge_request_reviewers.query.graphql';
import mergeRequestReviewersUpdatedSubscription from '../../queries/merge_request_reviewers.subscription.graphql';
@@ -26,6 +27,7 @@ export default {
ReviewerTitle,
Reviewers,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
mediator: {
type: Object,
@@ -56,6 +58,7 @@ export default {
return {
iid: this.issuableIid,
fullPath: this.projectPath,
+ mrRequestChanges: this.glFeatures.mrRequestChanges,
};
},
update(data) {
@@ -74,6 +77,7 @@ export default {
variables() {
return {
issuableId: this.issuable?.id,
+ mrRequestChanges: this.glFeatures.mrRequestChanges,
};
},
skip() {
@@ -153,7 +157,7 @@ export default {
.saveReviewers(this.field)
.then(() => {
this.loading = false;
- refreshUserMergeRequestCounts();
+ fetchUserCounts();
this.$apollo.queries.issuable.refetch();
})
.catch(() => {
diff --git a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
index 99f9d5e872c..3d0e7db6a68 100644
--- a/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
+++ b/app/assets/javascripts/sidebar/components/reviewers/uncollapsed_reviewer_list.vue
@@ -2,12 +2,36 @@
import { GlButton, GlTooltipDirective, GlIcon } from '@gitlab/ui';
import { TYPE_ISSUE } from '~/issues/constants';
import { __, sprintf, s__ } from '~/locale';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import ReviewerAvatarLink from './reviewer_avatar_link.vue';
const LOADING_STATE = 'loading';
const SUCCESS_STATE = 'success';
const JUST_APPROVED = 'approved';
+const REVIEW_STATE_ICONS = {
+ APPROVED: {
+ name: 'status-success',
+ class: 'gl-text-green-500',
+ title: __('Reviewer approved changes'),
+ },
+ REQUESTED_CHANGES: {
+ name: 'status-alert',
+ class: 'gl-text-red-500',
+ title: __('Reviewer requested changes'),
+ },
+ REVIEWED: {
+ name: 'comment',
+ class: 'gl-bg-blue-500 gl-text-white gl-icon s16 gl-rounded-full gl--flex-center',
+ size: 8,
+ title: __('Reviewer commented'),
+ },
+ UNREVIEWED: {
+ name: 'dotted-circle',
+ title: __('Awaiting review'),
+ },
+};
+
export default {
i18n: {
reRequestReview: __('Re-request review'),
@@ -20,6 +44,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
users: {
type: Array,
@@ -105,6 +130,25 @@ export default {
this.loadingStates[userId] = null;
}
},
+ reviewStateIcon(user) {
+ if (user.mergeRequestInteraction.approved) {
+ return {
+ ...REVIEW_STATE_ICONS.APPROVED,
+ class: [
+ REVIEW_STATE_ICONS.APPROVED.class,
+ this.loadingStates[user.id] === JUST_APPROVED && 'merge-request-approved-icon',
+ ],
+ };
+ }
+ return REVIEW_STATE_ICONS[user.mergeRequestInteraction.reviewState];
+ },
+ showRequestReviewButton(user) {
+ if (this.glFeatures.mrRequestChanges && !user.mergeRequestInteraction.approved) {
+ return user.mergeRequestInteraction.reviewState !== 'UNREVIEWED';
+ }
+
+ return true;
+ },
},
LOADING_STATE,
SUCCESS_STATE,
@@ -134,7 +178,7 @@ export default {
</div>
</reviewer-avatar-link>
<gl-button
- v-if="user.mergeRequestInteraction.canUpdate && user.mergeRequestInteraction.reviewed"
+ v-if="user.mergeRequestInteraction.canUpdate && showRequestReviewButton(user)"
v-gl-tooltip.left
:title="$options.i18n.reRequestReview"
:aria-label="$options.i18n.reRequestReview"
@@ -146,25 +190,42 @@ export default {
data-testid="re-request-button"
@click="reRequestReview(user.id)"
/>
- <gl-icon
- v-if="user.mergeRequestInteraction.approved"
- v-gl-tooltip.left
- :size="16"
- :title="approvedByTooltipTitle(user)"
- name="status-success"
- class="float-right gl-my-2 gl-ml-auto gl-text-green-500 gl-flex-shrink-0"
- :class="approveAnimation(user.id)"
- data-testid="approved"
- />
- <gl-icon
- v-else-if="user.mergeRequestInteraction.reviewed"
- v-gl-tooltip.left
- :size="16"
- :title="reviewedButNotApprovedTooltip(user)"
- name="dotted-circle"
- class="float-right gl-my-2 gl-ml-auto gl-text-gray-400 gl-flex-shrink-0"
- data-testid="reviewed-not-approved"
- />
+ <template v-if="glFeatures.mrRequestChanges">
+ <span
+ v-gl-tooltip.top.viewport
+ :title="reviewStateIcon(user).title"
+ :class="reviewStateIcon(user).class"
+ class="float-right gl-my-2 gl-ml-auto gl-flex-shrink-0"
+ >
+ <gl-icon
+ :size="reviewStateIcon(user).size || 16"
+ :name="reviewStateIcon(user).name"
+ :aria-label="reviewStateIcon(user).title"
+ data-testid="reviewer-state-icon"
+ />
+ </span>
+ </template>
+ <template v-else>
+ <gl-icon
+ v-if="user.mergeRequestInteraction.approved"
+ v-gl-tooltip.left
+ :size="16"
+ :title="approvedByTooltipTitle(user)"
+ name="status-success"
+ class="float-right gl-my-2 gl-ml-auto gl-text-green-500 gl-flex-shrink-0"
+ :class="approveAnimation(user.id)"
+ data-testid="approved"
+ />
+ <gl-icon
+ v-else-if="user.mergeRequestInteraction.reviewed"
+ v-gl-tooltip.left
+ :size="16"
+ :title="reviewedButNotApprovedTooltip(user)"
+ name="dotted-circle"
+ class="float-right gl-my-2 gl-ml-auto gl-text-gray-400 gl-flex-shrink-0"
+ data-testid="reviewed-not-approved"
+ />
+ </template>
</div>
</div>
</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
index c9450244b40..5cc3c552bf8 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown.vue
@@ -87,6 +87,11 @@ export default {
return [WORKSPACE_GROUP, WORKSPACE_PROJECT].includes(value);
},
},
+ showWorkItemEpics: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -115,6 +120,7 @@ export default {
fullPath: this.attrWorkspacePath,
state: this.issuableAttributesState[this.issuableAttribute],
sort: defaultEpicSort,
+ includeWorkItems: this.showWorkItemEpics,
};
if (epicIidPattern.test(this.searchTerm)) {
@@ -127,7 +133,12 @@ export default {
return variables;
},
- update: (data) => data?.workspace?.attributes?.nodes ?? [],
+ update(data) {
+ return [
+ ...(data?.workspace?.attributes?.nodes ?? []),
+ ...(data?.workspace?.workItems?.nodes ?? []),
+ ];
+ },
error(error) {
createAlert({ message: this.i18n.listFetchError, captureError: true, error });
},
@@ -188,7 +199,7 @@ export default {
this.skipQuery = false;
},
setFocus() {
- this.$refs.search.focusInput();
+ this.$refs?.search?.focusInput();
},
show() {
this.$refs.dropdown.show();
@@ -211,7 +222,12 @@ export default {
@show="handleShow"
@shown="setFocus"
>
- <gl-search-box-by-type ref="search" v-model="searchTerm" :placeholder="__('Search')" />
+ <gl-search-box-by-type
+ v-if="!showWorkItemEpics"
+ ref="search"
+ v-model="searchTerm"
+ :placeholder="__('Search')"
+ />
<gl-dropdown-item
:data-testid="`no-${formatIssuableAttribute.kebab}-item`"
is-check-item
diff --git a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
index 28b88a59405..0ecf89bd169 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue
@@ -3,10 +3,11 @@ import { GlButton, GlIcon, GlLink, GlPopover, GlTooltipDirective } from '@gitlab
import { kebabCase, snakeCase } from 'lodash';
import { createAlert } from '~/alert';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
+import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import { __ } from '~/locale';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+
import {
dropdowni18nText,
LocalizedIssuableAttributeType,
@@ -79,6 +80,21 @@ export default {
required: false,
default: undefined,
},
+ showWorkItemEpics: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isEpicAttribute: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ issuableParent: {
+ type: Object,
+ required: false,
+ default: null,
+ },
},
apollo: {
issuable: {
@@ -98,7 +114,7 @@ export default {
return data.workspace?.issuable || {};
},
result({ data }) {
- if (this.glFeatures?.epicWidgetEditConfirmation && this.isEpic) {
+ if (this.glFeatures?.epicWidgetEditConfirmation && this.isEpicAttribute) {
this.hasCurrentAttribute = data?.workspace?.issuable.hasEpic;
}
},
@@ -140,6 +156,9 @@ export default {
},
computed: {
currentAttribute() {
+ if (this.isEpicAttribute && this.issuableParent?.attribute) {
+ return this.issuableParent.attribute;
+ }
return this.issuable.attribute;
},
issuableId() {
@@ -171,10 +190,6 @@ export default {
LocalizedIssuableAttributeType[IssuableAttributeTypeKeyMap[this.issuableAttribute]];
return dropdowni18nText(localizedAttribute, this.issuableType);
},
- isEpic() {
- // MV to EE https://gitlab.com/gitlab-org/gitlab/-/issues/345311
- return this.issuableAttribute === TYPE_EPIC;
- },
formatIssuableAttribute() {
return {
kebab: kebabCase(this.issuableAttribute),
@@ -186,7 +201,7 @@ export default {
return false;
}
- return this.isEpic && this.currentAttribute === null && this.hasCurrentAttribute
+ return this.isEpicAttribute && this.currentAttribute === null && this.hasCurrentAttribute
? !this.editConfirmation
: false;
},
@@ -195,46 +210,50 @@ export default {
},
},
methods: {
- updateAttribute({ id }) {
+ updateAttribute({ id, workItemType }) {
if (this.currentAttribute === null && id === null) return;
if (id === this.currentAttribute?.id) return;
- this.updating = true;
+ if (this.showWorkItemEpics && this.isEpicAttribute) {
+ this.$emit('updateAttribute', { id, workItemType });
+ } else {
+ this.updating = true;
- const { current } = this.issuableAttributeQuery;
- const { mutation } = current[this.issuableType];
+ const { current } = this.issuableAttributeQuery;
+ const { mutation } = current[this.issuableType];
- this.$apollo
- .mutate({
- mutation,
- variables: {
- fullPath: this.workspacePath,
- attributeId:
- this.issuableAttribute === IssuableAttributeType.Milestone &&
- this.issuableType === TYPE_ISSUE
- ? getIdFromGraphQLId(id)
- : id,
- iid: this.iid,
- },
- })
- .then(({ data }) => {
- if (data.issuableSetAttribute?.errors?.length) {
- createAlert({
- message: data.issuableSetAttribute.errors[0],
- captureError: true,
- error: data.issuableSetAttribute.errors[0],
- });
- } else {
- this.$emit('attribute-updated', data);
- }
- })
- .catch((error) => {
- createAlert({ message: this.i18n.updateError, captureError: true, error });
- })
- .finally(() => {
- this.updating = false;
- this.selectedTitle = null;
- });
+ this.$apollo
+ .mutate({
+ mutation,
+ variables: {
+ fullPath: this.workspacePath,
+ attributeId:
+ this.issuableAttribute === IssuableAttributeType.Milestone &&
+ this.issuableType === TYPE_ISSUE
+ ? getIdFromGraphQLId(id)
+ : id,
+ iid: this.iid,
+ },
+ })
+ .then(({ data }) => {
+ if (data.issuableSetAttribute?.errors?.length) {
+ createAlert({
+ message: data.issuableSetAttribute.errors[0],
+ captureError: true,
+ error: data.issuableSetAttribute.errors[0],
+ });
+ } else {
+ this.$emit('attribute-updated', data);
+ }
+ })
+ .catch((error) => {
+ createAlert({ message: this.i18n.updateError, captureError: true, error });
+ })
+ .finally(() => {
+ this.updating = false;
+ this.selectedTitle = null;
+ });
+ }
},
isAttributeOverdue(attribute) {
return this.issuableAttribute === IssuableAttributeType.Milestone
@@ -356,6 +375,7 @@ export default {
:current-attribute="currentAttribute"
:issuable-attribute="issuableAttribute"
:issuable-type="issuableType"
+ :show-work-item-epics="showWorkItemEpics"
@change="updateAttribute"
>
<template #list="{ attributesList, isAttributeChecked, updateAttribute: update }">
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index 2653748861b..ad83866ceb2 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -164,7 +164,7 @@ export default {
</gl-button>
</div>
<template v-if="!initialLoading">
- <div v-show="!edit" data-testid="collapsed-content" class="gl-line-height-14">
+ <div v-show="!edit" data-testid="collapsed-content">
<slot name="collapsed">{{ __('None') }}</slot>
</div>
<div v-show="edit" data-testid="expanded-content" :class="{ 'gl-mt-3': !isClassicSidebar }">
diff --git a/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
index f087ca6c982..6d3d2302a19 100644
--- a/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
+++ b/app/assets/javascripts/sidebar/queries/get_merge_request_reviewers.query.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
+query mergeRequestReviewers($fullPath: ID!, $iid: String!, $mrRequestChanges: Boolean!) {
workspace: project(fullPath: $fullPath) {
id
issuable: mergeRequest(iid: $iid) {
@@ -14,7 +14,8 @@ query mergeRequestReviewers($fullPath: ID!, $iid: String!) {
canMerge
canUpdate
approved
- reviewed
+ reviewed @skip(if: $mrRequestChanges)
+ reviewState @include(if: $mrRequestChanges)
}
}
}
diff --git a/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
index a1b16b378b3..0fcfe297394 100644
--- a/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
+++ b/app/assets/javascripts/sidebar/queries/merge_request_reviewers.subscription.graphql
@@ -1,7 +1,7 @@
#import "~/graphql_shared/fragments/user.fragment.graphql"
#import "~/graphql_shared/fragments/user_availability.fragment.graphql"
-subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
+subscription mergeRequestReviewersUpdated($issuableId: IssuableID!, $mrRequestChanges: Boolean!) {
mergeRequestReviewersUpdated(issuableId: $issuableId) {
... on MergeRequest {
id
@@ -13,7 +13,8 @@ subscription mergeRequestReviewersUpdated($issuableId: IssuableID!) {
canMerge
canUpdate
approved
- reviewed
+ reviewed @skip(if: $mrRequestChanges)
+ reviewState @include(if: $mrRequestChanges)
}
}
}
diff --git a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
index 9b0a1db23f2..3a85f66cfa2 100644
--- a/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
+++ b/app/assets/javascripts/snippets/components/snippet_blob_edit.vue
@@ -56,6 +56,7 @@ export default {
.get(url, {
// This prevents axios from automatically JSON.parse response
transformResponse: [(f) => f],
+ headers: { 'Cache-Control': 'no-cache' },
})
.then((res) => {
this.notifyAboutUpdates({ content: res.data });
diff --git a/app/assets/javascripts/snippets/components/snippet_title.vue b/app/assets/javascripts/snippets/components/snippet_title.vue
index 0e4dbf55963..33058fcc58b 100644
--- a/app/assets/javascripts/snippets/components/snippet_title.vue
+++ b/app/assets/javascripts/snippets/components/snippet_title.vue
@@ -1,15 +1,24 @@
<script>
-import { GlSprintf } from '@gitlab/ui';
-
+import { GlIcon, GlTooltipDirective, GlSprintf } from '@gitlab/ui';
+import { __, s__ } from '~/locale';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import SnippetDescription from './snippet_description_view.vue';
export default {
+ name: 'SnippetTitle',
+ i18n: {
+ hiddenTooltip: s__('Snippets|This snippet is hidden because its author has been banned'),
+ hiddenAriaLabel: __('Hidden'),
+ },
components: {
+ GlIcon,
TimeAgoTooltip,
GlSprintf,
SnippetDescription,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
props: {
snippet: {
type: Object,
@@ -19,14 +28,31 @@ export default {
};
</script>
<template>
- <div class="snippet-header limited-header-width">
- <h2 class="snippet-title gl-mt-0 mb-3" data-testid="snippet-title-content">
- {{ snippet.title }}
- </h2>
+ <div class="snippet-header limited-header-width gl-py-3">
+ <div class="gl-display-flex">
+ <span
+ v-if="snippet.hidden"
+ class="gl-bg-orange-50 gl-text-orange-600 gl-h-6 gl-w-6 border-radius-default gl-line-height-24 gl-text-center gl-mr-3 gl-mt-2"
+ >
+ <gl-icon
+ v-gl-tooltip.bottom
+ name="spam"
+ :title="$options.i18n.hiddenTooltip"
+ :aria-label="$options.i18n.hiddenAriaLabel"
+ />
+ </span>
+
+ <h2 class="snippet-title gl-mt-0 gl-mb-5" data-testid="snippet-title-content">
+ {{ snippet.title }}
+ </h2>
+ </div>
<snippet-description v-if="snippet.description" :description="snippet.descriptionHtml" />
- <small v-if="snippet.updatedAt !== snippet.createdAt" class="edited-text">
+ <small
+ v-if="snippet.updatedAt !== snippet.createdAt"
+ class="edited-text gl-text-secondary gl-display-inline-block gl-mt-4"
+ >
<gl-sprintf :message="__('Edited %{timeago}')">
<template #timeago>
<time-ago-tooltip :time="snippet.updatedAt" tooltip-placement="bottom" />
diff --git a/app/assets/javascripts/sortable/constants.js b/app/assets/javascripts/sortable/constants.js
index f5bb0a3b11f..eac8033448b 100644
--- a/app/assets/javascripts/sortable/constants.js
+++ b/app/assets/javascripts/sortable/constants.js
@@ -1,4 +1,5 @@
export const DRAG_CLASS = 'is-dragging';
+export const DRAG_DELAY = 100;
/**
* Default config options for sortablejs.
diff --git a/app/assets/javascripts/super_sidebar/components/counter.vue b/app/assets/javascripts/super_sidebar/components/counter.vue
index 49efc5ab5b9..267f9d4321b 100644
--- a/app/assets/javascripts/super_sidebar/components/counter.vue
+++ b/app/assets/javascripts/super_sidebar/components/counter.vue
@@ -48,7 +48,7 @@ export default {
:is="component"
:aria-label="ariaLabel"
:href="href"
- class="counter gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-font-sm gl-hover-text-gray-900 gl-hover-text-decoration-none gl-focus--focus"
+ class="user-bar-button gl-display-block gl-flex-grow-1 gl-text-center gl-py-3 gl-rounded-base gl-border-none gl-line-height-1 gl-font-sm gl-hover-text-decoration-none"
>
<gl-icon aria-hidden="true" :name="icon" />
<span v-if="count" aria-hidden="true" class="gl-ml-1">{{ formattedCount }}</span>
diff --git a/app/assets/javascripts/super_sidebar/components/extra_info.vue b/app/assets/javascripts/super_sidebar/components/extra_info.vue
new file mode 100644
index 00000000000..23340f1190f
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/extra_info.vue
@@ -0,0 +1,7 @@
+<script>
+export default {};
+</script>
+<template>
+ <!-- This is intentionally left blank -->
+ <div></div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
index 6f0a0a1fe79..252967b33b5 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_groups.vue
@@ -1,36 +1,33 @@
<script>
import { s__ } from '~/locale';
-import { MAX_FREQUENT_GROUPS_COUNT } from '~/super_sidebar/constants';
+import currentUserFrecentGroupsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql';
import FrequentItems from './frequent_items.vue';
export default {
name: 'FrequentlyVisitedGroups',
+ apollo: {
+ frecentGroups: {
+ query: currentUserFrecentGroupsQuery,
+ },
+ },
components: {
FrequentItems,
},
inject: ['groupsPath'],
- data() {
- const username = gon.current_username;
-
- return {
- storageKey: username ? `${username}/frequent-groups` : null,
- };
- },
i18n: {
groupName: s__('Navigation|Frequently visited groups'),
viewAllText: s__('Navigation|View all my groups'),
emptyStateText: s__('Navigation|Groups you visit often will appear here.'),
},
- MAX_FREQUENT_GROUPS_COUNT,
};
</script>
<template>
<frequent-items
+ :loading="$apollo.queries.frecentGroups.loading"
:empty-state-text="$options.i18n.emptyStateText"
:group-name="$options.i18n.groupName"
- :max-items="$options.MAX_FREQUENT_GROUPS_COUNT"
- :storage-key="storageKey"
+ :items="frecentGroups"
view-all-items-icon="group"
:view-all-items-text="$options.i18n.viewAllText"
:view-all-items-path="groupsPath"
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
index 5371887ee0f..b76d238c559 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item.vue
@@ -1,31 +1,17 @@
<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
-import { __ } from '~/locale';
export default {
name: 'FrequentlyVisitedItem',
components: {
- GlButton,
ProjectAvatar,
},
- directives: {
- GlTooltip: GlTooltipDirective,
- },
props: {
item: {
type: Object,
required: true,
},
},
- methods: {
- onRemove() {
- this.$emit('remove', this.item);
- },
- },
- i18n: {
- remove: __('Remove'),
- },
};
</script>
@@ -49,16 +35,5 @@ export default {
{{ item.subtitle }}
</div>
</div>
-
- <gl-button
- v-gl-tooltip.left
- icon="dash"
- category="tertiary"
- :aria-label="$options.i18n.remove"
- :title="$options.i18n.remove"
- class="show-on-focus-or-hover--target"
- @click.stop.prevent="onRemove"
- @keydown.enter.stop.prevent="onRemove"
- />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item_skeleton.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item_skeleton.vue
new file mode 100644
index 00000000000..dce18b2c46e
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_item_skeleton.vue
@@ -0,0 +1,17 @@
+<script>
+import { GlSkeletonLoader } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlSkeletonLoader,
+ },
+};
+</script>
+
+<template>
+ <gl-skeleton-loader :width="737" :height="48">
+ <rect width="24" height="24" y="12" x="8" />
+ <rect width="120" height="12" y="10" x="36" />
+ <rect width="100" height="12" y="26" x="36" />
+ </gl-skeleton-loader>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
index ddadd6856ca..60692361683 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_items.vue
@@ -1,10 +1,10 @@
<script>
import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlIcon } from '@gitlab/ui';
import { truncateNamespace } from '~/lib/utils/text_utility';
-import { getItemsFromLocalStorage, removeItemFromLocalStorage } from '~/super_sidebar/utils';
import { TRACKING_UNKNOWN_PANEL } from '~/super_sidebar/constants';
import { TRACKING_CLICK_COMMAND_PALETTE_ITEM } from '../command_palette/constants';
import FrequentItem from './frequent_item.vue';
+import FrequentItemSkeleton from './frequent_item_skeleton.vue';
export default {
name: 'FrequentlyVisitedItems',
@@ -13,8 +13,14 @@ export default {
GlDisclosureDropdownItem,
GlIcon,
FrequentItem,
+ FrequentItemSkeleton,
},
props: {
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
emptyStateText: {
type: String,
required: true,
@@ -23,15 +29,6 @@ export default {
type: String,
required: true,
},
- maxItems: {
- type: Number,
- required: true,
- },
- storageKey: {
- type: String,
- required: false,
- default: null,
- },
viewAllItemsText: {
type: String,
required: true,
@@ -45,14 +42,11 @@ export default {
required: false,
default: null,
},
- },
- data() {
- return {
- items: getItemsFromLocalStorage({
- storageKey: this.storageKey,
- maxItems: this.maxItems,
- }),
- };
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
},
computed: {
formattedItems() {
@@ -83,7 +77,7 @@ export default {
}));
},
showEmptyState() {
- return this.items.length === 0;
+ return !this.loading && this.formattedItems.length === 0;
},
viewAllItem() {
return {
@@ -92,38 +86,26 @@ export default {
};
},
},
- created() {
- if (!this.storageKey) {
- this.$emit('nothing-to-render');
- }
- },
- methods: {
- removeItem(item) {
- removeItemFromLocalStorage({
- storageKey: this.storageKey,
- item,
- });
-
- this.items = this.items.filter((i) => i.id !== item.id);
- },
- },
};
</script>
<template>
- <gl-disclosure-dropdown-group v-if="storageKey" v-bind="$attrs">
+ <gl-disclosure-dropdown-group v-bind="$attrs">
<template #group-label>{{ groupName }}</template>
- <gl-disclosure-dropdown-item
- v-for="item of formattedItems"
- :key="item.forDropdown.id"
- :item="item.forDropdown"
- class="show-on-focus-or-hover--context"
- >
- <template #list-item
- ><frequent-item :item="item.forRenderer" @remove="removeItem"
- /></template>
+ <gl-disclosure-dropdown-item v-if="loading">
+ <frequent-item-skeleton />
</gl-disclosure-dropdown-item>
+ <template v-else>
+ <gl-disclosure-dropdown-item
+ v-for="item of formattedItems"
+ :key="item.forDropdown.id"
+ :item="item.forDropdown"
+ class="show-on-focus-or-hover--context"
+ >
+ <template #list-item><frequent-item :item="item.forRenderer" /></template>
+ </gl-disclosure-dropdown-item>
+ </template>
<gl-disclosure-dropdown-item v-if="showEmptyState" class="gl-cursor-text">
<span class="gl-text-gray-500 gl-font-sm gl-my-3 gl-mx-3">{{ emptyStateText }}</span>
diff --git a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
index 35b254099c2..2d13ab3dd4a 100644
--- a/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
+++ b/app/assets/javascripts/super_sidebar/components/global_search/components/frequent_projects.vue
@@ -1,36 +1,33 @@
<script>
import { s__ } from '~/locale';
-import { MAX_FREQUENT_PROJECTS_COUNT } from '~/super_sidebar/constants';
+import currentUserFrecentProjectsQuery from '~/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql';
import FrequentItems from './frequent_items.vue';
export default {
name: 'FrequentlyVisitedProjects',
+ apollo: {
+ frecentProjects: {
+ query: currentUserFrecentProjectsQuery,
+ },
+ },
components: {
FrequentItems,
},
inject: ['projectsPath'],
- data() {
- const username = gon.current_username;
-
- return {
- storageKey: username ? `${username}/frequent-projects` : null,
- };
- },
i18n: {
groupName: s__('Navigation|Frequently visited projects'),
viewAllText: s__('Navigation|View all my projects'),
emptyStateText: s__('Navigation|Projects you visit often will appear here.'),
},
- MAX_FREQUENT_PROJECTS_COUNT,
};
</script>
<template>
<frequent-items
+ :loading="$apollo.queries.frecentProjects.loading"
:empty-state-text="$options.i18n.emptyStateText"
:group-name="$options.i18n.groupName"
- :max-items="$options.MAX_FREQUENT_PROJECTS_COUNT"
- :storage-key="storageKey"
+ :items="frecentProjects"
view-all-items-icon="project"
:view-all-items-text="$options.i18n.viewAllText"
:view-all-items-path="projectsPath"
diff --git a/app/assets/javascripts/super_sidebar/components/help_center.vue b/app/assets/javascripts/super_sidebar/components/help_center.vue
index 069987d4006..5278bd66f47 100644
--- a/app/assets/javascripts/super_sidebar/components/help_center.vue
+++ b/app/assets/javascripts/super_sidebar/components/help_center.vue
@@ -14,9 +14,6 @@ import { STORAGE_KEY } from '~/whats_new/utils/notification';
import Tracking from '~/tracking';
import { DROPDOWN_Y_OFFSET, HELP_MENU_TRACKING_DEFAULTS, helpCenterState } from '../constants';
-// Left offset required for the dropdown to be aligned with the super sidebar
-const DROPDOWN_X_OFFSET = -4;
-
export default {
components: {
GlBadge,
@@ -50,6 +47,7 @@ export default {
return {
showWhatsNewNotification: this.shouldShowWhatsNewNotification(),
helpCenterState,
+ toggleWhatsNewDrawer: null,
};
},
computed: {
@@ -180,12 +178,11 @@ export default {
this.showWhatsNewNotification = false;
if (!this.toggleWhatsNewDrawer) {
- const appEl = document.getElementById('whats-new-app');
const { default: toggleWhatsNewDrawer } = await import(
/* webpackChunkName: 'whatsNewApp' */ '~/whats_new'
);
this.toggleWhatsNewDrawer = toggleWhatsNewDrawer;
- this.toggleWhatsNewDrawer(appEl);
+ this.toggleWhatsNewDrawer(this.sidebarData.whats_new_version_digest);
} else {
this.toggleWhatsNewDrawer();
}
@@ -204,7 +201,7 @@ export default {
});
},
},
- dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET, crossAxis: DROPDOWN_X_OFFSET },
+ dropdownOffset: { mainAxis: DROPDOWN_Y_OFFSET },
};
</script>
@@ -215,7 +212,11 @@ export default {
@hidden="trackDropdownToggle(false)"
>
<template #toggle>
- <gl-button category="tertiary" icon="question-o" class="btn-with-notification">
+ <gl-button
+ category="tertiary"
+ icon="question-o"
+ class="super-sidebar-help-center-toggle btn-with-notification"
+ >
<span
v-if="showWhatsNewNotification"
data-testid="notification-dot"
@@ -250,7 +251,7 @@ export default {
<template #list-item="{ item }">
<span class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
{{ item.text }}
- <gl-icon v-if="item.icon" :name="item.icon" class="gl-text-purple-600" />
+ <gl-icon v-if="item.icon" :name="item.icon" class="gl-text-gray-500" />
</span>
</template>
</gl-disclosure-dropdown-group>
diff --git a/app/assets/javascripts/super_sidebar/components/menu_section.vue b/app/assets/javascripts/super_sidebar/components/menu_section.vue
index a672e254004..292373df9f4 100644
--- a/app/assets/javascripts/super_sidebar/components/menu_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/menu_section.vue
@@ -1,6 +1,7 @@
<script>
import { kebabCase } from 'lodash';
import { GlCollapse, GlIcon } from '@gitlab/ui';
+import { NAV_ITEM_LINK_ACTIVE_CLASS } from '../constants';
import NavItem from './nav_item.vue';
import FlyoutMenu from './flyout_menu.vue';
@@ -61,9 +62,7 @@ export default {
return this.isExpanded ? 'chevron-up' : 'chevron-down';
},
computedLinkClasses() {
- return {
- 'gl-bg-t-gray-a-08': this.isActive,
- };
+ return this.isActive ? NAV_ITEM_LINK_ACTIVE_CLASS : null;
},
isActive() {
return !this.isExpanded && this.item.is_active;
@@ -109,9 +108,9 @@ export default {
<hr v-if="separated" aria-hidden="true" class="gl-mx-4 gl-my-2" />
<button
:id="`menu-section-button-${itemId}`"
- class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-2 gl-py-2 gl-px-3 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus"
+ class="super-sidebar-nav-item gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-2 gl-py-2 gl-px-3 gl-text-black-normal! gl-text-decoration-none! gl-appearance-none gl-border-0 gl-bg-transparent gl-text-left gl-w-full gl-focus--focus"
:class="computedLinkClasses"
- data-qa-selector="menu_section_button"
+ data-testid="menu-section-button"
:data-qa-section-name="item.title"
v-bind="buttonProps"
@click="isExpanded = !isExpanded"
@@ -126,7 +125,11 @@ export default {
></span>
<span class="gl-flex-shrink-0 gl-w-6 gl-display-flex">
<slot name="icon">
- <gl-icon v-if="item.icon" :name="item.icon" class="gl-m-auto item-icon" />
+ <gl-icon
+ v-if="item.icon"
+ :name="item.icon"
+ class="super-sidebar-nav-item-icon gl-m-auto"
+ />
</slot>
</span>
@@ -153,7 +156,7 @@ export default {
:id="itemId"
v-model="isExpanded"
class="gl-list-style-none gl-p-0 gl-m-0 gl-transition-duration-medium gl-transition-timing-function-ease"
- data-qa-selector="menu_section"
+ data-testid="menu-section"
:data-qa-section-name="item.title"
>
<slot>
diff --git a/app/assets/javascripts/super_sidebar/components/nav_item.vue b/app/assets/javascripts/super_sidebar/components/nav_item.vue
index 3ae33bf8b37..14dd704c24c 100644
--- a/app/assets/javascripts/super_sidebar/components/nav_item.vue
+++ b/app/assets/javascripts/super_sidebar/components/nav_item.vue
@@ -223,20 +223,24 @@ export default {
:is="navItemLinkComponent"
#default="{ isActive }"
v-bind="linkProps"
- class="gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
+ class="super-sidebar-nav-item gl-relative gl-display-flex gl-align-items-center gl-min-h-7 gl-gap-3 gl-mb-1 gl-py-2 gl-text-black-normal! gl-text-decoration-none! gl-focus--focus show-on-focus-or-hover--control hide-on-focus-or-hover--control"
:class="computedLinkClasses"
data-testid="nav-item-link"
>
<div
:class="[isActive ? 'gl-opacity-10' : 'gl-opacity-0']"
- class="active-indicator gl-bg-blue-500 gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow"
+ class="active-indicator gl-absolute gl-left-2 gl-top-2 gl-bottom-2 gl-transition-slow"
aria-hidden="true"
:style="activeIndicatorStyle"
data-testid="active-indicator"
></div>
<div v-if="!isFlyout" class="gl-flex-shrink-0 gl-w-6 gl-display-flex">
<slot name="icon">
- <gl-icon v-if="item.icon" :name="item.icon" class="gl-m-auto item-icon" />
+ <gl-icon
+ v-if="item.icon"
+ :name="item.icon"
+ class="super-sidebar-nav-item-icon gl-m-auto"
+ />
<gl-icon
v-else-if="isInPinnedSection"
name="grip"
@@ -264,7 +268,6 @@ export default {
v-if="hasPill"
size="sm"
variant="neutral"
- class="gl-bg-t-gray-a-08!"
:class="{
'hide-on-focus-or-hover--target transition-opacity-on-hover--target': isPinnable,
}"
diff --git a/app/assets/javascripts/super_sidebar/components/scroll_scrim.vue b/app/assets/javascripts/super_sidebar/components/scroll_scrim.vue
new file mode 100644
index 00000000000..0e849b08a39
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/components/scroll_scrim.vue
@@ -0,0 +1,72 @@
+<script>
+export default {
+ name: 'ScrollScrim',
+ data() {
+ return {
+ topBoundaryVisible: true,
+ bottomBoundaryVisible: true,
+ };
+ },
+ computed: {
+ scrimClasses() {
+ return {
+ 'top-scrim-visible': !this.topBoundaryVisible,
+ 'bottom-scrim-visible gl-border-b': !this.bottomBoundaryVisible,
+ };
+ },
+ },
+ mounted() {
+ this.observeScroll();
+ },
+ beforeDestroy() {
+ this.scrollObserver?.disconnect();
+ },
+
+ methods: {
+ observeScroll() {
+ const root = this.$el;
+
+ const options = {
+ rootMargin: '8px',
+ root,
+ threshold: 1.0,
+ };
+
+ this.scrollObserver?.disconnect();
+
+ const observer = new IntersectionObserver((entries) => {
+ entries.forEach((entry) => {
+ this[entry.target?.$__visibilityProp] = entry.isIntersecting;
+ });
+ }, options);
+
+ const topBoundary = this.$refs['top-boundary'];
+ const bottomBoundary = this.$refs['bottom-boundary'];
+
+ topBoundary.$__visibilityProp = 'topBoundaryVisible';
+ observer.observe(topBoundary);
+
+ bottomBoundary.$__visibilityProp = 'bottomBoundaryVisible';
+ observer.observe(bottomBoundary);
+
+ this.scrollObserver = observer;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-scroll-scrim gl-overflow-auto" :class="scrimClasses">
+ <div class="top-scrim-wrapper">
+ <div class="top-scrim"></div>
+ </div>
+ <div ref="top-boundary"></div>
+
+ <slot></slot>
+
+ <div ref="bottom-boundary"></div>
+ <div class="bottom-scrim-wrapper">
+ <div class="bottom-scrim"></div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
index c04addf5262..5f067621814 100644
--- a/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/sidebar_menu.vue
@@ -194,7 +194,7 @@ export default {
/>
<ul
aria-labelledby="super-sidebar-context-header"
- class="gl-p-0 gl-list-style-none"
+ class="gl-p-0 gl-mb-0 gl-list-style-none"
data-testid="non-static-items-section"
>
<template v-for="item in nonStaticItems">
diff --git a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
index 5f7cfce93b1..57ba00ee0a6 100644
--- a/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
+++ b/app/assets/javascripts/super_sidebar/components/super_sidebar.vue
@@ -1,9 +1,13 @@
<script>
import { GlButton } from '@gitlab/ui';
+import { GlBreakpointInstance, breakpoints } from '@gitlab/ui/dist/utils';
+import ExtraInfo from 'jh_else_ce/super_sidebar/components/extra_info.vue';
import { Mousetrap } from '~/lib/mousetrap';
+import { TAB_KEY_CODE } from '~/lib/utils/keycodes';
import { keysFor, TOGGLE_SUPER_SIDEBAR } from '~/behaviors/shortcuts/keybindings';
import { __, s__ } from '~/locale';
import Tracking from '~/tracking';
+import eventHub from '../event_hub';
import {
sidebarState,
SUPER_SIDEBAR_PEEK_STATE_CLOSED as STATE_CLOSED,
@@ -18,16 +22,19 @@ import HelpCenter from './help_center.vue';
import SidebarMenu from './sidebar_menu.vue';
import SidebarPeekBehavior from './sidebar_peek_behavior.vue';
import SidebarHoverPeekBehavior from './sidebar_hover_peek_behavior.vue';
+import ScrollScrim from './scroll_scrim.vue';
export default {
components: {
GlButton,
UserBar,
HelpCenter,
+ ExtraInfo,
SidebarMenu,
SidebarPeekBehavior,
SidebarHoverPeekBehavior,
SidebarPortalTarget,
+ ScrollScrim,
TrialStatusWidget: () =>
import('ee_component/contextual_sidebar/components/trial_status_widget.vue'),
TrialStatusPopover: () =>
@@ -37,6 +44,7 @@ export default {
i18n: {
skipToMainContent: __('Skip to main content'),
primaryNavigation: s__('Navigation|Primary navigation'),
+ adminArea: s__('Navigation|Admin Area'),
},
inject: ['showTrialStatusWidget'],
props: {
@@ -51,6 +59,7 @@ export default {
showPeekHint: false,
isMouseover: false,
breakpoint: null,
+ showSuperSidebarContextHeader: true,
};
},
computed: {
@@ -68,6 +77,13 @@ export default {
};
},
},
+ watch: {
+ 'sidebarState.isCollapsed': {
+ handler() {
+ this.setupFocusTrapListener();
+ },
+ },
+ },
created() {
const {
is_logged_in: isLoggedIn,
@@ -80,9 +96,12 @@ export default {
}
},
mounted() {
+ this.setupFocusTrapListener();
Mousetrap.bind(keysFor(TOGGLE_SUPER_SIDEBAR), this.toggleSidebar);
+ eventHub.$on('toggle-menu-header', this.onToggleMenuHeader);
},
beforeDestroy() {
+ document.removeEventListener('keydown', this.focusTrap);
Mousetrap.unbind(keysFor(TOGGLE_SUPER_SIDEBAR));
},
methods: {
@@ -93,6 +112,17 @@ export default {
});
toggleSuperSidebarCollapsed(!isCollapsed(), true);
},
+ setupFocusTrapListener() {
+ /**
+ * Only trap focus when sidebar displays over page content to avoid
+ * focus moving to page content and being obscured by the sidebar
+ */
+ if (GlBreakpointInstance.windowWidth() < breakpoints.xl && !this.sidebarState.isCollapsed) {
+ document.addEventListener('keydown', this.focusTrap);
+ } else {
+ document.removeEventListener('keydown', this.focusTrap);
+ }
+ },
collapseSidebar() {
toggleSuperSidebarCollapsed(true, false);
},
@@ -122,6 +152,26 @@ export default {
this.sidebarState.isCollapsed = true;
}
},
+ focusTrap(event) {
+ const { keyCode, shiftKey } = event;
+ const firstFocusableElement = this.$refs.userBar.$el.querySelector('a');
+ const lastFocusableElement = this.$refs.helpCenter.$el.querySelector('button');
+
+ if (keyCode !== TAB_KEY_CODE) return;
+
+ if (shiftKey) {
+ if (document.activeElement === firstFocusableElement) {
+ lastFocusableElement.focus();
+ event.preventDefault();
+ }
+ } else if (document.activeElement === lastFocusableElement) {
+ firstFocusableElement.focus();
+ event.preventDefault();
+ }
+ },
+ onToggleMenuHeader(forceState) {
+ this.showSuperSidebarContextHeader = forceState;
+ },
},
};
</script>
@@ -144,7 +194,6 @@ export default {
class="super-sidebar"
:class="peekClasses"
data-testid="super-sidebar"
- data-qa-selector="navbar"
:inert="sidebarState.isCollapsed"
@mouseenter="isMouseover = true"
@mouseleave="isMouseover = false"
@@ -152,18 +201,19 @@ export default {
<h2 id="super-sidebar-heading" class="gl-sr-only">
{{ $options.i18n.primaryNavigation }}
</h2>
- <user-bar :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
+ <user-bar ref="userBar" :has-collapse-button="!showOverlay" :sidebar-data="sidebarData" />
<div v-if="showTrialStatusWidget" class="gl-px-2 gl-py-2">
<trial-status-widget
- class="gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-hover-bg-t-gray-a-08 gl-focus-bg-t-gray-a-08 gl-text-decoration-none! gl-py-3"
+ class="super-sidebar-nav-item gl-rounded-base gl-relative gl-display-flex gl-align-items-center gl-mb-1 gl-px-3 gl-line-height-normal gl-text-black-normal! gl-text-decoration-none! gl-py-3"
/>
<trial-status-popover />
</div>
<div
class="contextual-nav gl-display-flex gl-flex-direction-column gl-flex-grow-1 gl-overflow-hidden"
>
- <div class="gl-flex-grow-1 gl-overflow-auto" data-testid="nav-container">
+ <scroll-scrim class="gl-flex-grow-1" data-testid="nav-container">
<div
+ v-if="showSuperSidebarContextHeader"
id="super-sidebar-context-header"
class="gl-px-5 gl-pt-3 gl-pb-2 gl-m-0 gl-reset-line-height gl-font-weight-bold gl-font-sm super-sidebar-context-header"
>
@@ -178,9 +228,20 @@ export default {
:update-pins-url="sidebarData.update_pins_url"
/>
<sidebar-portal-target />
- </div>
- <div class="gl-p-3">
- <help-center :sidebar-data="sidebarData" />
+ </scroll-scrim>
+ <div class="gl-p-2">
+ <help-center ref="helpCenter" :sidebar-data="sidebarData" />
+ <gl-button
+ v-if="sidebarData.is_admin"
+ class="gl-fixed gl-right-0 gl-mr-3 gl-mt-2"
+ data-testid="sidebar-admin-link"
+ :href="sidebarData.admin_url"
+ icon="admin"
+ size="small"
+ >
+ {{ $options.i18n.adminArea }}
+ </gl-button>
+ <extra-info />
</div>
</div>
</nav>
diff --git a/app/assets/javascripts/super_sidebar/components/user_bar.vue b/app/assets/javascripts/super_sidebar/components/user_bar.vue
index 3c47245a1a6..3c8bf62ff5c 100644
--- a/app/assets/javascripts/super_sidebar/components/user_bar.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_bar.vue
@@ -200,7 +200,7 @@ export default {
id="super-sidebar-search"
v-gl-tooltip.bottom.html="searchTooltip"
v-gl-modal="$options.SEARCH_MODAL_ID"
- class="counter gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-inset-border-1-gray-a-08 gl-line-height-1 gl-focus--focus gl-w-full"
+ class="user-bar-button gl-display-block gl-py-3 gl-bg-gray-10 gl-rounded-base gl-text-gray-900 gl-border-none gl-line-height-1 gl-w-full"
data-testid="super-sidebar-search-button"
>
<gl-icon name="search" />
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index 5712b716f48..f129d067cdc 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -5,11 +5,13 @@ import {
GlDisclosureDropdownGroup,
GlDisclosureDropdownItem,
GlButton,
+ GlModalDirective,
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, __, sprintf } from '~/locale';
import Tracking from '~/tracking';
import PersistentUserCallout from '~/persistent_user_callout';
+import { SET_STATUS_MODAL_ID } from '~/set_status_modal/constants';
import { USER_MENU_TRACKING_DEFAULTS, DROPDOWN_Y_OFFSET, IMPERSONATING_OFFSET } from '../constants';
import UserMenuProfileItem from './user_menu_profile_item.vue';
@@ -18,10 +20,8 @@ const DROPDOWN_X_OFFSET_BASE = -211;
const DROPDOWN_X_OFFSET_IMPERSONATING = DROPDOWN_X_OFFSET_BASE + IMPERSONATING_OFFSET;
export default {
+ SET_STATUS_MODAL_ID,
i18n: {
- newNavigation: {
- sectionTitle: s__('NorthstarNavigation|Navigation redesign'),
- },
setStatus: s__('SetStatusModal|Set status'),
editStatus: s__('SetStatusModal|Edit status'),
editProfile: s__('CurrentUser|Edit profile'),
@@ -39,9 +39,14 @@ export default {
GlDisclosureDropdownItem,
GlButton,
UserMenuProfileItem,
+ SetStatusModal: () =>
+ import(
+ /* webpackChunkName: 'statusModalBundle' */ '~/set_status_modal/set_status_modal_wrapper.vue'
+ ),
},
directives: {
SafeHtml,
+ GlModal: GlModalDirective,
},
mixins: [Tracking.mixin()],
inject: ['isImpersonating'],
@@ -51,7 +56,16 @@ export default {
type: Object,
},
},
+ data() {
+ return {
+ setStatusModalReady: false,
+ updatedAvatarUrl: null,
+ };
+ },
computed: {
+ avatarUrl() {
+ return this.updatedAvatarUrl || this.data.avatar_url;
+ },
toggleText() {
return sprintf(__('%{user} user’s menu'), { user: this.data.name });
},
@@ -64,7 +78,8 @@ export default {
return {
text: statusLabel,
extraAttrs: {
- class: 'js-set-status-modal-trigger',
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'user_edit_status',
},
};
},
@@ -86,7 +101,7 @@ export default {
text: this.$options.i18n.editProfile,
href: this.data.settings.profile_path,
extraAttrs: {
- 'data-testid': 'edit_profile_link',
+ 'data-testid': 'edit-profile-link',
...USER_MENU_TRACKING_DEFAULTS,
'data-track-label': 'user_edit_profile',
},
@@ -135,7 +150,7 @@ export default {
href: this.data.sign_out_link,
extraAttrs: {
'data-method': 'post',
- 'data-testid': 'sign_out_link',
+ 'data-testid': 'sign-out-link',
class: 'sign-out-link',
},
},
@@ -143,24 +158,22 @@ export default {
};
},
statusModalData() {
- const defaultData = {
- 'data-current-emoji': '',
- 'data-current-message': '',
- 'data-default-emoji': 'speech_balloon',
- };
+ if (!this.data?.status?.can_update) {
+ return null;
+ }
const { busy, customized } = this.data.status;
if (!busy && !customized) {
- return defaultData;
+ return {};
}
+ const { emoji, message, availability, clear_after: clearAfter } = this.data.status;
return {
- ...defaultData,
- 'data-current-emoji': this.data.status.emoji,
- 'data-current-message': this.data.status.message,
- 'data-current-availability': this.data.status.availability,
- 'data-current-clear-status-after': this.data.status.clear_after,
+ 'current-emoji': emoji || '',
+ 'current-message': message || '',
+ 'current-availability': availability || '',
+ 'current-clear-status-after': clearAfter || '',
};
},
buyPipelineMinutesCalloutData() {
@@ -181,7 +194,16 @@ export default {
};
},
},
+ mounted() {
+ document.addEventListener('userAvatar:update', this.updateAvatar);
+ },
+ unmounted() {
+ document.removeEventListener('userAvatar:update', this.updateAvatar);
+ },
methods: {
+ updateAvatar(event) {
+ this.updatedAvatarUrl = event.detail?.url;
+ },
onShow() {
this.initBuyCIMinsCallout();
},
@@ -226,14 +248,14 @@ export default {
@shown="onShow"
>
<template #toggle>
- <gl-button category="tertiary" class="user-bar-item btn-with-notification">
+ <gl-button category="tertiary" class="user-bar-dropdown-toggle btn-with-notification">
<span class="gl-sr-only">{{ toggleText }}</span>
<gl-avatar
:size="24"
:entity-name="data.name"
- :src="data.avatar_url"
+ :src="avatarUrl"
aria-hidden="true"
- data-testid="user_avatar_content"
+ data-testid="user-avatar-content"
/>
<span
v-if="showNotificationDot"
@@ -251,7 +273,8 @@ export default {
<gl-disclosure-dropdown-group bordered>
<gl-disclosure-dropdown-item
- v-if="data.status.can_update"
+ v-if="setStatusModalReady && statusModalData"
+ v-gl-modal="$options.SET_STATUS_MODAL_ID"
:item="statusItem"
data-testid="status-item"
@action="closeDropdown"
@@ -307,11 +330,11 @@ export default {
@action="trackSignOut"
/>
</gl-disclosure-dropdown>
-
- <div
- v-if="data.status.can_update"
- class="js-set-status-modal-wrapper"
+ <set-status-modal
+ v-if="statusModalData"
+ default-emoji="speech_balloon"
v-bind="statusModalData"
- ></div>
+ @mounted="setStatusModalReady = true"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/super_sidebar/constants.js b/app/assets/javascripts/super_sidebar/constants.js
index e96dca3f365..815c2ec8e8e 100644
--- a/app/assets/javascripts/super_sidebar/constants.js
+++ b/app/assets/javascripts/super_sidebar/constants.js
@@ -25,9 +25,6 @@ export const helpCenterState = Vue.observable({
showTanukiBotChatDrawer: false,
});
-export const MAX_FREQUENT_PROJECTS_COUNT = 5;
-export const MAX_FREQUENT_GROUPS_COUNT = 3;
-
export const SUPER_SIDEBAR_PEEK_OPEN_DELAY = 200;
export const SUPER_SIDEBAR_PEEK_CLOSE_DELAY = 500;
export const SUPER_SIDEBAR_PEEK_STATE_CLOSED = 'closed';
@@ -57,6 +54,18 @@ export const SIDEBAR_COOKIE_EXPIRATION = 365 * 10;
export const DROPDOWN_Y_OFFSET = 4;
-export const NAV_ITEM_LINK_ACTIVE_CLASS = 'gl-bg-t-gray-a-08';
+export const NAV_ITEM_LINK_ACTIVE_CLASS = 'super-sidebar-nav-item-current';
export const IMPERSONATING_OFFSET = 34;
+
+// Frequent items constants
+export const FREQUENT_ITEMS = {
+ MAX_COUNT: 20,
+ ELIGIBLE_FREQUENCY: 3,
+};
+
+export const FIFTEEN_MINUTES_IN_MS = 900000;
+
+export const STORAGE_KEY = {
+ projects: 'frequent-projects',
+};
diff --git a/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql b/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql
new file mode 100644
index 00000000000..82b9a53c36e
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_groups.query.graphql
@@ -0,0 +1,9 @@
+query CurrentUserFrecentGroups {
+ frecentGroups {
+ id
+ name
+ namespace: fullName
+ webUrl
+ avatarUrl
+ }
+}
diff --git a/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql b/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql
new file mode 100644
index 00000000000..4b406d1ea6c
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/graphql/queries/current_user_frecent_projects.query.graphql
@@ -0,0 +1,9 @@
+query CurrentUserFrecentProjects {
+ frecentProjects {
+ id
+ name
+ namespace: nameWithNamespace
+ webUrl
+ avatarUrl
+ }
+}
diff --git a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
index 9e540175b48..6aa974878d0 100644
--- a/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
+++ b/app/assets/javascripts/super_sidebar/super_sidebar_bundle.js
@@ -1,7 +1,8 @@
import Vue from 'vue';
import { GlToast } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
import { convertObjectPropsToCamelCase, parseBoolean } from '~/lib/utils/common_utils';
-import { initStatusTriggers } from '../header';
+import createDefaultClient from '~/lib/graphql';
import { JS_TOGGLE_EXPAND_CLASS } from './constants';
import createStore from './components/global_search/store';
import {
@@ -12,6 +13,11 @@ import SuperSidebar from './components/super_sidebar.vue';
import SuperSidebarToggle from './components/super_sidebar_toggle.vue';
Vue.use(GlToast);
+Vue.use(VueApollo);
+
+const apolloProvider = new VueApollo({
+ defaultClient: createDefaultClient(),
+});
const getTrialStatusWidgetData = (sidebarData) => {
if (sidebarData.trial_status_widget_data_attrs && sidebarData.trial_status_popover_data_attrs) {
@@ -90,6 +96,7 @@ export const initSuperSidebar = () => {
return new Vue({
el,
name: 'SuperSidebarRoot',
+ apolloProvider,
provide: {
rootPath,
isImpersonating,
@@ -145,5 +152,3 @@ export const initSuperSidebarToggle = () => {
},
});
};
-
-requestIdleCallback(initStatusTriggers);
diff --git a/app/assets/javascripts/super_sidebar/user_counts_fetch.js b/app/assets/javascripts/super_sidebar/user_counts_fetch.js
new file mode 100644
index 00000000000..779cb2609c2
--- /dev/null
+++ b/app/assets/javascripts/super_sidebar/user_counts_fetch.js
@@ -0,0 +1,10 @@
+/**
+ * This triggers a re-fetch of the user counts
+ *
+ * It is separate from the user_counts_manager, so that
+ * this function is side-effect free and can be used in
+ * anywhere in the app without bloating bundle size
+ */
+export function fetchUserCounts() {
+ document.dispatchEvent(new CustomEvent('userCounts:fetch'));
+}
diff --git a/app/assets/javascripts/super_sidebar/utils.js b/app/assets/javascripts/super_sidebar/utils.js
index 3d6eef62ad2..18334a7d139 100644
--- a/app/assets/javascripts/super_sidebar/utils.js
+++ b/app/assets/javascripts/super_sidebar/utils.js
@@ -1,6 +1,6 @@
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import AccessorUtilities from '~/lib/utils/accessor';
-import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
+import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/super_sidebar/constants';
import axios from '~/lib/utils/axios_utils';
/**
@@ -26,8 +26,13 @@ const sortItemsByFrequencyAndLastAccess = (items) =>
return 0;
});
-// This imitates getTopFrequentItems from app/assets/javascripts/frequent_items/utils.js, but
-// adjusts the rules to accommodate for the context switcher's designs.
+/**
+ * Returns the most frequently visited items.
+ *
+ * @param {Array} items - A list of items retrieved from the local storage
+ * @param {Number} maxCount - The maximum number of items to be returned
+ * @returns {Array}
+ */
export const getTopFrequentItems = (items, maxCount) => {
if (!Array.isArray(items)) return [];
@@ -39,11 +44,12 @@ export const getTopFrequentItems = (items, maxCount) => {
/**
* This tracks projects' and groups' visits in order to suggest a list of frequently visited
- * entities to the user. Currently, this track visits in two ways:
- * - The legacy approach uses a simple counting algorithm and stores the data in the local storage.
- * - The above approach is being migrated to a backend-based one, where visits will be stored in the
- * DB, and suggestions will be made through a smarter algorithm. When we are ready to transition
- * to the newer approach, the legacy one will be cleaned up.
+ * entities to the user. The suggestion logic is implemented server-side and computed items can be
+ * retrieved through the GraphQL API.
+ * To persist a visit in the DB, an AJAX request needs to be triggered by the client. To avoid making
+ * the request on every visited page, we also keep track of the visits in the local storage so that
+ * the request is only sent once every 15 minutes per namespace per user.
+ *
* @param {object} item The project/group item being tracked.
* @param {string} namespace A string indicating whether the tracked entity is a project or a group.
* @param {string} trackVisitsPath The API endpoint to track visits server-side.
@@ -115,31 +121,4 @@ export const trackContextAccess = (username, context, trackVisitsPath) => {
return localStorage.setItem(storageKey, JSON.stringify(storedItems));
};
-export const getItemsFromLocalStorage = ({ storageKey, maxItems }) => {
- if (!AccessorUtilities.canUseLocalStorage()) {
- return [];
- }
-
- try {
- const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
- return getTopFrequentItems(parsedCachedFrequentItems, maxItems);
- } catch (e) {
- Sentry.captureException(e);
- return [];
- }
-};
-
-export const removeItemFromLocalStorage = ({ storageKey, item }) => {
- try {
- const parsedCachedFrequentItems = JSON.parse(localStorage.getItem(storageKey));
- const filteredItems = parsedCachedFrequentItems.filter((i) => i.id !== item.id);
- localStorage.setItem(storageKey, JSON.stringify(filteredItems));
-
- return filteredItems;
- } catch (e) {
- Sentry.captureException(e);
- return [];
- }
-};
-
export const ariaCurrent = (isActive) => (isActive ? 'page' : null);
diff --git a/app/assets/javascripts/task_list.js b/app/assets/javascripts/task_list.js
index bb344ade344..c4aaacb4159 100644
--- a/app/assets/javascripts/task_list.js
+++ b/app/assets/javascripts/task_list.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import 'deckar01-task_list';
import { __ } from '~/locale';
import { createAlert } from '~/alert';
+import { TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
import axios from './lib/utils/axios_utils';
export default class TaskList {
@@ -94,7 +95,8 @@ export default class TaskList {
const { index, checked, lineNumber, lineSource } = e.detail;
const patchData = {};
- patchData[this.dataType] = {
+ const dataType = this.dataType === TYPE_INCIDENT ? TYPE_ISSUE : this.dataType;
+ patchData[dataType] = {
[this.fieldName]: $target.val(),
lock_version: this.lockVersion,
update_task: {
diff --git a/app/assets/javascripts/terms/components/app.vue b/app/assets/javascripts/terms/components/app.vue
index 29099bcc366..75ee0e16d4e 100644
--- a/app/assets/javascripts/terms/components/app.vue
+++ b/app/assets/javascripts/terms/components/app.vue
@@ -66,7 +66,7 @@ export default {
<template>
<div>
- <div class="gl-relative gl-pb-0 gl-px-0" data-qa-selector="terms_content">
+ <div class="gl-relative gl-pb-0 gl-px-0" data-testid="terms-content">
<div
class="terms-fade gl-absolute gl-left-5 gl-right-5 gl-bottom-0 gl-h-11 gl-pointer-events-none"
></div>
@@ -97,7 +97,7 @@ export default {
type="submit"
variant="confirm"
:disabled="acceptDisabled"
- data-qa-selector="accept_terms_button"
+ data-testid="accept-terms-button"
>{{ $options.i18n.accept }}</gl-button
>
<input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
diff --git a/app/assets/javascripts/terraform/components/states_table.vue b/app/assets/javascripts/terraform/components/states_table.vue
index 273cd599308..bbd9a056efc 100644
--- a/app/assets/javascripts/terraform/components/states_table.vue
+++ b/app/assets/javascripts/terraform/components/states_table.vue
@@ -11,7 +11,7 @@ import {
} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { s__, sprintf } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import StateActions from './states_table_actions.vue';
diff --git a/app/assets/javascripts/token_access/components/inbound_token_access.vue b/app/assets/javascripts/token_access/components/inbound_token_access.vue
index 345db1752f6..3c44d014edc 100644
--- a/app/assets/javascripts/token_access/components/inbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/inbound_token_access.vue
@@ -43,14 +43,12 @@ export default {
key: 'project',
label: __('Project with access'),
thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
},
{
key: 'actions',
label: '',
tdClass: 'gl-text-right',
thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-10p',
},
],
components: {
diff --git a/app/assets/javascripts/token_access/components/outbound_token_access.vue b/app/assets/javascripts/token_access/components/outbound_token_access.vue
index 846b0d1791f..3df466de4d3 100644
--- a/app/assets/javascripts/token_access/components/outbound_token_access.vue
+++ b/app/assets/javascripts/token_access/components/outbound_token_access.vue
@@ -51,14 +51,12 @@ export default {
key: 'project',
label: __('Project that can be accessed'),
thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-40p',
},
{
key: 'actions',
label: '',
tdClass: 'gl-text-right',
thClass: 'gl-border-t-none!',
- columnClass: 'gl-w-10p',
},
],
components: {
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index d5bc428934c..a6d14bfbfd8 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -13,24 +13,17 @@ const InternalEvents = {
/**
*
* @param {string} event
- * @param {object} data
*/
- trackEvent(event, data = {}) {
- const { context, ...rest } = data;
-
- const defaultContext = {
- schema: SERVICE_PING_SCHEMA,
- data: {
- event_name: event,
- data_source: 'redis_hll',
- },
- };
- const mergedContext = context ? [defaultContext, context] : defaultContext;
-
+ trackEvent(event) {
API.trackInternalEvent(event);
Tracking.event(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
- context: mergedContext,
- ...rest,
+ context: {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
+ },
});
this.trackBrowserSDK(event);
},
@@ -41,8 +34,8 @@ const InternalEvents = {
mixin() {
return {
methods: {
- trackEvent(event, data = {}) {
- InternalEvents.trackEvent(event, data);
+ trackEvent(event) {
+ InternalEvents.trackEvent(event);
},
},
};
diff --git a/app/assets/javascripts/tracking/tracking.js b/app/assets/javascripts/tracking/tracking.js
index 923aea433f1..f4c8781ae20 100644
--- a/app/assets/javascripts/tracking/tracking.js
+++ b/app/assets/javascripts/tracking/tracking.js
@@ -13,12 +13,17 @@ const Tracking = Object.assign(Tracker, {
return {
computed: {
trackingCategory() {
- const localCategory = this.tracking ? this.tracking.category : null;
+ // TODO: refactor to remove potentially undefined property
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/432995
+ const localCategory = 'tracking' in this ? this.tracking.category : null;
return localCategory || opts.category;
},
trackingOptions() {
const options = addExperimentContext(opts);
- return { ...options, ...this.tracking };
+ // TODO: refactor to remove potentially undefined property
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/432995
+ const tracking = 'tracking' in this ? this.tracking : {};
+ return { ...options, ...tracking };
},
},
methods: {
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
index 9bf6d27235c..cd990ccc77a 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.stories.js
@@ -1,6 +1,6 @@
import { mockGetProjectStorageStatisticsGraphQLResponse } from 'jest/usage_quotas/storage/mock_data';
import createMockApollo from 'helpers/mock_apollo_helper';
-import getProjectStorageStatisticsQuery from '../queries/project_storage.query.graphql';
+import getProjectStorageStatisticsQuery from 'ee_else_ce/usage_quotas/storage/queries/project_storage.query.graphql';
import ProjectStorageApp from './project_storage_app.vue';
const meta = {
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
index a5e1cc398e3..cc4219c2ca9 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_app.vue
@@ -4,6 +4,9 @@ import { sprintf } from '~/locale';
import { updateRepositorySize } from '~/api/projects_api';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import SectionedPercentageBar from '~/usage_quotas/components/sectioned_percentage_bar.vue';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import getCostFactoredProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql';
+import getProjectStorageStatistics from 'ee_else_ce/usage_quotas/storage/queries/project_storage.query.graphql';
import {
ERROR_MESSAGE,
LEARN_MORE_LABEL,
@@ -18,7 +21,6 @@ import {
usageQuotasHelpPaths,
storageTypeHelpPaths,
} from '../constants';
-import getProjectStorageStatistics from '../queries/project_storage.query.graphql';
import { getStorageTypesFromProjectStatistics, descendingStorageUsageSort } from '../utils';
import ProjectStorageDetail from './project_storage_detail.vue';
@@ -32,10 +34,15 @@ export default {
ProjectStorageDetail,
SectionedPercentageBar,
},
+ mixins: [glFeatureFlagsMixin()],
inject: ['projectPath'],
apollo: {
project: {
- query: getProjectStorageStatistics,
+ query() {
+ return this.glFeatures?.displayCostFactoredStorageSizeOnProjectPages
+ ? getCostFactoredProjectStorageStatistics
+ : getProjectStorageStatistics;
+ },
variables() {
return {
fullPath: this.projectPath,
diff --git a/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue
index 6cc1f63e04f..35e43c76310 100644
--- a/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue
+++ b/app/assets/javascripts/usage_quotas/storage/components/project_storage_detail.vue
@@ -1,6 +1,6 @@
<script>
import { GlIcon, GlLink, GlSprintf, GlTableLite, GlPopover } from '@gitlab/ui';
-import { numberToHumanSize } from '~/lib/utils/number_utils';
+import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue';
import { thWidthPercent } from '~/lib/utils/table_utility';
import { sprintf } from '~/locale';
import {
@@ -19,6 +19,7 @@ export default {
GlSprintf,
StorageTypeIcon,
GlPopover,
+ NumberToHumanSize,
},
props: {
storageTypes: {
@@ -32,7 +33,6 @@ export default {
linkTitle,
});
},
- numberToHumanSize,
},
projectTableFields: [
{
@@ -92,9 +92,7 @@ export default {
</template>
<template #cell(value)="{ item }">
- <span :data-testid="item.id + '-value'">
- {{ numberToHumanSize(item.value, 1) }}
- </span>
+ <number-to-human-size :value="item.value" :data-testid="item.id + '-value'" />
<template v-if="item.warning">
<gl-icon
diff --git a/app/assets/javascripts/usage_quotas/storage/constants.js b/app/assets/javascripts/usage_quotas/storage/constants.js
index 3fdf61a5947..ac447fc96d1 100644
--- a/app/assets/javascripts/usage_quotas/storage/constants.js
+++ b/app/assets/javascripts/usage_quotas/storage/constants.js
@@ -31,6 +31,9 @@ export const PROJECT_TABLE_LABEL_STORAGE_TYPE = s__('UsageQuota|Storage type');
export const PROJECT_TABLE_LABEL_USAGE = s__('UsageQuota|Usage');
export const usageQuotasHelpPaths = {
+ repositorySizeLimit: helpPagePath('administration/settings/account_and_limit_settings', {
+ anchor: 'repository-size-limit',
+ }),
usageQuotas: helpPagePath('user/usage_quotas'),
usageQuotasProjectStorageLimit: helpPagePath('user/usage_quotas', {
anchor: 'project-storage-limit',
diff --git a/app/assets/javascripts/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql b/app/assets/javascripts/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql
new file mode 100644
index 00000000000..4438ad4cc3d
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/queries/cost_factored_project_storage.query.graphql
@@ -0,0 +1,23 @@
+query getCostFactoredProjectStorageStatistics($fullPath: ID!) {
+ project(fullPath: $fullPath) {
+ id
+ statisticsDetailsPaths {
+ containerRegistry
+ buildArtifacts
+ packages
+ repository
+ snippets
+ wiki
+ }
+ statistics {
+ containerRegistrySize
+ buildArtifactsSize
+ lfsObjectsSize
+ packagesSize
+ repositorySize
+ snippetsSize
+ storageSize
+ wikiSize
+ }
+ }
+}
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 524f2c045e6..6a1f5f0bb44 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
@@ -1,7 +1,7 @@
<script>
-import { GlButton, GlSprintf } from '@gitlab/ui';
+import { GlForm, GlButton, GlSprintf } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { visitUrl } from '~/lib/utils/url_utility';
+import csrf from '~/lib/utils/csrf';
import { STATUS_MERGED } from '~/issues/constants';
import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import { HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
@@ -23,7 +23,9 @@ export default {
StateContainer,
GlButton,
GlSprintf,
+ GlForm,
},
+ csrf,
mixins: [approvalsMixin, glFeatureFlagsMixin()],
props: {
mr: {
@@ -169,16 +171,15 @@ export default {
.join(', ')
.concat('.');
},
+ samlApprovalPath() {
+ return this.mr.samlApprovalPath;
+ },
requireSamlAuthToApprove() {
return this.mr.requireSamlAuthToApprove;
},
},
methods: {
approve() {
- if (this.requireSamlAuthToApprove) {
- this.approveWithSamlAuth();
- return;
- }
if (this.requirePasswordToApprove) {
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
return;
@@ -195,7 +196,7 @@ export default {
},
approveWithSamlAuth() {
// Intentionally direct to SAML Identity Provider for renewed authorization even if SSO session exists
- visitUrl(this.mr.samlApprovalPath);
+ this.$refs.form.$el.submit();
},
approveWithAuth(data) {
this.updateApproval(
@@ -270,17 +271,40 @@ export default {
<template v-else>
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-flex-direction-row gl-align-items-center">
- <gl-button
- v-if="action"
- :variant="action.variant"
- :category="action.category"
- :loading="isApproving"
- class="gl-mr-3"
- data-testid="approve-button"
- @click="action.action"
- >
- {{ action.text }}
- </gl-button>
+ <div v-if="requireSamlAuthToApprove && showApprove">
+ <gl-form
+ ref="form"
+ :action="samlApprovalPath"
+ method="post"
+ data-testid="approve-form"
+ >
+ <gl-button
+ v-if="action"
+ :variant="action.variant"
+ :category="action.category"
+ :loading="isApproving"
+ class="gl-mr-3"
+ data-testid="approve-button"
+ type="submit"
+ >
+ {{ action.text }}
+ </gl-button>
+ <input :value="$options.csrf.token" type="hidden" name="authenticity_token" />
+ </gl-form>
+ </div>
+ <span v-if="!requireSamlAuthToApprove || showUnapprove">
+ <gl-button
+ v-if="action"
+ :variant="action.variant"
+ :category="action.category"
+ :loading="isApproving"
+ class="gl-mr-3"
+ data-testid="approve-button"
+ @click="action.action"
+ >
+ {{ action.text }}
+ </gl-button>
+ </span>
<approvals-summary-optional
v-if="isOptional"
:can-approve="hasAction"
@@ -293,7 +317,7 @@ export default {
:multiple-approval-rules-available="mr.multipleApprovalRulesAvailable"
/>
</div>
- <div v-if="hasInvalidRules" class="gl-text-gray-400 gl-mt-2" data-testid="invalid-rules">
+ <div v-if="hasInvalidRules" class="gl-text-secondary gl-mt-2" data-testid="invalid-rules">
<gl-sprintf :message="pluralizedRuleText">
<template #danger="{ content }">
<span class="gl-font-weight-bold text-danger">{{ content }}</span>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
index 431348e1d57..24bc7017e06 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/constants.js
@@ -1,6 +1,7 @@
export const COMPONENTS = {
conflict: () => import('./conflicts.vue'),
- unresolved_discussions: () => import('./unresolved_discussions.vue'),
+ discussions_not_resolved: () => import('./unresolved_discussions.vue'),
+ draft_status: () => import('./draft.vue'),
need_rebase: () => import('./rebase.vue'),
default: () => import('./message.vue'),
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
new file mode 100644
index 00000000000..537c975652f
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.stories.js
@@ -0,0 +1,74 @@
+import createMockApollo from 'helpers/mock_apollo_helper';
+import draftStateQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
+import Draft from './draft.vue';
+
+const defaultRender = ({ apolloProvider, check, mr }) => ({
+ components: { Draft },
+ apolloProvider,
+ data() {
+ return { mr, check };
+ },
+ template: '<draft :check="check" :mr="mr" />',
+});
+
+const Template = ({ userPermissionUpdateMergeRequest }) => {
+ const requestHandlers = [
+ [
+ draftStateQuery,
+ () =>
+ Promise.resolve({
+ data: {
+ project: {
+ id: '1',
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ userPermissions: {
+ updateMergeRequest: userPermissionUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ }),
+ ],
+ [
+ removeDraftMutation,
+ () =>
+ Promise.resolve({
+ data: {
+ mergeRequestSetDraft: {
+ mergeRequest: {
+ draft: false,
+ id: '2',
+ title: 'MR title',
+ mergeableDiscussionsState: true,
+ },
+ errors: [],
+ },
+ },
+ }),
+ ],
+ ];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return defaultRender({
+ apolloProvider,
+ check: {
+ identifier: 'draft_status',
+ status: 'FAILED',
+ },
+ });
+};
+
+export const Default = Template.bind({});
+Default.args = {
+ userPermissionUpdateMergeRequest: true,
+};
+
+export default {
+ title: 'vue_merge_request_widget/merge_checks/draft',
+ component: Draft,
+};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
new file mode 100644
index 00000000000..dbe0d2ac243
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/draft.vue
@@ -0,0 +1,169 @@
+<script>
+import { produce } from 'immer';
+
+import { createAlert } from '~/alert';
+import MergeRequest from '~/merge_request';
+
+import mergeRequestQueryVariablesMixin from '../../mixins/merge_request_query_variables';
+
+import draftStateQuery from '../../queries/states/draft.query.graphql';
+import removeDraftMutation from '../../queries/toggle_draft.mutation.graphql';
+
+import ActionButtons from '../action_buttons.vue';
+import MergeChecksMessage from './message.vue';
+
+import { DRAFT_CHECK_READY, DRAFT_CHECK_ERROR } from './i18n';
+
+export default {
+ name: 'MergeChecksDraft',
+ components: {
+ MergeChecksMessage,
+ ActionButtons,
+ },
+ mixins: [mergeRequestQueryVariablesMixin],
+ apollo: {
+ state: {
+ query: draftStateQuery,
+ variables() {
+ return this.mergeRequestQueryVariables;
+ },
+ update: (data) => data?.project?.mergeRequest,
+ },
+ },
+ props: {
+ mr: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ check: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ state: {},
+ isMutating: false,
+ };
+ },
+ computed: {
+ networking() {
+ return this.isLoading || this.isMutating;
+ },
+ isLoading() {
+ return this.$apollo.queries.state.loading;
+ },
+ userCanUpdateMergeRequest() {
+ return this.state.userPermissions.updateMergeRequest;
+ },
+ showTertiaryButton() {
+ return !this.networking && this.userCanUpdateMergeRequest;
+ },
+ tertiaryActionsButtons() {
+ return [
+ {
+ text: DRAFT_CHECK_READY,
+ category: 'default',
+ testId: 'mark-as-ready-button',
+ onClick: () => this.removeDraft(),
+ },
+ ];
+ },
+ },
+ methods: {
+ removeDraft() {
+ const { mergeRequestQueryVariables } = this;
+
+ this.isMutating = true;
+
+ this.$apollo
+ .mutate({
+ mutation: removeDraftMutation,
+ variables: {
+ ...mergeRequestQueryVariables,
+ draft: false,
+ },
+ update(
+ store,
+ {
+ data: {
+ mergeRequestSetDraft: {
+ errors,
+ mergeRequest: { mergeableDiscussionsState, draft, title },
+ },
+ },
+ },
+ ) {
+ if (errors?.length) {
+ createAlert({
+ message: DRAFT_CHECK_ERROR,
+ });
+
+ return;
+ }
+
+ const sourceData = store.readQuery({
+ query: draftStateQuery,
+ variables: mergeRequestQueryVariables,
+ });
+
+ const data = produce(sourceData, (draftState) => {
+ draftState.project.mergeRequest.mergeableDiscussionsState = mergeableDiscussionsState;
+ draftState.project.mergeRequest.draft = draft;
+ draftState.project.mergeRequest.title = title;
+ });
+
+ store.writeQuery({
+ query: draftStateQuery,
+ data,
+ variables: mergeRequestQueryVariables,
+ });
+ },
+ optimisticResponse: {
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ __typename: 'Mutation',
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: this.mr.issuableId,
+ mergeableDiscussionsState: true,
+ title: this.mr.title,
+ draft: false,
+ },
+ },
+ },
+ })
+ .then(
+ ({
+ data: {
+ mergeRequestSetDraft: {
+ mergeRequest: { title },
+ },
+ },
+ }) => {
+ MergeRequest.toggleDraftStatus(title, true);
+ },
+ )
+ .catch(() =>
+ createAlert({
+ message: DRAFT_CHECK_ERROR,
+ }),
+ )
+ .finally(() => {
+ this.isMutating = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <merge-checks-message :check="check">
+ <template #failed>
+ <action-buttons v-if="showTertiaryButton" :tertiary-buttons="tertiaryActionsButtons" />
+ </template>
+ </merge-checks-message>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
new file mode 100644
index 00000000000..de504af5fcc
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/i18n.js
@@ -0,0 +1,4 @@
+import { __, s__ } from '~/locale';
+
+export const DRAFT_CHECK_ERROR = __('Something went wrong. Please try again.');
+export const DRAFT_CHECK_READY = s__('mrWidgetDraftCheck|Mark as ready');
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
index 058b9e1fe99..7f21445559a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/message.vue
@@ -8,7 +8,7 @@ const ICON_NAMES = {
success: 'success',
};
-const FAILURE_REASONS = {
+export const FAILURE_REASONS = {
broken_status: __('Cannot merge the source into the target branch, due to a conflict.'),
ci_must_pass: __('Pipeline must succeed.'),
conflict: __('Merge conflicts must be resolved.'),
@@ -20,6 +20,7 @@ const FAILURE_REASONS = {
policies_denied: __('Denied licenses must be removed or approved.'),
merge_request_blocked: __('Merge request is blocked by another merge request.'),
status_checks_must_pass: __('Status checks must pass.'),
+ jira_association_missing: __('Either the title or description must reference a Jira issue.'),
};
export default {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
index c0ac1818ffa..a4594409977 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.stories.js
@@ -63,7 +63,7 @@ const Template = ({
apolloProvider,
check: {
identifier: 'need_rebase',
- status: failed ? 'failed' : 'passed',
+ status: failed ? 'FAILED' : 'SUCCESS',
},
mr: { onlyAllowMergeIfPipelineSucceeds },
canCreatePipelineInTargetProject,
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
index 72140c22a89..63fa90fcc7a 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/checks/rebase.vue
@@ -65,8 +65,9 @@ export default {
},
showRebaseWithoutPipeline() {
return (
- !this.mr.onlyAllowMergeIfPipelineSucceeds ||
- (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline)
+ this.state.userPermissions.pushToSourceBranch &&
+ (!this.mr.onlyAllowMergeIfPipelineSucceeds ||
+ (this.mr.onlyAllowMergeIfPipelineSucceeds && this.mr.allowMergeOnSkippedPipeline))
);
},
isForkMergeRequest() {
@@ -85,10 +86,8 @@ export default {
);
},
tertiaryActionsButtons() {
- if (this.check.result === 'success') return [];
-
return [
- {
+ this.state.userPermissions.pushToSourceBranch && {
text: s__('mrWidget|Rebase'),
loading: this.isMakingRequest || this.rebaseInProgress,
testId: 'standard-rebase-button',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
index 1e5f91e12cf..d4c00aa86e3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/extensions/status_icon.vue
@@ -66,7 +66,7 @@ export default {
:name="$options.EXTENSION_ICON_NAMES[iconName]"
:size="size"
:aria-label="iconAriaLabel"
- :data-qa-selector="`status_${iconName}_icon`"
+ :data-testid="`status-${iconName}-icon`"
class="gl-display-block"
/>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
index 77dc5b1d0da..a1171fe5d25 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.stories.js
@@ -41,6 +41,10 @@ const Template = ({ canMerge, failed, pushToSourceBranch }) => {
identifier: 'CONFLICT',
status: failed ? 'FAILED' : 'SUCCESS',
},
+ {
+ identifier: 'DRAFT_STATUS',
+ status: failed ? 'FAILED' : 'SUCCESS',
+ },
],
},
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
index ac403c2c6f2..750f53a29b6 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/merge_checks.vue
@@ -1,9 +1,12 @@
<script>
import { GlSkeletonLoader } from '@gitlab/ui';
import { __, n__, sprintf } from '~/locale';
+import { TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
import { COMPONENTS } from '~/vue_merge_request_widget/components/checks/constants';
import mergeRequestQueryVariablesMixin from '../mixins/merge_request_query_variables';
import mergeChecksQuery from '../queries/merge_checks.query.graphql';
+import mergeChecksSubscription from '../queries/merge_checks.subscription.graphql';
import StateContainer from './state_container.vue';
import BoldText from './bold_text.vue';
@@ -18,6 +21,31 @@ export default {
return this.mergeRequestQueryVariables;
},
update: (data) => data?.project?.mergeRequest,
+ subscribeToMore: {
+ document() {
+ return mergeChecksSubscription;
+ },
+ skip() {
+ return !this.mr?.id;
+ },
+ variables() {
+ return {
+ issuableId: convertToGraphQLId(TYPENAME_MERGE_REQUEST, this.mr?.id),
+ };
+ },
+ updateQuery(
+ _,
+ {
+ subscriptionData: {
+ data: { mergeRequestMergeStatusUpdated },
+ },
+ },
+ ) {
+ if (mergeRequestMergeStatusUpdated) {
+ this.state = mergeRequestMergeStatusUpdated;
+ }
+ },
+ },
},
},
components: {
@@ -86,7 +114,7 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-rounded-0!">
<state-container
:is-loading="isLoading"
:status="statusIcon"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index efc74241941..e19617b2e28 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -10,7 +10,7 @@ import {
} from '@gitlab/ui';
import SafeHtml from '~/vue_shared/directives/safe_html';
import { s__, n__ } from '~/locale';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon/ci_icon.vue';
import { keepLatestDownstreamPipelines } from '~/ci/pipeline_details/utils/parsing_utils';
import PipelineArtifacts from '~/ci/pipelines_page/components/pipelines_artifacts.vue';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
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 370e07b397c..d0771a79a5d 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,6 +1,6 @@
<script>
import { GlIcon } from '@gitlab/ui';
-import { STATUS_CLOSED, STATUS_MERGED } from '~/issues/constants';
+import { STATUS_CLOSED, STATUS_MERGED, STATUS_EMPTY } from '~/issues/constants';
import StatusIcon from './extensions/status_icon.vue';
export default {
@@ -24,6 +24,9 @@ export default {
isMerged() {
return this.status === STATUS_MERGED;
},
+ isEmpty() {
+ return this.status === STATUS_EMPTY;
+ },
},
};
</script>
@@ -33,6 +36,7 @@ export default {
<gl-icon v-if="isMerged" name="merge" :size="16" class="gl-text-blue-500" />
<gl-icon v-else-if="isClosed" name="merge-request-close" :size="16" class="gl-text-red-500" />
<gl-icon v-else-if="status === 'approval'" name="approval" :size="16" />
+ <status-icon v-else-if="isEmpty" icon-name="neutral" :level="1" class="gl-m-0!" />
<status-icon v-else :is-loading="isLoading" :icon-name="status" :level="1" class="gl-m-0!" />
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_conflicts.vue
index 55ae390216d..d2c1c914028 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
@@ -22,14 +22,14 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- update: (data) => data.project.mergeRequest.userPermissions,
+ update: (data) => data.project?.mergeRequest.userPermissions || {},
},
state: {
query: conflictsStateQuery,
variables() {
return this.mergeRequestQueryVariables;
},
- update: (data) => data.project.mergeRequest,
+ update: (data) => data.project?.mergeRequest || {},
},
},
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
index 122abc7d034..8fb2b6acc4d 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue
@@ -50,7 +50,6 @@ export default {
{
text: s__('mrWidget|Refresh now'),
onClick: () => this.refresh(),
- testId: 'merge-request-failed-refresh-button',
},
];
},
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
index 9258bc39bcb..2c5f6b9a3ec 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merging.vue
@@ -1,9 +1,9 @@
<script>
-import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { STATUS_MERGED } from '~/issues/constants';
import simplePoll from '~/lib/utils/simple_poll';
import MergeRequest from '~/merge_request';
import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
+import { fetchUserCounts } from '~/super_sidebar/user_counts_fetch';
import eventHub from '../../event_hub';
import { MERGE_ACTIVE_STATUS_PHRASES, STATE_MACHINE } from '../../constants';
import StatusIcon from '../mr_widget_status_icon.vue';
@@ -58,7 +58,7 @@ export default {
MergeRequest.decreaseCounter();
stopPolling();
- refreshUserMergeRequestCounts();
+ fetchUserCounts();
// If user checked remove source branch and we didn't remove the branch yet
// we should start another polling for source branch remove process
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
index 2db5c71be82..16f1bac73ab 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/new_ready_to_merge.vue
@@ -37,7 +37,7 @@ export default {
<template>
<div class="mr-widget-body media">
<status-icon status="success" />
- <p class="media-body gl-m-0! gl-font-weight-bold gl-text-gray-900!">
+ <p class="media-body gl-mt-1 gl-mb-0! gl-font-weight-bold gl-text-gray-900!">
<template v-if="canMerge">
{{ __('Ready to merge!') }}
</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 e1c54a8827c..b80b5246e24 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/nothing_to_merge.vue
@@ -1,48 +1,48 @@
<script>
import { GlSprintf, GlLink } from '@gitlab/ui';
-import EMPTY_STATE_SVG_URL from '@gitlab/svgs/dist/illustrations/empty-state/empty-merge-requests-md.svg?url';
+import { STATUS_EMPTY } from '~/issues/constants';
import { helpPagePath } from '~/helpers/help_page_helper';
+import StatusIcon from '../mr_widget_status_icon.vue';
export default {
name: 'MRWidgetNothingToMerge',
components: {
GlSprintf,
GlLink,
+ StatusIcon,
+ },
+ computed: {
+ statusEmpty() {
+ return STATUS_EMPTY;
+ },
},
ciHelpPage: helpPagePath('ci/quick_start/index.html'),
- EMPTY_STATE_SVG_URL,
};
</script>
<template>
- <div class="mr-widget-body mr-widget-empty-state">
- <div class="row">
- <div
- class="col-md-3 col-12 text-center d-flex justify-content-center align-items-center svg-content svg-130 pb-0 pt-0"
- >
- <img :src="$options.EMPTY_STATE_SVG_URL" :alt="''" />
- </div>
- <div class="text col-md-9 col-12">
- <p class="highlight mt-3">
- {{ s__('mrWidgetNothingToMerge|Merge request contains no changes') }}
- </p>
- <p data-testid="nothing-to-merge-body">
- <gl-sprintf
- :message="
- s__(
- 'mrWidgetNothingToMerge|Use merge requests to propose changes to your project and discuss them with your team. To make changes, use the %{boldStart}Code%{boldEnd} dropdown list above, then test them with %{linkStart}CI/CD%{linkEnd} before merging.',
- )
- "
- >
- <template #bold="{ content }">
- <b>{{ content }}</b>
- </template>
- <template #link="{ content }">
- <gl-link :href="$options.ciHelpPage" target="_blank">{{ content }}</gl-link>
- </template>
- </gl-sprintf>
- </p>
- </div>
+ <div class="mr-widget-body media">
+ <status-icon :status="statusEmpty" />
+ <div>
+ <p class="media-body gl-mt-1 gl-mb-1 gl-font-weight-bold gl-text-gray-900!">
+ {{ s__('mrWidgetNothingToMerge|Merge request contains no changes') }}
+ </p>
+ <p class="gl-m-0! gl-text-secondary" data-testid="nothing-to-merge-body">
+ <gl-sprintf
+ :message="
+ s__(
+ 'mrWidgetNothingToMerge|Use merge requests to propose changes to your project and discuss them with your team. To make changes, use the %{boldStart}Code%{boldEnd} dropdown list above, then test them with %{linkStart}CI/CD%{linkEnd} before merging.',
+ )
+ "
+ >
+ <template #bold="{ content }">
+ <b>{{ content }}</b>
+ </template>
+ <template #link="{ content }">
+ <gl-link :href="$options.ciHelpPage" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </p>
</div>
</div>
</template>
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 3c2d8efaffc..1516b63f96d 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
@@ -59,6 +59,7 @@ export default {
apollo: {
state: {
query: readyToMergeQuery,
+ fetchPolicy: fetchPolicies.NO_CACHE,
variables() {
return this.mergeRequestQueryVariables;
},
@@ -119,6 +120,14 @@ export default {
) {
if (mergeRequestMergeStatusUpdated) {
this.state = mergeRequestMergeStatusUpdated;
+
+ if (!this.commitMessageIsTouched) {
+ this.commitMessage = mergeRequestMergeStatusUpdated.defaultMergeCommitMessage;
+ }
+
+ if (!this.squashCommitMessageIsTouched) {
+ this.squashCommitMessage = mergeRequestMergeStatusUpdated.defaultSquashCommitMessage;
+ }
}
},
},
@@ -349,12 +358,6 @@ export default {
eventHub.$on('ApprovalUpdated', this.updateGraphqlState);
eventHub.$on('MRWidgetUpdateRequested', this.updateGraphqlState);
eventHub.$on('mr.discussion.updated', this.updateGraphqlState);
-
- if (this.glFeatures.widgetPipelinePassSubscriptionUpdate) {
- this.$apollo.queries.state.setOptions({
- fetchPolicy: fetchPolicies.NO_CACHE,
- });
- }
},
beforeDestroy() {
eventHub.$off('ApprovalUpdated', this.updateGraphqlState);
@@ -610,6 +613,7 @@ export default {
:label="__('Merge commit message')"
input-id="merge-message-edit"
class="gl-m-0! gl-p-0!"
+ data-testid="merge-commit-message"
@input="setCommitMessage"
>
<template #header>
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 267facb0a50..4a23f8847d3 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
@@ -29,7 +29,7 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- update: (data) => data.project.mergeRequest.userPermissions,
+ update: (data) => data.project?.mergeRequest?.userPermissions || {},
},
},
props: {
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
index d4375690ad1..cbc7b91922b 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/dynamic_content.vue
@@ -72,10 +72,14 @@ export default {
:widget-name="widgetName"
:header="data.header"
:help-popover="data.helpPopover"
- :class="{ 'gl-border-top-0': rowIndex === 0 }"
+ :class="{
+ 'gl-border-top-0': rowIndex === 0,
+ 'gl-align-items-start': data.supportingText,
+ 'gl-align-items-baseline': !data.supportingText,
+ }"
>
<template #body>
- <div class="gl-w-full gl-display-flex" :class="{ 'gl-flex-direction-column': level === 1 }">
+ <div class="gl-w-full gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1 gl-align-items-baseline">
<div>
@@ -109,7 +113,6 @@ export default {
:data="childData"
:widget-name="widgetName"
:level="3"
- data-qa-selector="child_content"
@clickedAction="onClickedAction"
/>
</li>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
index 4e8098677cc..e1378e78df8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/status_icon.vue
@@ -58,7 +58,7 @@ export default {
:name="$options.EXTENSION_ICON_NAMES[iconName]"
:size="12"
:aria-label="iconAriaLabel"
- :data-qa-selector="`status_${iconName}_icon`"
+ :data-testid="`status-${iconName}-icon`"
class="gl-relative gl-z-index-1"
/>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
index 0eb50b9ff4f..d85ba5374d3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget.vue
@@ -19,7 +19,7 @@ import ActionButtons from './action_buttons.vue';
const WIDGET_PREFIX = 'Widget';
const MISSING_RESPONSE_HEADERS =
- 'MR Widget: raesponse object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.';
+ 'MR Widget: response object should contain status and headers object. Make sure to include that in your `fetchCollapsedData` and `fetchExpandedData` functions.';
const LOADING_STATE_COLLAPSED = 'collapsed';
const LOADING_STATE_EXPANDED = 'expanded';
@@ -386,7 +386,6 @@ export default {
category="tertiary"
data-testid="toggle-button"
size="small"
- data-qa-selector="expand_report_button"
@click="toggleCollapsed"
/>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
index bb82da7796a..7413e2237c3 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue
@@ -81,8 +81,7 @@ export default {
<div
class="gl-display-flex"
:class="{
- 'gl-border-t gl-py-3 gl-pl-7 gl-align-items-baseline': level === 2,
- 'gl-align-items-center': level === 3,
+ 'gl-border-t gl-py-3 gl-pl-7': level === 2,
}"
>
<status-icon
diff --git a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js b/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
deleted file mode 100644
index 3af984dcf6c..00000000000
--- a/app/assets/javascripts/vue_merge_request_widget/extensions/code_quality/index.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
-import { SEVERITY_ICONS_MR_WIDGET } from '~/ci/reports/codequality_report/constants';
-import { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
-import { parseCodeclimateMetrics } from '~/ci/reports/codequality_report/store/utils/codequality_parser';
-import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
-import { i18n, codeQualityPrefixes } from './constants';
-
-export default {
- name: 'WidgetCodeQuality',
- enablePolling: true,
- props: ['codeQuality', 'blobPath'],
- i18n,
- computed: {
- shouldCollapse(data) {
- const { newErrors, resolvedErrors, parsingInProgress } = data;
- if (parsingInProgress || (newErrors.length === 0 && resolvedErrors.length === 0)) {
- return false;
- }
- return true;
- },
- summary(data) {
- const { newErrors, resolvedErrors, parsingInProgress } = data;
- if (parsingInProgress) {
- return i18n.loading;
- }
- if (newErrors.length >= 1 && resolvedErrors.length >= 1) {
- return i18n.improvementAndDegradationCopy(
- i18n.findings(resolvedErrors, codeQualityPrefixes.fixed),
- i18n.findings(newErrors, codeQualityPrefixes.new),
- );
- }
- if (resolvedErrors.length >= 1) {
- return i18n.singularCopy(i18n.findings(resolvedErrors, codeQualityPrefixes.fixed));
- }
- if (newErrors.length >= 1) {
- return i18n.singularCopy(i18n.findings(newErrors, codeQualityPrefixes.new));
- }
- return i18n.noChanges;
- },
- statusIcon() {
- if (this.collapsedData.newErrors.length >= 1) {
- return EXTENSION_ICONS.warning;
- }
- if (this.collapsedData.resolvedErrors.length >= 1) {
- return EXTENSION_ICONS.success;
- }
- return EXTENSION_ICONS.neutral;
- },
- },
- methods: {
- fetchCollapsedData() {
- return axios.get(this.codeQuality).then((response) => {
- const { data = {}, status } = response;
- return {
- ...response,
- data: {
- parsingInProgress: status === HTTP_STATUS_NO_CONTENT,
- resolvedErrors: parseCodeclimateMetrics(data.resolved_errors, this.blobPath.head_path),
- newErrors: parseCodeclimateMetrics(data.new_errors, this.blobPath.head_path),
- },
- };
- });
- },
- fetchFullData() {
- const fullData = [];
-
- this.collapsedData.newErrors.map((e) => {
- return fullData.push({
- text: e.check_name
- ? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
- : `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
- subtext: {
- prependText: i18n.prependText,
- text: `${e.file_path}:${e.line}`,
- href: e.urlPath,
- },
- icon: {
- name: SEVERITY_ICONS_MR_WIDGET[e.severity],
- },
- });
- });
-
- this.collapsedData.resolvedErrors.map((e) => {
- return fullData.push({
- text: e.check_name
- ? `${capitalizeFirstCharacter(e.severity)} - ${e.check_name} - ${e.description}`
- : `${capitalizeFirstCharacter(e.severity)} - ${e.description}`,
- subtext: {
- prependText: i18n.prependText,
- text: `${e.file_path}:${e.line}`,
- href: e.urlPath,
- },
- icon: {
- name: SEVERITY_ICONS_MR_WIDGET[e.severity],
- },
- badge: {
- variant: 'neutral',
- text: i18n.fixed,
- },
- });
- });
-
- return Promise.resolve(fullData);
- },
- fetchReport(endpoint) {
- return axios.get(endpoint).then((res) => res.data);
- },
- },
-};
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 cc116b42f1e..e2f301b1911 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
@@ -54,6 +54,7 @@ import getStateQuery from './queries/get_state.query.graphql';
import getStateSubscription from './queries/get_state.subscription.graphql';
import ReportWidgetContainer from './components/report_widget_container.vue';
import MrWidgetReadyToMerge from './components/states/new_ready_to_merge.vue';
+import MergeChecks from './components/merge_checks.vue';
export default {
// False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/25
@@ -94,6 +95,7 @@ export default {
MergeChecksFailed: () => import('./components/states/merge_checks_failed.vue'),
ReadyToMerge: ReadyToMergeState,
ReportWidgetContainer,
+ MergeChecks,
},
apollo: {
state: {
@@ -248,6 +250,25 @@ export default {
hasExtensions() {
return registeredExtensions.extensions.length;
},
+ mergeBlockedComponentEnabled() {
+ return (
+ window.gon?.features?.mergeBlockedComponent &&
+ !(
+ [
+ 'checking',
+ 'preparing',
+ 'nothingToMerge',
+ 'archived',
+ 'missingBranch',
+ 'merged',
+ 'closed',
+ 'merging',
+ 'autoMergeEnabled',
+ 'shaMismatch',
+ ].includes(this.mr.state) || ['MERGING', 'AUTO_MERGE'].includes(this.mr.machineValue)
+ )
+ );
+ },
},
watch: {
'mr.machineValue': {
@@ -556,7 +577,8 @@ export default {
</div>
<div class="mr-widget-section" data-testid="mr-widget-content">
- <component :is="componentName" :mr="mr" :service="service" />
+ <merge-checks v-if="mergeBlockedComponentEnabled" :mr="mr" :service="service" />
+ <component :is="componentName" v-else :mr="mr" :service="service" />
<ready-to-merge
v-if="mr.commitsCount"
v-show="shouldShowMergeDetails"
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.subscription.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.subscription.graphql
new file mode 100644
index 00000000000..9cf2b9be405
--- /dev/null
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/merge_checks.subscription.graphql
@@ -0,0 +1,14 @@
+subscription mergeChecksSubscrption($issuableId: IssuableID!) {
+ mergeRequestMergeStatusUpdated(issuableId: $issuableId) {
+ ... on MergeRequest {
+ id
+ userPermissions {
+ canMerge
+ }
+ mergeabilityChecks {
+ identifier
+ status
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
index 54f2233439f..c1190a07ef8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
+++ b/app/assets/javascripts/vue_merge_request_widget/queries/states/draft.query.graphql
@@ -2,7 +2,10 @@ query mrUserPermission($projectPath: ID!, $iid: String!) {
project(fullPath: $projectPath) {
id
mergeRequest(iid: $iid) {
+ draft
id
+ mergeableDiscussionsState
+ title
userPermissions {
updateMergeRequest
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index d6bab074f3f..5765d7a56fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -15,25 +15,25 @@ export default function deviseState() {
return stateKey.missingBranch;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CHECKING) {
- return stateKey.checking;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.checking;
}
if (this.hasConflicts) {
- return stateKey.conflicts;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.conflicts;
}
if (this.shouldBeRebased) {
- return stateKey.rebase;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.rebase;
}
if (this.hasMergeChecksFailed && !this.autoMergeEnabled) {
- return stateKey.mergeChecksFailed;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.mergeChecksFailed;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.CI_MUST_PASS) {
- return stateKey.pipelineFailed;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.pipelineFailed;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DRAFT_STATUS) {
- return stateKey.draft;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.draft;
}
if (this.detailedMergeStatus === DETAILED_MERGE_STATUS.DISCUSSIONS_NOT_RESOLVED) {
- return stateKey.unresolvedDiscussions;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.unresolvedDiscussions;
}
if (this.canMerge && this.isSHAMismatch) {
return stateKey.shaMismatch;
@@ -47,5 +47,5 @@ export default function deviseState() {
) {
return stateKey.readyToMerge;
}
- return stateKey.checking;
+ return window.gon?.features?.mergeBlockedComponent ? null : stateKey.checking;
}
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 a1b86c86979..9ce5448d86e 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js
@@ -30,7 +30,8 @@ export default class MergeRequestStore {
this.stateMachine = machine(STATE_MACHINE.definition);
this.machineValue = this.stateMachine.value;
- this.mergeDetailsCollapsed = window.innerWidth < 768;
+ this.mergeDetailsCollapsed =
+ !window.gon?.features?.mergeBlockedComponent && window.innerWidth < 768;
this.mergeError = data.mergeError;
this.multipleApprovalRulesAvailable = data.multiple_approval_rules_available || false;
this.id = data.id;
@@ -121,6 +122,7 @@ export default class MergeRequestStore {
this.availableAutoMergeStrategies,
);
this.ffOnlyEnabled = data.ff_only_enabled;
+ this.ffMergePossible = data.ff_merge_possible;
this.isRemovingSourceBranch = this.isRemovingSourceBranch || false;
this.mergeRequestState = data.state;
this.isOpen = this.mergeRequestState === STATUS_OPEN;
@@ -196,7 +198,9 @@ export default class MergeRequestStore {
}
this.commitsCount = mergeRequest.commitCount;
- this.branchMissing = !mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists;
+ this.branchMissing =
+ mergeRequest.detailedMergeStatus !== 'NOT_OPEN' &&
+ (!mergeRequest.sourceBranchExists || !mergeRequest.targetBranchExists);
this.hasConflicts = mergeRequest.conflicts;
this.hasMergeableDiscussionsState = mergeRequest.mergeableDiscussionsState === false;
this.mergeError = mergeRequest.mergeError;
@@ -418,6 +422,10 @@ export default class MergeRequestStore {
}
toggleMergeDetails(val = !this.mergeDetailsCollapsed) {
+ if (window.gon?.features?.mergeBlockedComponent) {
+ return;
+ }
+
this.mergeDetailsCollapsed = val;
}
}
diff --git a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
index 93581dbbd40..655a16dea01 100644
--- a/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/alert_details/components/system_notes/system_note.vue
@@ -36,7 +36,7 @@ export default {
<li
:id="noteAnchorId"
class="timeline-entry note system-note note-wrapper gl-p-0!"
- data-qa-selector="alert_system_note_container"
+ data-testid="alert-system-note-container"
>
<div class="gl-display-inline-flex gl-align-items-center gl-relative">
<div
diff --git a/app/assets/javascripts/vue_shared/components/awards_list.vue b/app/assets/javascripts/vue_shared/components/awards_list.vue
index 59f03b41144..3c19df9c196 100644
--- a/app/assets/javascripts/vue_shared/components/awards_list.vue
+++ b/app/assets/javascripts/vue_shared/components/awards_list.vue
@@ -94,14 +94,12 @@ export default {
return awardList.some((award) => award.user.id === this.currentUserId);
},
createAwardList(name, list) {
- const url = list.length ? list[0].url : null;
-
return {
name,
list,
title: this.getAwardListTitle(list, name),
classes: this.getAwardClassBindings(list),
- html: glEmojiTag(name, { url }),
+ html: glEmojiTag(name),
};
},
getAwardListTitle(awardsList, name) {
diff --git a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
index 2a47e96b2e2..5a807d10f24 100644
--- a/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/changed_file_icon.vue
@@ -82,8 +82,6 @@ export default {
:title="tooltipTitle"
:class="{ 'ml-auto': isCentered }"
class="file-changed-icon d-inline-block"
- data-qa-selector="changed_file_icon_content"
- :data-qa-title="tooltipTitle"
>
<gl-icon v-if="showIcon" :name="changedIcon" :size="size" :class="changedIconClass" />
</span>
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js
new file mode 100644
index 00000000000..66012cefeaf
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.stories.js
@@ -0,0 +1,31 @@
+import CiIcon from './ci_icon.vue';
+
+export default {
+ component: CiIcon,
+ title: 'vue_shared/ci_icon',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { CiIcon },
+ props: Object.keys(argTypes),
+ template: '<ci-icon v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ status: {
+ icon: 'status_success',
+ text: 'Success',
+ detailsPath: 'https://gitab.com/',
+ },
+};
+
+export const WithText = Template.bind({});
+WithText.args = {
+ status: {
+ icon: 'status_success',
+ text: 'Success',
+ detailsPath: 'https://gitab.com/',
+ },
+ showStatusText: true,
+};
diff --git a/app/assets/javascripts/vue_shared/components/ci_icon.vue b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue
index a2b6b4642c9..a2b6b4642c9 100644
--- a/app/assets/javascripts/vue_shared/components/ci_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue
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 2bdc8a174d0..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,11 +36,6 @@ export default {
required: false,
default: 'confirm-danger-button',
},
- buttonQaSelector: {
- type: String,
- required: false,
- default: null,
- },
buttonVariant: {
type: String,
required: false,
@@ -58,7 +53,6 @@ export default {
:variant="buttonVariant"
:disabled="disabled"
:data-testid="buttonTestid"
- :data-qa-selector="buttonQaSelector"
>{{ buttonText }}</gl-button
>
<confirm-danger-modal
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 a1ef1f30ebb..5019ab901fd 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
@@ -72,7 +72,7 @@ export default {
attributes: {
variant: 'danger',
disabled: !this.isValid,
- 'data-qa-selector': 'confirm_danger_modal_button',
+ 'data-testid': 'confirm-danger-modal-button',
},
};
},
@@ -133,8 +133,7 @@ export default {
id="confirm_name_input"
v-model="confirmationPhrase"
class="form-control"
- data-qa-selector="confirm_danger_field"
- data-testid="confirm-danger-input"
+ data-testid="confirm-danger-field"
type="text"
/>
</gl-form-group>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
index 1370f7b2a8c..7b9ecc18ce1 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/content_viewer.vue
@@ -54,7 +54,7 @@ export default {
</script>
<template>
- <div class="preview-container" data-qa-selector="preview_container">
+ <div class="preview-container">
<image-viewer v-if="type === 'image'" :path="path" :file-size="fileSize" />
<markdown-viewer
v-if="type === 'markdown'"
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
index f28a2801bc0..332424c70ac 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/download_viewer.vue
@@ -41,14 +41,7 @@ export default {
{{ fileName }}
<template v-if="fileSize > 0"> ({{ fileSizeReadable }}) </template>
</p>
- <a
- :href="path"
- class="btn btn-default"
- rel="nofollow"
- :download="fileName"
- target="_blank"
- data-qa-selector="download_button"
- >
+ <a :href="path" class="btn btn-default" rel="nofollow" :download="fileName" target="_blank">
<gl-icon :size="16" name="download" class="float-left gl-mr-3" />
{{ __('Download') }}
</a>
diff --git a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
index 04ab0fd00aa..9742118cd5f 100644
--- a/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
+++ b/app/assets/javascripts/vue_shared/components/content_viewer/viewers/image_viewer.vue
@@ -88,7 +88,7 @@ export default {
</script>
<template>
- <div data-testid="image-viewer" data-qa-selector="image_viewer_container">
+ <div data-testid="image-viewer">
<div :class="innerCssClasses" class="position-relative">
<img ref="contentImg" :src="safePath" @load="onImgLoad" />
<slot
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
index 1a215454ab6..ea787bfe63e 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/entity_select.vue
@@ -57,7 +57,7 @@ export default {
type: Function,
required: true,
},
- fetchInitialSelectionText: {
+ fetchInitialSelection: {
type: Function,
required: false,
default: null,
@@ -77,35 +77,23 @@ export default {
searchString: '',
items: [],
page: 1,
- selectedValue: null,
- selectedText: null,
+ selected: this.initialSelection || '',
+ initialSelectedItem: {},
errorMessage: '',
};
},
computed: {
- selected: {
- set(value) {
- this.selectedValue = value;
- this.selectedText =
- value === null ? null : this.items.find((item) => item.value === value).text;
- this.$emit('input', {
- value: this.selectedValue,
- text: this.selectedText,
- });
- },
- get() {
- return this.selectedValue;
- },
+ selectedItem() {
+ const item = this.items.find(({ value }) => value === this.selected);
+
+ return item || this.initialSelectedItem;
},
toggleText() {
- return this.selectedText ?? this.defaultToggleText;
+ return this.selectedItem?.text ?? this.defaultToggleText;
},
resetButtonLabel() {
return this.clearable ? RESET_LABEL : '';
},
- inputValue() {
- return this.selectedValue ? this.selectedValue : '';
- },
isSearchQueryTooShort() {
return this.searchString && this.searchString.length < MINIMUM_QUERY_LENGTH;
},
@@ -115,8 +103,13 @@ export default {
: this.$options.i18n.noResultsText;
},
},
+ watch: {
+ selected() {
+ this.$emit('input', this.selectedItem);
+ },
+ },
created() {
- this.fetchInitialSelection();
+ this.getInitialSelection();
},
methods: {
search: debounce(function debouncedSearch(searchString) {
@@ -148,23 +141,20 @@ export default {
this.searching = false;
this.infiniteScrollLoading = false;
},
- async fetchInitialSelection() {
+ async getInitialSelection() {
if (!this.initialSelection) {
this.pristine = false;
return;
}
- if (!this.fetchInitialSelectionText) {
+ if (!this.fetchInitialSelection) {
throw new Error(
'`initialSelection` is provided but lacks `fetchInitialSelectionText` to retrieve the corresponding text',
);
}
this.searching = true;
- const name = await this.fetchInitialSelectionText(this.initialSelection);
-
- this.selectedValue = this.initialSelection;
- this.selectedText = name;
+ this.initialSelectedItem = await this.fetchInitialSelection(this.initialSelection);
this.pristine = false;
this.searching = false;
},
@@ -218,6 +208,6 @@ export default {
<slot name="list-item" :item="item"></slot>
</template>
</gl-collapsible-listbox>
- <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="inputValue" />
+ <input :id="inputId" data-testid="input" type="hidden" :name="inputName" :value="selected" />
</gl-form-group>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
index 8a338551fbe..da42c017541 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/group_select.vue
@@ -76,11 +76,7 @@ export default {
try {
const url = groupsPath(this.groupsFilter, this.parentGroupID);
const { data = [], headers } = await axios.get(url, { params });
- groups = data.map((group) => ({
- ...group,
- text: group.full_name,
- value: String(group.id),
- }));
+ groups = data.map((group) => this.mapGroupData(group));
totalPages = parseIntPagination(normalizeHeaders(headers)).totalPages;
} catch (error) {
@@ -88,15 +84,19 @@ export default {
}
return { items: groups, totalPages };
},
- async fetchGroupName(groupId) {
- let groupName = '';
+ async fetchInitialGroup(groupId) {
try {
const group = await Api.group(groupId);
- groupName = group.full_name;
+
+ return this.mapGroupData(group);
} catch (error) {
this.handleError({ message: FETCH_GROUP_ERROR, error });
+
+ return {};
}
- return groupName;
+ },
+ mapGroupData(group) {
+ return { ...group, text: group.full_name, value: String(group.id) };
},
handleError({ message, error }) {
Sentry.captureException(error);
@@ -123,7 +123,7 @@ export default {
:header-text="$options.i18n.selectGroup"
:default-toggle-text="$options.i18n.toggleText"
:fetch-items="fetchGroups"
- :fetch-initial-selection-text="fetchGroupName"
+ :fetch-initial-selection="fetchInitialGroup"
v-on="$listeners"
>
<template #error>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
index d068d86d95b..9f4671abbb1 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/organization_select.vue
@@ -1,10 +1,11 @@
<script>
import { GlAlert } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import getCurrentUserOrganizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import getCurrentUserOrganizationsQuery from '~/organizations/shared/graphql/queries/organizations.query.graphql';
import getOrganizationQuery from '~/organizations/shared/graphql/queries/organization.query.graphql';
import { getIdFromGraphQLId, convertToGraphQLId } from '~/graphql_shared/utils';
-import { TYPENAME_ORGANIZATION } from '~/graphql_shared/constants';
+import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
+import { DEFAULT_PER_PAGE } from '~/api';
import {
ORGANIZATION_TOGGLE_TEXT,
ORGANIZATION_HEADER_TEXT,
@@ -62,54 +63,60 @@ export default {
data() {
return {
errorMessage: '',
+ endCursor: null,
};
},
methods: {
- async fetchOrganizations() {
+ async fetchOrganizations(search, page = 1) {
+ if (page === 1) {
+ this.endCursor = null;
+ }
+
try {
- const {
- data: {
- currentUser: {
- organizations: { nodes },
- },
- },
- } = await this.$apollo.query({
+ const response = await this.$apollo.query({
query: getCurrentUserOrganizationsQuery,
- // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
+ // TODO: implement search support - https://gitlab.com/gitlab-org/gitlab/-/issues/433954.
+ variables: { after: this.endCursor, first: DEFAULT_PER_PAGE },
});
+ const { nodes, pageInfo } = response.data.currentUser.organizations;
+ this.endCursor = pageInfo.endCursor;
return {
- items: nodes.map((organization) => ({
- text: organization.name,
- value: getIdFromGraphQLId(organization.id),
- })),
- // TODO: implement pagination - https://gitlab.com/gitlab-org/gitlab/-/issues/429999.
- totalPages: 1,
+ items: nodes.map((organization) => this.mapOrganizationData(organization)),
+ // `EntitySelect` expects a `totalPages` key but GraphQL requests don't provide this data
+ // because it uses keyset pagination. Since the dropdown uses infinite scroll it
+ // only needs to know if there is a next page. We pass `page + 1` if there is a next page,
+ // otherwise we just set this to the current page.
+ totalPages: pageInfo.hasNextPage ? page + 1 : page,
};
} catch (error) {
+ this.endCursor = null;
this.handleError({ message: FETCH_ORGANIZATIONS_ERROR, error });
return { items: [], totalPages: 0 };
}
},
- async fetchOrganizationName(id) {
+ async fetchInitialOrganization(id) {
try {
- const {
- data: {
- organization: { name },
- },
- } = await this.$apollo.query({
+ const response = await this.$apollo.query({
query: getOrganizationQuery,
- variables: { id: convertToGraphQLId(TYPENAME_ORGANIZATION, id) },
+ variables: { id: convertToGraphQLId(TYPE_ORGANIZATION, id) },
});
- return name;
+ return this.mapOrganizationData(response.data.organization);
} catch (error) {
this.handleError({ message: FETCH_ORGANIZATION_ERROR, error });
- return '';
+ return {};
}
},
+ mapOrganizationData(organization) {
+ return {
+ ...organization,
+ text: organization.name,
+ value: getIdFromGraphQLId(organization.id),
+ };
+ },
handleError({ message, error }) {
Sentry.captureException(error);
this.errorMessage = message;
@@ -137,7 +144,7 @@ export default {
:header-text="$options.i18n.selectGroup"
:default-toggle-text="$options.i18n.toggleText"
:fetch-items="fetchOrganizations"
- :fetch-initial-selection-text="fetchOrganizationName"
+ :fetch-initial-selection="fetchInitialOrganization"
:toggle-class="toggleClass"
v-on="$listeners"
>
diff --git a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
index 8c371e3d4ce..8c873d39496 100644
--- a/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
+++ b/app/assets/javascripts/vue_shared/components/entity_select/project_select.vue
@@ -120,24 +120,29 @@ export default {
membership: this.membership,
});
})();
- projects = data.map((item) => ({
- text: item.name_with_namespace || item.name,
- value: String(item.id),
- }));
+ projects = data.map((project) => this.mapProjectData(project));
} catch (error) {
this.handleError({ message: FETCH_PROJECTS_ERROR, error });
}
return { items: projects, totalPages: 1 };
},
- async fetchProjectName(projectId) {
- let projectName = '';
+ async fetchInitialProject(projectId) {
try {
- const { data: project } = await Api.project(projectId);
- projectName = project.name_with_namespace;
+ const response = await Api.project(projectId);
+
+ return this.mapProjectData(response.data);
} catch (error) {
this.handleError({ message: FETCH_PROJECT_ERROR, error });
+
+ return {};
}
- return projectName;
+ },
+ mapProjectData(project) {
+ return {
+ ...project,
+ text: project.name_with_namespace || project.name,
+ value: String(project.id),
+ };
},
handleError({ message, error }) {
Sentry.captureException(error);
@@ -163,7 +168,7 @@ export default {
:header-text="$options.i18n.selectProject"
:default-toggle-text="$options.i18n.searchForProject"
:fetch-items="fetchProjects"
- :fetch-initial-selection-text="fetchProjectName"
+ :fetch-initial-selection="fetchInitialProject"
:block="block"
clearable
v-on="$listeners"
diff --git a/app/assets/javascripts/vue_shared/components/file_icon.vue b/app/assets/javascripts/vue_shared/components/file_icon.vue
index 6a10557c6bc..4738d0f5a38 100644
--- a/app/assets/javascripts/vue_shared/components/file_icon.vue
+++ b/app/assets/javascripts/vue_shared/components/file_icon.vue
@@ -90,12 +90,6 @@ export default {
<svg v-else-if="!folder" :key="spriteHref" :class="[iconSizeClass, cssClasses]">
<use :href="spriteHref" />
</svg>
- <gl-icon
- v-else
- :name="folderIconName"
- :size="size"
- class="folder-icon"
- data-qa-selector="folder_icon_content"
- />
+ <gl-icon v-else :name="folderIconName" :size="size" class="folder-icon" />
</span>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index cecd1be82e9..6ac75230d88 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -132,11 +132,7 @@ export default {
@click="clickFile"
@mouseleave="$emit('mouseleave', $event)"
>
- <div
- class="file-row-name-container"
- data-qa-selector="file_row_container"
- :data-qa-file-name="file.name"
- >
+ <div class="file-row-name-container">
<span
ref="textOutput"
class="file-row-name"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
index c698b94749d..5362ceac9ee 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js
@@ -31,6 +31,8 @@ export const OPERATORS_IS_NOT = [...OPERATORS_IS, ...OPERATORS_NOT];
export const OPERATORS_IS_NOT_OR = [...OPERATORS_IS, ...OPERATORS_NOT, ...OPERATORS_OR];
export const OPERATORS_AFTER_BEFORE = [...OPERATORS_AFTER, ...OPERATORS_BEFORE];
+export const OPERATORS_TO_GROUP = [OPERATOR_OR, OPERATOR_NOT];
+
export const OPTION_NONE = { value: FILTER_NONE, text: __('None'), title: __('None') };
export const OPTION_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') };
export const OPTION_CURRENT = { value: FILTER_CURRENT, text: __('Current') };
@@ -66,6 +68,7 @@ export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_CONTACT = s__('Crm|Contact');
export const TOKEN_TITLE_GROUP = __('Group');
export const TOKEN_TITLE_LABEL = __('Label');
+export const TOKEN_TITLE_PROJECT = __('Project');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
export const TOKEN_TITLE_MY_REACTION = __('My-Reaction');
export const TOKEN_TITLE_ORGANIZATION = s__('Crm|Organization');
@@ -76,6 +79,7 @@ export const TOKEN_TITLE_STATUS = __('Status');
export const TOKEN_TITLE_JOBS_RUNNER_TYPE = s__('Job|Runner type');
export const TOKEN_TITLE_TARGET_BRANCH = __('Target Branch');
export const TOKEN_TITLE_TYPE = __('Type');
+export const TOKEN_TITLE_VERSION = __('Version');
export const TOKEN_TITLE_SEARCH_WITHIN = __('Search Within');
export const TOKEN_TITLE_CREATED = __('Created date');
export const TOKEN_TITLE_CLOSED = __('Closed date');
@@ -91,6 +95,7 @@ export const TOKEN_TYPE_EPIC = 'epic';
// this is in the shared constants. Until we have not decoupled the EE filtered search bar
// from the CE component, we need to keep this in the CE code.
// https://gitlab.com/gitlab-org/gitlab/-/issues/377838
+export const TOKEN_TYPE_PROJECT = 'project';
export const TOKEN_TYPE_HEALTH = 'health';
export const TOKEN_TYPE_ITERATION = 'iteration';
export const TOKEN_TYPE_LABEL = 'label';
@@ -104,6 +109,7 @@ export const TOKEN_TYPE_STATUS = 'status';
export const TOKEN_TYPE_JOBS_RUNNER_TYPE = 'jobs-runner-type';
export const TOKEN_TYPE_TARGET_BRANCH = 'target-branch';
export const TOKEN_TYPE_TYPE = 'type';
+export const TOKEN_TYPE_VERSION = 'version';
export const TOKEN_TYPE_WEIGHT = 'weight';
export const TOKEN_TYPE_SEARCH_WITHIN = 'in';
export const TOKEN_TYPE_CREATED = 'created';
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 d39e4d2ee42..364ba10e888 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
@@ -1,13 +1,5 @@
<script>
-import {
- GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlFormCheckbox,
- GlTooltipDirective,
-} from '@gitlab/ui';
+import { GlFilteredSearch, GlSorting, GlFormCheckbox, GlTooltipDirective } from '@gitlab/ui';
import RecentSearchesStorageKeys from 'ee_else_ce/filtered_search/recent_searches_storage_keys';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
@@ -22,10 +14,7 @@ import { filterEmptySearchTerm, uniqueTokens } from './filtered_search_utils';
export default {
components: {
GlFilteredSearch,
- GlButtonGroup,
- GlButton,
- GlDropdown,
- GlDropdownItem,
+ GlSorting,
GlFormCheckbox,
},
directives: {
@@ -118,8 +107,7 @@ export default {
recentSearchesPromise: null,
recentSearches: [],
filterValue: this.initialFilterValue,
- selectedSortOption: this.sortOptions[0],
- selectedSortDirection: SORT_DIRECTION.descending,
+ ...this.getInitialSort(),
};
},
computed: {
@@ -141,15 +129,14 @@ export default {
{},
);
},
- sortDirectionIcon() {
- return this.selectedSortDirection === SORT_DIRECTION.ascending
- ? 'sort-lowest'
- : 'sort-highest';
+ transformedSortOptions() {
+ return this.sortOptions.map(({ id: value, title: text }) => ({ value, text }));
},
- sortDirectionTooltip() {
- return this.selectedSortDirection === SORT_DIRECTION.ascending
- ? __('Sort direction: Ascending')
- : __('Sort direction: Descending');
+ selectedSortDirection() {
+ return this.sortDirectionAscending ? SORT_DIRECTION.ascending : SORT_DIRECTION.descending;
+ },
+ selectedSortOption() {
+ return this.sortOptions.find((sortOption) => sortOption.id === this.sortById);
},
/**
* This prop fixes a behaviour affecting GlFilteredSearch
@@ -184,14 +171,13 @@ export default {
this.filterValue = newValue;
}
},
- initialSortBy(newValue) {
- if (this.syncFilterAndSort) {
- this.updateSelectedSortValues(newValue);
+ initialSortBy(newInitialSortBy) {
+ if (this.syncFilterAndSort && newInitialSortBy) {
+ this.updateSelectedSortValues();
}
},
},
created() {
- this.updateSelectedSortValues(this.initialSortBy);
if (this.recentSearchesStorageKey) this.setupRecentSearch();
},
methods: {
@@ -273,15 +259,12 @@ export default {
return filter;
});
},
- handleSortOptionClick(sortBy) {
- this.selectedSortOption = sortBy;
- this.$emit('onSort', sortBy.sortDirection[this.selectedSortDirection]);
+ handleSortByChange(sortById) {
+ this.sortById = sortById;
+ this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
- handleSortDirectionClick() {
- this.selectedSortDirection =
- this.selectedSortDirection === SORT_DIRECTION.ascending
- ? SORT_DIRECTION.descending
- : SORT_DIRECTION.ascending;
+ handleSortDirectionChange(isAscending) {
+ this.sortDirectionAscending = isAscending;
this.$emit('onSort', this.selectedSortOption.sortDirection[this.selectedSortDirection]);
},
handleHistoryItemSelected(filters) {
@@ -328,18 +311,30 @@ export default {
const cleared = true;
this.$emit('onFilter', [], cleared);
},
- updateSelectedSortValues(sort) {
- if (!sort) {
- return;
+ updateSelectedSortValues() {
+ Object.assign(this, this.getInitialSort());
+ },
+ getInitialSort() {
+ for (const sortOption of this.sortOptions) {
+ if (sortOption.sortDirection.ascending === this.initialSortBy) {
+ return {
+ sortById: sortOption.id,
+ sortDirectionAscending: true,
+ };
+ }
+
+ if (sortOption.sortDirection.descending === this.initialSortBy) {
+ return {
+ sortById: sortOption.id,
+ sortDirectionAscending: false,
+ };
+ }
}
- this.selectedSortOption = this.sortOptions.find(
- (sortBy) =>
- sortBy.sortDirection.ascending === sort || sortBy.sortDirection.descending === sort,
- );
- this.selectedSortDirection = Object.keys(this.selectedSortOption?.sortDirection || {}).find(
- (key) => this.selectedSortOption.sortDirection[key] === sort,
- );
+ return {
+ sortById: this.sortOptions[0]?.id,
+ sortDirectionAscending: false,
+ };
},
},
};
@@ -390,25 +385,14 @@ export default {
</template>
</template>
</gl-filtered-search>
- <gl-button-group v-if="selectedSortOption" class="sort-dropdown-container d-flex">
- <gl-dropdown :text="selectedSortOption.title" :right="true" class="w-100">
- <gl-dropdown-item
- v-for="sortBy in sortOptions"
- :key="sortBy.id"
- is-check-item
- :is-checked="sortBy.id === selectedSortOption.id"
- @click="handleSortOptionClick(sortBy)"
- >{{ sortBy.title }}</gl-dropdown-item
- >
- </gl-dropdown>
- <gl-button
- v-gl-tooltip
- :title="sortDirectionTooltip"
- :aria-label="sortDirectionTooltip"
- :icon="sortDirectionIcon"
- class="flex-shrink-1"
- @click="handleSortDirectionClick"
- />
- </gl-button-group>
+ <gl-sorting
+ v-if="selectedSortOption"
+ :sort-options="transformedSortOptions"
+ :sort-by="sortById"
+ :is-ascending="sortDirectionAscending"
+ class="sort-dropdown-container"
+ @sortByChange="handleSortByChange"
+ @sortDirectionChange="handleSortDirectionChange"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
index 3857dd9c55d..5d72ac34e73 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue
@@ -11,7 +11,13 @@ import { debounce, last } from 'lodash';
import { stripQuotes } from '~/lib/utils/text_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { DEBOUNCE_DELAY, FILTERS_NONE_ANY, OPERATOR_NOT, OPERATOR_OR } from '../constants';
+import {
+ DEBOUNCE_DELAY,
+ FILTERS_NONE_ANY,
+ OPERATOR_NOT,
+ OPERATOR_OR,
+ OPERATORS_TO_GROUP,
+} from '../constants';
import { getRecentlyUsedSuggestions, setTokenValueToRecentlyUsed } from '../filtered_search_utils';
export default {
@@ -102,7 +108,7 @@ export default {
},
activeTokenValue() {
const data =
- this.glFeatures.groupMultiSelectTokens && Array.isArray(this.value.data)
+ this.multiSelectEnabled && Array.isArray(this.value.data)
? last(this.value.data)
: this.value.data;
return this.getActiveTokenValue(this.suggestions, data);
@@ -153,6 +159,22 @@ export default {
? this.activeTokenValue[this.searchBy]
: undefined;
},
+ multiSelectEnabled() {
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
+ },
+ validatedConfig() {
+ if (this.config.multiSelect && !this.multiSelectEnabled) {
+ return {
+ ...this.config,
+ multiSelect: false,
+ };
+ }
+ return this.config;
+ },
},
watch: {
active: {
@@ -199,7 +221,7 @@ export default {
}
}, DEBOUNCE_DELAY),
handleTokenValueSelected(selectedValue) {
- if (this.glFeatures.groupMultiSelectTokens) {
+ if (this.multiSelectEnabled) {
this.$emit('token-selected', selectedValue);
}
@@ -228,7 +250,7 @@ export default {
<template>
<gl-filtered-search-token
- :config="config"
+ :config="validatedConfig"
:value="value"
:active="active"
:multi-select-values="multiSelectValues"
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
index c5326ead60d..87e295d00dd 100644
--- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/user_token.vue
@@ -7,7 +7,7 @@ import { __ } from '~/locale';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
-import { OPTIONS_NONE_ANY } from '../constants';
+import { OPERATORS_TO_GROUP, OPTIONS_NONE_ANY } from '../constants';
import BaseToken from './base_token.vue';
@@ -57,7 +57,11 @@ export default {
return this.config.fetchUsers ? this.config.fetchUsers : this.fetchUsersBySearchTerm;
},
multiSelectEnabled() {
- return this.config.multiSelect && this.glFeatures.groupMultiSelectTokens;
+ return (
+ this.config.multiSelect &&
+ this.glFeatures.groupMultiSelectTokens &&
+ OPERATORS_TO_GROUP.includes(this.value.operator)
+ );
},
},
watch: {
@@ -94,7 +98,7 @@ export default {
return user?.avatarUrl || user?.avatar_url;
},
displayNameFor(username) {
- return this.getActiveUser(this.allUsers, username)?.name || `@${username}`;
+ return this.getActiveUser(this.allUsers, username)?.name || username;
},
avatarFor(username) {
const user = this.getActiveUser(this.allUsers, username);
diff --git a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
index 0455685627d..b03da19a896 100644
--- a/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
+++ b/app/assets/javascripts/vue_shared/components/form/input_copy_toggle_visibility.vue
@@ -179,7 +179,6 @@ export default {
:aria-label="toggleVisibilityLabel"
:icon="toggleVisibilityIcon"
data-testid="toggle-visibility-button"
- data-qa-selector="toggle_visibility_button"
@click.stop="handleToggleVisibilityButtonClick"
/>
<clipboard-button
diff --git a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue b/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
deleted file mode 100644
index d68c4399275..00000000000
--- a/app/assets/javascripts/vue_shared/components/keep_alive_slots.vue
+++ /dev/null
@@ -1,51 +0,0 @@
-<script>
-export default {
- props: {
- slotKey: {
- type: String,
- required: false,
- default: '',
- },
- },
- data() {
- return {
- aliveSlotsLookup: {},
- };
- },
- computed: {
- aliveSlots() {
- return Object.keys(this.aliveSlotsLookup);
- },
- },
- watch: {
- slotKey: {
- handler(val) {
- if (!val) {
- return;
- }
-
- this.$set(this.aliveSlotsLookup, val, true);
- },
- immediate: true,
- },
- },
- methods: {
- isCurrentSlot(key) {
- return key === this.slotKey;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div
- v-for="slot in aliveSlots"
- v-show="isCurrentSlot(slot)"
- :key="slot"
- class="gl-h-full gl-w-full"
- >
- <slot :name="slot"></slot>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/constants.js b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
index cff9c56a1c0..ad826c6f3e5 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/constants.js
+++ b/app/assets/javascripts/vue_shared/components/list_selector/constants.js
@@ -1,6 +1,26 @@
import { __ } from '~/locale';
+import UserItem from './user_item.vue';
+import GroupItem from './group_item.vue';
+import DeployKeyItem from './deploy_key_item.vue';
export const CONFIG = {
- users: { title: __('Users'), icon: 'user', filterKey: 'username', showNamespaceDropdown: true },
- groups: { title: __('Groups'), icon: 'group', filterKey: 'name' },
+ users: {
+ title: __('Users'),
+ icon: 'user',
+ filterKey: 'username',
+ showNamespaceDropdown: true,
+ component: UserItem,
+ },
+ groups: {
+ title: __('Groups'),
+ icon: 'group',
+ filterKey: 'name',
+ component: GroupItem,
+ },
+ deployKeys: {
+ title: __('Deploy keys'),
+ icon: 'key',
+ filterKey: 'name',
+ component: DeployKeyItem,
+ },
};
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
new file mode 100644
index 00000000000..4dbbd44f0b5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/list_selector/deploy_key_item.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlButton, GlIcon } from '@gitlab/ui';
+import { sprintf, __ } from '~/locale';
+
+export default {
+ name: 'DeployKeyItem',
+ components: { GlButton, GlIcon },
+ props: {
+ data: {
+ type: Object,
+ required: true,
+ },
+ canDelete: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ const { title, owner, id } = this.data;
+ return {
+ deleteButtonLabel: sprintf(__('Delete %{name}'), { name: title }),
+ title,
+ owner,
+ id,
+ };
+ },
+};
+</script>
+
+<template>
+ <span
+ class="gl-display-flex gl-align-items-center gl-gap-3"
+ data-testid="deploy-key-wrapper"
+ @click="$emit('select', id)"
+ >
+ <gl-icon name="key" />
+ <span class="gl-display-flex gl-flex-direction-column gl-flex-grow-1">
+ <span class="gl-font-weight-bold">{{ title }}</span>
+ <span class="gl-text-gray-600">@{{ owner }}</span>
+ </span>
+
+ <gl-button
+ v-if="canDelete"
+ icon="remove"
+ :aria-label="deleteButtonLabel"
+ category="tertiary"
+ @click.stop="$emit('delete', id)"
+ />
+ </span>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/list_selector/index.vue b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
index b8480a0c496..d79a8d6a00c 100644
--- a/app/assets/javascripts/vue_shared/components/list_selector/index.vue
+++ b/app/assets/javascripts/vue_shared/components/list_selector/index.vue
@@ -5,8 +5,6 @@ import { createAlert } from '~/alert';
import { __ } from '~/locale';
import groupsAutocompleteQuery from '~/graphql_shared/queries/groups_autocomplete.query.graphql';
import Api from '~/api';
-import UserItem from './user_item.vue';
-import GroupItem from './group_item.vue';
import { CONFIG } from './constants';
const I18N = {
@@ -25,10 +23,6 @@ export default {
GlCollapsibleListbox,
},
props: {
- title: {
- type: String,
- required: true,
- },
type: {
type: String,
required: true,
@@ -61,12 +55,6 @@ export default {
config() {
return CONFIG[this.type];
},
- isUserVariant() {
- return this.type === 'users';
- },
- component() {
- return this.isUserVariant ? UserItem : GroupItem;
- },
namespaceDropdownText() {
return parseBoolean(this.isProjectNamespace)
? this.$options.i18n.projectGroups
@@ -77,12 +65,14 @@ export default {
async handleSearchInput(search) {
this.$refs.results.open();
+ const searchMethod = {
+ users: this.fetchUsersBySearchTerm,
+ groups: this.fetchGroupsBySearchTerm,
+ deployKeys: this.fetchDeployKeysBySearchTerm,
+ };
+
try {
- if (this.isUserVariant) {
- this.items = await this.fetchUsersBySearchTerm(search);
- } else {
- this.items = await this.fetchGroupsBySearchTerm(search);
- }
+ this.items = await searchMethod[this.type](search);
} catch (e) {
createAlert({
message: this.$options.i18n.apiErrorMessage,
@@ -114,6 +104,10 @@ export default {
})),
);
},
+ fetchDeployKeysBySearchTerm() {
+ // TODO - implement API request (follow-up)
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/432494
+ },
getItemByKey(key) {
return this.items.find((item) => item[this.config.filterKey] === key);
},
@@ -139,7 +133,7 @@ export default {
<gl-card header-class="gl-new-card-header gl-border-none" body-class="gl-card-footer">
<template #header
><strong data-testid="list-selector-title"
- >{{ title }}
+ >{{ config.title }}
<span class="gl-text-gray-700 gl-ml-3"
><gl-icon :name="config.icon" /> {{ selectedItems.length }}</span
></strong
@@ -166,7 +160,7 @@ export default {
</template>
<template #list-item="{ item }">
- <component :is="component" :data="item" @select="handleSelectItem" />
+ <component :is="config.component" :data="item" @select="handleSelectItem" />
</template>
</gl-collapsible-listbox>
@@ -180,7 +174,7 @@ export default {
</div>
<component
- :is="component"
+ :is="config.component"
v-for="(item, index) of selectedItems"
:key="index"
:class="{ 'gl-border-t': index > 0 }"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
index d99b90fa561..a7dfc1e2cdb 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/comment_templates_dropdown.vue
@@ -88,7 +88,7 @@ export default {
placement="right"
searchable
size="small"
- class="comment-template-dropdown gl-mr-3"
+ class="comment-template-dropdown gl-mr-2"
positioning-strategy="fixed"
:searching="$apollo.queries.savedReplies.loading"
@shown="fetchCommentTemplates"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue
index 24211833026..e80f5c7f092 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/field.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue
@@ -361,7 +361,7 @@ export default {
<template>
<div
ref="gl-form"
- class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden"
+ class="js-vue-markdown-field md-area position-relative gfm-form"
:data-uploads-path="uploadsPath"
>
<markdown-header
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index cc3c95a047b..cffd8471d18 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -21,6 +21,7 @@ import { updateText } from '~/lib/utils/text_markdown';
import ToolbarButton from './toolbar_button.vue';
import DrawioToolbarButton from './drawio_toolbar_button.vue';
import CommentTemplatesDropdown from './comment_templates_dropdown.vue';
+import HeaderDivider from './header_divider.vue';
export default {
components: {
@@ -30,6 +31,7 @@ export default {
DrawioToolbarButton,
CommentTemplatesDropdown,
AiActionsDropdown: () => import('ee_component/ai/components/ai_actions_dropdown.vue'),
+ HeaderDivider,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -188,14 +190,6 @@ export default {
})
.catch(() => {});
},
- handleAttachFile(e) {
- e.preventDefault();
- const $gfmForm = $(this.$el).closest('.gfm-form');
- const $gfmTextarea = $gfmForm.find('.js-gfm-input');
-
- $gfmForm.find('.div-dropzone').click();
- $gfmTextarea.focus();
- },
insertIntoTextarea(text) {
const textArea = this.$el.closest('.md-area')?.querySelector('textarea');
if (textArea) {
@@ -254,252 +248,281 @@ export default {
</script>
<template>
- <div class="md-header gl-border-b gl-border-gray-100 gl-px-3">
+ <div
+ class="md-header gl-bg-white gl-border-b gl-border-gray-100 gl-rounded-lg gl-rounded-bottom-left-none gl-rounded-bottom-right-none gl-px-3"
+ :class="{ 'md-header-preview': previewMarkdown }"
+ >
<div class="gl-display-flex gl-align-items-center gl-flex-wrap">
<div
data-testid="md-header-toolbar"
- class="md-header-toolbar gl-display-flex gl-py-3 gl-flex-wrap gl-row-gap-3"
+ class="md-header-toolbar gl-display-flex gl-py-3 gl-row-gap-2 gl-flex-grow-1 gl-align-items-flex-start"
>
- <gl-button
- v-if="enablePreview"
- data-testid="preview-toggle"
- :value="previewMarkdown ? 'preview' : 'edit'"
- :label="$options.i18n.previewTabTitle"
- class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal! gl-mr-2"
- size="small"
- category="tertiary"
- @click="switchPreview"
- >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
- >
- <template v-if="!previewMarkdown && canSuggest">
+ <div class="gl-display-flex gl-flex-wrap gl-row-gap-2">
+ <gl-button
+ v-if="enablePreview"
+ data-testid="preview-toggle"
+ :value="previewMarkdown ? 'preview' : 'edit'"
+ :label="$options.i18n.previewTabTitle"
+ class="js-md-preview-button gl-flex-direction-row-reverse gl-align-items-center gl-font-weight-normal!"
+ size="small"
+ category="tertiary"
+ @click="switchPreview"
+ >{{ previewMarkdown ? $options.i18n.hidePreview : $options.i18n.preview }}</gl-button
+ >
+ <template v-if="!previewMarkdown && canSuggest">
+ <div class="gl-display-flex gl-row-gap-2">
+ <header-divider :preview-markdown="previewMarkdown" />
+ <toolbar-button
+ ref="suggestButton"
+ :tag="mdSuggestion"
+ :prepend="true"
+ :button-title="__('Insert suggestion')"
+ :cursor-offset="4"
+ :tag-content="lineContent"
+ tracking-property="codeSuggestion"
+ icon="doc-code"
+ data-testid="suggestion-button"
+ class="js-suggestion-btn"
+ @click="handleSuggestDismissed"
+ />
+ <gl-popover
+ v-if="suggestPopoverVisible"
+ :target="$refs.suggestButton.$el"
+ :css-classes="['diff-suggest-popover']"
+ placement="bottom"
+ :show="suggestPopoverVisible"
+ triggers=""
+ >
+ <strong>{{ __('New! Suggest changes directly') }}</strong>
+ <p class="mb-2">
+ {{
+ __(
+ 'Suggest code changes which can be immediately applied in one click. Try it out!',
+ )
+ }}
+ </p>
+ <gl-button
+ variant="confirm"
+ category="primary"
+ size="small"
+ data-testid="dismiss-suggestion-popover-button"
+ @click="handleSuggestDismissed"
+ >
+ {{ __('Got it') }}
+ </gl-button>
+ </gl-popover>
+ </div>
+ </template>
+ <div class="gl-display-flex gl-row-gap-2">
+ <div
+ v-if="!previewMarkdown && editorAiActions.length"
+ class="gl-display-flex gl-row-gap-2"
+ >
+ <header-divider :preview-markdown="previewMarkdown" />
+ <ai-actions-dropdown
+ :actions="editorAiActions"
+ @input="insertAIAction"
+ @replace="replaceTextarea"
+ />
+ </div>
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
<toolbar-button
- ref="suggestButton"
- :tag="mdSuggestion"
+ v-show="!previewMarkdown"
+ tag="**"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.bold"
+ icon="bold"
+ tracking-property="bold"
+ />
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="_"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.italic"
+ icon="italic"
+ tracking-property="italic"
+ />
+ <div class="gl-display-flex gl-row-gap-2">
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('strikethrough')"
+ v-show="!previewMarkdown"
+ tag="~~"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), {
+ modifierKey,
+ shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.strikethrough"
+ icon="strikethrough"
+ tracking-property="strike"
+ />
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('quote')"
+ v-show="!previewMarkdown"
:prepend="true"
- :button-title="__('Insert suggestion')"
- :cursor-offset="4"
- :tag-content="lineContent"
- tracking-property="codeSuggestion"
- icon="doc-code"
- data-testid="suggestion-button"
- class="js-suggestion-btn"
- @click="handleSuggestDismissed"
+ :tag="tag"
+ :button-title="__('Insert a quote')"
+ icon="quote"
+ tracking-property="blockquote"
+ @click="handleQuote"
/>
- <gl-popover
- v-if="suggestPopoverVisible"
- :target="$refs.suggestButton.$el"
- :css-classes="['diff-suggest-popover']"
- placement="bottom"
- :show="suggestPopoverVisible"
- triggers=""
- >
- <strong>{{ __('New! Suggest changes directly') }}</strong>
- <p class="mb-2">
- {{
- __(
- 'Suggest code changes which can be immediately applied in one click. Try it out!',
- )
- }}
- </p>
- <gl-button
- variant="confirm"
- category="primary"
- size="small"
- data-testid="dismiss-suggestion-popover-button"
- @click="handleSuggestDismissed"
- >
- {{ __('Got it') }}
- </gl-button>
- </gl-popover>
- </template>
- <ai-actions-dropdown
- v-if="!previewMarkdown && editorAiActions.length"
- :actions="editorAiActions"
- @input="insertAIAction"
- @replace="replaceTextarea"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="**"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add bold text (%{modifierKey}B)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.bold"
- icon="bold"
- tracking-property="bold"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="_"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add italic text (%{modifierKey}I)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.italic"
- icon="italic"
- tracking-property="italic"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('strikethrough')"
- v-show="!previewMarkdown"
- tag="~~"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add strikethrough text (%{modifierKey}%{shiftKey}X)'), {
- modifierKey,
- shiftKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.strikethrough"
- icon="strikethrough"
- tracking-property="strike"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('quote')"
- v-show="!previewMarkdown"
- :prepend="true"
- :tag="tag"
- :button-title="__('Insert a quote')"
- icon="quote"
- tracking-property="blockquote"
- @click="handleQuote"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="`"
- tag-block="```"
- :button-title="__('Insert code')"
- icon="code"
- tracking-property="code"
- />
- <toolbar-button
- v-show="!previewMarkdown"
- tag="[{text}](url)"
- tag-select="url"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
- modifierKey,
- }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
- "
- :shortcuts="$options.shortcuts.link"
- icon="link"
- tracking-property="link"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('bullet-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="- "
- :button-title="__('Add a bullet list')"
- icon="list-bulleted"
- tracking-property="bulletList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('numbered-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="1. "
- :button-title="__('Add a numbered list')"
- icon="list-numbered"
- tracking-property="orderedList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('task-list')"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="- [ ] "
- :button-title="__('Add a checklist')"
- icon="list-task"
- tracking-property="taskList"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('indent')"
- v-show="!previewMarkdown"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.indent"
- command="indentLines"
- icon="list-indent"
- tracking-property="indent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('outdent')"
- v-show="!previewMarkdown"
- class="gl-display-none"
- :button-title="
- /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
- sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
- modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
- })
- "
- :shortcuts="$options.shortcuts.outdent"
- command="outdentLines"
- icon="list-outdent"
- tracking-property="outdent"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('collapsible-section')"
- v-show="!previewMarkdown"
- :tag="mdCollapsibleSection"
- :prepend="true"
- tag-select="Click to expand"
- :button-title="__('Add a collapsible section')"
- icon="details-block"
- tracking-property="details"
- />
- <toolbar-button
- v-if="!restrictedToolBarItems.includes('table')"
- v-show="!previewMarkdown"
- :tag="mdTable"
- :prepend="true"
- :button-title="__('Add a table')"
- icon="table"
- tracking-property="table"
- />
- <toolbar-button
- v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')"
- data-testid="button-attach-file"
- :button-title="__('Attach a file or image')"
- icon="paperclip"
- class="gl-mr-3"
- tracking-property="upload"
- @click="handleAttachFile"
- />
- <drawio-toolbar-button
- v-if="!previewMarkdown && drawioEnabled"
- :uploads-path="uploadsPath"
- :markdown-preview-path="markdownPreviewPath"
- />
- <!-- TODO Add icon and trigger functionality from here -->
- <toolbar-button
- v-if="supportsQuickActions"
- v-show="!previewMarkdown"
- :prepend="true"
- tag="/"
- :button-title="__('Add a quick action')"
- icon="quick-actions"
- tracking-property="quickAction"
- />
- <comment-templates-dropdown
- v-if="!previewMarkdown && newCommentTemplatePath"
- :new-comment-template-path="newCommentTemplatePath"
- @select="insertSavedReply"
- />
- <div v-if="!previewMarkdown" class="full-screen">
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="`"
+ tag-block="```"
+ :button-title="__('Insert code')"
+ icon="code"
+ tracking-property="code"
+ />
+ <toolbar-button
+ v-show="!previewMarkdown"
+ tag="[{text}](url)"
+ tag-select="url"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Add a link (%{modifierKey}K)'), {
+ modifierKey,
+ }) /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */
+ "
+ :shortcuts="$options.shortcuts.link"
+ icon="link"
+ tracking-property="link"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('bullet-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="- "
+ :button-title="__('Add a bullet list')"
+ icon="list-bulleted"
+ tracking-property="bulletList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('numbered-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="1. "
+ :button-title="__('Add a numbered list')"
+ icon="list-numbered"
+ tracking-property="orderedList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('task-list')"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="- [ ] "
+ :button-title="__('Add a checklist')"
+ icon="list-task"
+ tracking-property="taskList"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('indent')"
+ v-show="!previewMarkdown"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.indent"
+ command="indentLines"
+ icon="list-indent"
+ tracking-property="indent"
+ />
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('outdent')"
+ v-show="!previewMarkdown"
+ class="gl-display-none"
+ :button-title="
+ /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */
+ sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), {
+ modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */,
+ })
+ "
+ :shortcuts="$options.shortcuts.outdent"
+ command="outdentLines"
+ icon="list-outdent"
+ tracking-property="outdent"
+ />
+ <div class="gl-display-flex gl-row-gap-2">
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('collapsible-section')"
+ v-show="!previewMarkdown"
+ :tag="mdCollapsibleSection"
+ :prepend="true"
+ tag-select="Click to expand"
+ :button-title="__('Add a collapsible section')"
+ icon="details-block"
+ tracking-property="details"
+ />
+ <header-divider :preview-markdown="previewMarkdown" />
+ </div>
+ <toolbar-button
+ v-if="!restrictedToolBarItems.includes('table')"
+ v-show="!previewMarkdown"
+ :tag="mdTable"
+ :prepend="true"
+ :button-title="__('Add a table')"
+ icon="table"
+ tracking-property="table"
+ />
+ <!--
+ The attach file button's click behavior is added by
+ dropzone_input.js.
+ -->
+ <toolbar-button
+ v-if="!previewMarkdown && !restrictedToolBarItems.includes('attach-file')"
+ data-testid="button-attach-file"
+ data-button-type="attach-file"
+ :button-title="__('Attach a file or image')"
+ icon="paperclip"
+ class="gl-mr-2"
+ tracking-property="upload"
+ />
+ <drawio-toolbar-button
+ v-if="!previewMarkdown && drawioEnabled"
+ :uploads-path="uploadsPath"
+ :markdown-preview-path="markdownPreviewPath"
+ />
+ <!-- TODO Add icon and trigger functionality from here -->
+ <toolbar-button
+ v-if="supportsQuickActions"
+ v-show="!previewMarkdown"
+ :prepend="true"
+ tag="/"
+ :button-title="__('Add a quick action')"
+ icon="quick-actions"
+ tracking-property="quickAction"
+ />
+ <comment-templates-dropdown
+ v-if="!previewMarkdown && newCommentTemplatePath"
+ :new-comment-template-path="newCommentTemplatePath"
+ @select="insertSavedReply"
+ />
+ </div>
+ <div
+ v-if="!previewMarkdown"
+ class="full-screen gl-flex-grow-1 gl-justify-content-end gl-display-flex"
+ >
<toolbar-button
v-if="!restrictedToolBarItems.includes('full-screen')"
- class="js-zen-enter"
+ class="js-zen-enter gl-mr-0!"
icon="maximize"
:button-title="__('Go full screen')"
:prepend="true"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue
new file mode 100644
index 00000000000..d08a3d4cd34
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/markdown/header_divider.vue
@@ -0,0 +1,16 @@
+<script>
+export default {
+ props: {
+ previewMarkdown: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+<template>
+ <div v-if="!previewMarkdown" class="md-toolbar-divider gl-display-flex gl-py-2">
+ <div class="gl-border-l gl-pl-3 gl-ml-2"></div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
index cf484443c07..182da7945ff 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue
@@ -111,7 +111,7 @@ export default {
type="button"
category="tertiary"
size="small"
- class="js-md gl-mr-3"
+ class="js-md gl-mr-2"
data-container="body"
@click="$emit('click', $event)"
/>
diff --git a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
index 3bee539688b..1ee752e8c19 100644
--- a/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/noteable_warning.vue
@@ -72,7 +72,7 @@ export default {
};
</script>
<template>
- <div class="issuable-note-warning">
+ <div class="issuable-note-warning" data-testid="issuable-note-warning">
<gl-icon v-if="!isLockedAndConfidential" :name="warningIcon" :size="16" class="icon inline" />
<span v-if="isLockedAndConfidential" ref="lockedAndConfidential">
diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js
new file mode 100644
index 00000000000..59b1967ad31
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.stories.js
@@ -0,0 +1,34 @@
+import NumberToHumanSize from './number_to_human_size.vue';
+
+export default {
+ component: NumberToHumanSize,
+ title: 'vue_shared/number_to_human_size',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { NumberToHumanSize },
+ props: Object.keys(argTypes),
+ template: '<number-to-human-size v-bind="$props" />',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ value: 42.55 * 1024 * 1024 * 1024,
+ fractionDigits: 1,
+ labelClass: '',
+ plainZero: false,
+};
+
+export const PlainZero = Template.bind({});
+PlainZero.args = {
+ ...Default.args,
+ value: 0,
+ plainZero: true,
+};
+
+export const CustomStyles = Template.bind({});
+CustomStyles.args = {
+ ...Default.args,
+ class: 'gl-font-weight-bold',
+ labelClass: 'gl-font-sm gl-text-gray-500',
+};
diff --git a/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue
new file mode 100644
index 00000000000..d6c56b2c465
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/number_to_human_size/number_to_human_size.vue
@@ -0,0 +1,48 @@
+<script>
+import { numberToHumanSizeSplit } from '~/lib/utils/number_utils';
+
+export default {
+ name: 'NumberToHumanSize',
+ props: {
+ value: {
+ type: Number,
+ required: true,
+ },
+ fractionDigits: {
+ type: Number,
+ required: false,
+ default: 1,
+ },
+ labelClass: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ plainZero: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ formattedValue() {
+ if (this.plainZero && this.value === 0) {
+ return ['0'];
+ }
+
+ return numberToHumanSizeSplit(this.value, this.fractionDigits);
+ },
+ number() {
+ return this.formattedValue[0];
+ },
+ label() {
+ return this.formattedValue[1];
+ },
+ },
+};
+</script>
+<template>
+ <span
+ >{{ number }}<span v-if="label" :class="labelClass"> {{ label }}</span></span
+ >
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
index 67ad7769c7c..f3b483c5f53 100644
--- a/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
+++ b/app/assets/javascripts/vue_shared/components/project_selector/project_selector.vue
@@ -100,7 +100,7 @@ export default {
type="search"
class="mb-3"
autofocus
- data-qa-selector="project_search_field"
+ data-testid="project-search-field"
@input="onInput"
/>
<div class="d-flex flex-column">
@@ -120,7 +120,7 @@ export default {
:project="project"
:matcher="searchQuery"
class="js-project-list-item"
- data-qa-selector="project_list_item"
+ data-testid="project-list-item"
@click="projectClicked(project)"
/>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/registry/details_row.vue b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
index 72e06b45561..85b4ea241ef 100644
--- a/app/assets/javascripts/vue_shared/components/registry/details_row.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/details_row.vue
@@ -32,12 +32,14 @@ export default {
<template>
<div
- class="gl-display-flex gl-align-items-center gl-font-monospace gl-font-sm gl-word-break-all"
+ class="gl-display-flex gl-align-items-top gl-font-monospace gl-font-sm gl-word-break-all"
:class="[padding, borderClass]"
>
- <gl-icon v-if="icon" :name="icon" class="gl-mr-4" />
- <span>
+ <div v-if="icon" class="gl-w-5 gl-mr-4">
+ <gl-icon :name="icon" />
+ </div>
+ <div>
<slot></slot>
- </span>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/registry/list_item.vue b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
index ccda8c5fea7..868e348adc0 100644
--- a/app/assets/javascripts/vue_shared/components/registry/list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/registry/list_item.vue
@@ -70,9 +70,11 @@ export default {
<slot name="left-action"></slot>
</div>
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
>
- <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1"
+ >
<div
v-if="
$slots['left-primary'] /* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
index 3b6dcace8fe..89b64f03e1f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/components/chunk_new.vue
@@ -66,6 +66,10 @@ export default {
const page = getPageParamValue(this.number);
return getPageSearchString(this.blamePath, page);
},
+ codeStyling() {
+ const defaultGutterWidth = 96;
+ return { marginLeft: `${this.$refs.lineNumbers?.offsetWidth || defaultGutterWidth}px` };
+ },
},
methods: {
handleChunkAppear() {
@@ -80,7 +84,7 @@ export default {
</script>
<template>
<div class="gl-display-flex">
- <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column">
+ <div v-if="shouldHighlight" class="gl-display-flex gl-flex-direction-column gl-absolute">
<div
v-for="(n, index) in totalLines"
:key="index"
@@ -102,14 +106,14 @@ export default {
</div>
</div>
- <div v-else class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
+ <div v-else ref="lineNumbers" class="line-numbers gl-p-0! gl-mr-3 gl-text-transparent">
<!-- Placeholder for line numbers while content is not highlighted -->
</div>
<gl-intersection-observer class="gl-w-full" @appear="handleChunkAppear">
<pre
class="gl-m-0 gl-p-0! gl-w-full gl-overflow-visible! gl-border-none! code highlight gl-line-height-0"
- ><code v-if="shouldHighlight" v-safe-html="highlightedContent" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
+ ><code v-if="shouldHighlight" v-safe-html="highlightedContent" :style="codeStyling" data-testid="content"></code><code v-else v-once class="line gl-white-space-pre-wrap! gl-ml-1" data-testid="content" v-text="rawContent"></code></pre>
</gl-intersection-observer>
</div>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
index 582093e5739..47b802d9d17 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/constants.js
@@ -14,6 +14,7 @@ export const ROUGE_TO_HLJS_LANGUAGE_MAP = {
clean: 'clean',
clojure: 'clojure',
cmake: 'cmake',
+ codeowners: 'codeowners',
coffeescript: 'coffeescript',
coq: 'coq',
cpp: 'cpp',
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
index a5f3f348cfc..c497224cde3 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql
@@ -14,6 +14,7 @@ query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine:
span
commit {
id
+ authorName
titleHtml
message
authoredDate
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
index dcefa66c403..bc46f11ab2d 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue
@@ -5,7 +5,7 @@ import SafeHtml from '~/vue_shared/directives/safe_html';
import Tracking from '~/tracking';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import LineHighlighter from '~/blob/line_highlighter';
-import { EVENT_ACTION, EVENT_LABEL_VIEWER } from './constants';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER, CODEOWNERS_FILE_NAME } from './constants';
import Chunk from './components/chunk_new.vue';
import Blame from './components/blame_info.vue';
import { calculateBlameOffset, shouldRender, toggleBlameClasses } from './utils';
@@ -21,6 +21,7 @@ export default {
components: {
Chunk,
Blame,
+ CodeownersValidation: () => import('ee_component/blob/components/codeowners_validation.vue'),
},
directives: {
SafeHtml,
@@ -45,6 +46,10 @@ export default {
type: String,
required: true,
},
+ currentRef: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -66,6 +71,9 @@ export default {
return result;
}, []);
},
+ isCodeownersFile() {
+ return this.blob.name === CODEOWNERS_FILE_NAME;
+ },
},
watch: {
showBlame: {
@@ -136,11 +144,19 @@ export default {
<blame v-if="showBlame && blameInfo.length" :blame-info="blameInfo" />
<div
- class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full"
+ class="file-content code js-syntax-highlight blob-content gl-display-flex gl-flex-direction-column gl-overflow-auto gl-w-full blob-viewer"
:class="$options.userColorScheme"
data-type="simple"
:data-path="blob.path"
+ data-testid="blob-viewer-file-content"
>
+ <codeowners-validation
+ v-if="isCodeownersFile"
+ class="gl-text-black-normal"
+ :current-ref="currentRef"
+ :project-path="projectPath"
+ :file-path="blob.path"
+ />
<chunk
v-for="(chunk, index) in chunks"
:key="index"
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
index 8d8e945cd5f..057a1c2d113 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_utils.js
@@ -1,13 +1,35 @@
import hljs from 'highlight.js/lib/core';
-import json from 'highlight.js/lib/languages/json';
+import languageLoader from '~/content_editor/services/highlight_js_language_loader';
import { registerPlugins } from '../plugins/index';
import { LINES_PER_CHUNK, NEWLINE, ROUGE_TO_HLJS_LANGUAGE_MAP } from '../constants';
-const initHighlightJs = (fileType, content, language) => {
- // The Highlight Worker is currently scoped to JSON files.
- // See the following issue for more: https://gitlab.com/gitlab-org/gitlab/-/issues/415753
- hljs.registerLanguage(language, json);
+const loadLanguage = async (language) => {
+ const languageDefinition = await languageLoader[language]();
+ hljs.registerLanguage(language, languageDefinition.default);
+};
+
+const loadSubLanguages = async (languageDefinition) => {
+ // Some files can contain sub-languages (i.e., Svelte); this ensures that sub-languages are also loaded
+ if (!languageDefinition?.contains) return;
+
+ // generate list of languages to load
+ const languages = new Set(
+ languageDefinition.contains
+ .filter((component) => Boolean(component.subLanguage))
+ .map((component) => component.subLanguage),
+ );
+
+ if (languageDefinition.subLanguage) {
+ languages.add(languageDefinition.subLanguage);
+ }
+
+ await Promise.all([...languages].map(loadLanguage));
+};
+
+const initHighlightJs = async (fileType, content, language) => {
registerPlugins(hljs, fileType, content, true);
+ await loadLanguage(language);
+ await loadSubLanguages(hljs.getLanguage(language));
};
const splitByLineBreaks = (content = '') => content.split(/\r?\n/);
@@ -35,12 +57,12 @@ const splitIntoChunks = (language, rawContent, highlightedContent) => {
return result;
};
-const highlight = (fileType, rawContent, lang) => {
+const highlight = async (fileType, rawContent, lang) => {
const language = ROUGE_TO_HLJS_LANGUAGE_MAP[lang.toLowerCase()];
let result;
if (language) {
- initHighlightJs(fileType, rawContent, language);
+ await initHighlightJs(fileType, rawContent, language);
const highlightedContent = hljs.highlight(rawContent, { language }).value;
result = splitIntoChunks(language, rawContent, highlightedContent);
}
diff --git a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
index 535e857d7a9..49afaba3d2f 100644
--- a/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
+++ b/app/assets/javascripts/vue_shared/components/source_viewer/workers/highlight_worker.js
@@ -4,7 +4,7 @@ import { highlight } from './highlight_utils';
* A webworker for highlighting large amounts of content with Highlight.js
*/
// eslint-disable-next-line no-restricted-globals
-self.addEventListener('message', ({ data: { fileType, content, language } }) => {
+self.addEventListener('message', async ({ data: { fileType, content, language } }) => {
// eslint-disable-next-line no-restricted-globals
- self.postMessage(highlight(fileType, content, language));
+ self.postMessage(await highlight(fileType, content, language));
});
diff --git a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
index 779a2ab5461..45d49e5339a 100644
--- a/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
+++ b/app/assets/javascripts/vue_shared/components/usage_quotas/usage_banner.vue
@@ -21,9 +21,11 @@ export default {
<div class="gl-display-flex gl-flex-direction-column">
<div class="gl-display-flex gl-align-items-center gl-py-3">
<div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-sm-flex-direction-row gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
>
- <div class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1">
+ <div
+ class="gl-display-flex gl-flex-direction-column gl-mb-3 gl-sm-mb-0 gl-min-w-0 gl-flex-grow-1"
+ >
<div
v-if="
/* eslint-disable-line @gitlab/vue-prefer-dollar-scopedslots */ $slots[
diff --git a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
index e5558c038b3..43e35f2b1f0 100644
--- a/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
+++ b/app/assets/javascripts/vue_shared/components/user_access_role_badge.vue
@@ -12,11 +12,18 @@ export default {
components: {
GlBadge,
},
+ props: {
+ size: {
+ type: String,
+ required: false,
+ default: 'md',
+ },
+ },
};
</script>
<template>
- <gl-badge class="gl-bg-transparent! gl-inset-border-1-gray-100!">
+ <gl-badge :size="size" class="gl-bg-transparent! gl-inset-border-1-gray-100!">
<slot></slot>
</gl-badge>
</template>
diff --git a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue b/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
deleted file mode 100644
index 46496d2e483..00000000000
--- a/app/assets/javascripts/vue_shared/components/vuex_module_provider.vue
+++ /dev/null
@@ -1,18 +0,0 @@
-<script>
-export default {
- provide() {
- return {
- vuexModule: this.vuexModule,
- };
- },
- props: {
- vuexModule: {
- type: String,
- required: true,
- },
- },
- render() {
- return this.$scopedSlots.default?.();
- },
-};
-</script>
diff --git a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
index b4afb27c497..96b2bd37080 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide/confirm_fork_modal.vue
@@ -82,7 +82,6 @@ export default {
attributes: {
href: this.forkPath,
variant: 'confirm',
- 'data-qa-selector': 'fork_project_button',
},
},
};
@@ -94,7 +93,6 @@ export default {
<template>
<gl-modal
:visible="visible"
- data-qa-selector="confirm_fork_modal"
:modal-id="modalId"
:title="$options.i18n.title"
:action-primary="btnActions.primary"
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 441b4c31b3a..3514a9c2d5d 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -145,6 +145,11 @@ export default {
required: false,
default: '',
},
+ cssClasses: {
+ type: String,
+ required: false,
+ default: 'gl-sm-ml-3',
+ },
},
data() {
return {
@@ -329,7 +334,7 @@ export default {
</script>
<template>
- <div class="gl-sm-ml-3">
+ <div :class="cssClasses">
<gl-disclosure-dropdown
v-if="hasActions"
:variant="isBlob ? 'confirm' : 'default'"
diff --git a/app/assets/javascripts/vue_shared/global_search/constants.js b/app/assets/javascripts/vue_shared/global_search/constants.js
index 14ea0389bad..b3840a0adbf 100644
--- a/app/assets/javascripts/vue_shared/global_search/constants.js
+++ b/app/assets/javascripts/vue_shared/global_search/constants.js
@@ -1,11 +1,10 @@
-import { s__, __, sprintf } from '~/locale';
+import { s__, __ } from '~/locale';
export const AUTOCOMPLETE_ERROR_MESSAGE = s__(
'GlobalSearch|There was an error fetching search autocomplete suggestions.',
);
export const ALL_GITLAB = __('All GitLab');
-export const SEARCH_GITLAB = s__('GlobalSearch|Search GitLab');
export const PLACES = s__('GlobalSearch|Places');
export const COMMAND_PALETTE = s__('GlobalSearch|Command palette');
@@ -24,17 +23,9 @@ export const SEARCH_DESCRIBED_BY_UPDATED = s__(
);
export const SEARCH_RESULTS_LOADING = s__('GlobalSearch|Search results are loading');
export const SEARCH_RESULTS_SCOPE = s__('GlobalSearch|in %{scope}');
-export const KBD_HELP = sprintf(
- s__('GlobalSearch|Use the shortcut key %{kbdOpen}/%{kbdClose} to start a search'),
- { kbdOpen: '<kbd>', kbdClose: '</kbd>' },
- false,
-);
export const MIN_SEARCH_TERM = s__(
'GlobalSearch|The search term must be at least 3 characters long.',
);
-
-export const SCOPED_SEARCH_ITEM_ARIA_LABEL = s__('GlobalSearch| %{search} %{description} %{scope}');
-
export const MSG_ISSUES_ASSIGNED_TO_ME = s__('GlobalSearch|Issues assigned to me');
export const MSG_ISSUES_IVE_CREATED = s__("GlobalSearch|Issues I've created");
@@ -76,8 +67,6 @@ export const SEARCH_RESULTS_ORDER = [
SETTINGS_CATEGORY,
HELP_CATEGORY,
];
-export const DROPDOWN_ORDER = SEARCH_RESULTS_ORDER;
-
export const SEARCH_LABELS = s__('GlobalSearch|Search labels');
export const DROPDOWN_HEADER = s__('GlobalSearch|Labels');
diff --git a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
index b4287d86289..1828208bd0f 100644
--- a/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
+++ b/app/assets/javascripts/vue_shared/issuable/create/components/issuable_label_selector.vue
@@ -18,6 +18,7 @@ export default {
'initialLabels',
'issuableType',
'labelType',
+ 'issuableSupportsLockOnMerge',
'variant',
'workspaceType',
],
@@ -76,6 +77,7 @@ export default {
:issuable-type="issuableType"
:label-create-type="labelType"
:selected-labels="selectedLabels"
+ :issuable-supports-lock-on-merge="issuableSupportsLockOnMerge"
@updateSelectedLabels="handleUpdateSelectedLabels"
@onLabelRemove="handleLabelRemove"
>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
index 0db7417cebc..ad908a674d3 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue
@@ -6,6 +6,7 @@ import PageSizeSelector from '~/vue_shared/components/page_size_selector.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
+import { DRAG_DELAY } from '~/sortable/constants';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -24,6 +25,8 @@ export default {
forceFallback: true,
ghostClass: 'gl-visibility-hidden',
tag: 'ul',
+ delay: DRAG_DELAY,
+ delayOnTouchOnly: true,
},
components: {
GlAlert,
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
index dae3ddfe016..bac71c1eda2 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_body.vue
@@ -31,6 +31,10 @@ export default {
type: Boolean,
required: true,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ },
enableAutocomplete: {
type: Boolean,
required: true,
@@ -166,6 +170,7 @@ export default {
:issuable="issuable"
:status-icon="statusIcon"
:enable-edit="enableEdit"
+ :hide-edit-button="hideEditButton"
:workspace-type="workspaceType"
@edit-issuable="$emit('edit-issuable', $event)"
>
@@ -181,12 +186,12 @@ export default {
:task-list-update-path="taskListUpdatePath"
/>
<slot name="secondary-content"></slot>
- <small v-if="isUpdated" class="edited-text gl-font-sm!">
+ <small v-if="isUpdated" class="edited-text gl-font-sm! gl-text-secondary">
{{ __('Edited') }}
<time-ago-tooltip :time="issuable.updatedAt" tooltip-placement="bottom" />
<span v-if="updatedBy">
{{ __('by') }}
- <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm!">
+ <gl-link :href="updatedBy.webUrl" class="author-link gl-font-sm! gl-text-secondary">
<span>{{ updatedBy.name }}</span>
</gl-link>
</span>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
index 7c3dd5c3623..3353374310f 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_edit_form.vue
@@ -153,7 +153,10 @@ export default {
</template>
</markdown-field>
</gl-form-group>
- <div data-testid="actions" class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix">
+ <div
+ data-testid="actions"
+ class="col-12 gl-mt-3 gl-mb-3 gl-px-0 clearfix gl-display-flex gl-gap-3"
+ >
<slot
name="edit-form-actions"
:issuable-title="title"
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
index 62a2b44e660..1b95a2abdf9 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_header.vue
@@ -155,8 +155,8 @@ export default {
</script>
<template>
- <div class="detail-page-header gl-flex-direction-column gl-sm-flex-direction-row">
- <div class="detail-page-header-body gl-flex-wrap gl-gap-2">
+ <div class="detail-page-header gl-flex-direction-column gl-md-flex-direction-row">
+ <div class="detail-page-header-body gl-flex-wrap gl-column-gap-2">
<gl-badge :variant="badgeVariant" data-testid="issue-state-badge">
<gl-icon v-if="statusIcon" :name="statusIcon" :class="statusIconClass" />
<span class="gl-display-none gl-sm-display-block" :class="{ 'gl-ml-2': statusIcon }">
@@ -221,7 +221,7 @@ export default {
@click="handleRightSidebarToggleClick"
/>
</div>
- <div class="detail-page-header-actions gl-align-self-center gl-display-flex">
+ <div class="detail-page-header-actions gl-align-self-center gl-display-flex gl-gap-3">
<slot name="header-actions"></slot>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
index 040f49c7c25..1d44c4a1c14 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_show_root.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
enableAutocomplete: {
type: Boolean,
required: false,
@@ -137,6 +142,7 @@ export default {
:status-icon="statusIcon"
:status-icon-class="statusIconClass"
:enable-edit="enableEdit"
+ :hide-edit-button="hideEditButton"
:enable-autocomplete="enableAutocomplete"
:enable-autosave="enableAutosave"
:enable-zen-mode="enableZenMode"
@@ -169,6 +175,9 @@ export default {
</issuable-discussion>
<issuable-sidebar>
+ <template #right-sidebar-top-items="{ sidebarExpanded, toggleSidebar }">
+ <slot name="right-sidebar-top-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
+ </template>
<template #right-sidebar-items="{ sidebarExpanded, toggleSidebar }">
<slot name="right-sidebar-items" v-bind="{ sidebarExpanded, toggleSidebar }"></slot>
</template>
diff --git a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
index 5387e39e3eb..3dae894b127 100644
--- a/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
+++ b/app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue
@@ -33,6 +33,10 @@ export default {
type: Boolean,
required: true,
},
+ hideEditButton: {
+ type: Boolean,
+ required: false,
+ },
workspaceType: {
type: String,
required: false,
@@ -70,7 +74,7 @@ export default {
data-testid="issuable-title"
></h1>
<gl-button
- v-if="enableEdit"
+ v-if="enableEdit && !hideEditButton"
v-gl-tooltip.bottom
:title="$options.i18n.editTitleAndDescription"
:aria-label="$options.i18n.editTitleAndDescription"
diff --git a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
index 774267639fc..cb9ad6418a4 100644
--- a/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
+++ b/app/assets/javascripts/vue_shared/issuable/sidebar/components/issuable_sidebar_root.vue
@@ -1,13 +1,17 @@
<script>
-import { GlIcon } from '@gitlab/ui';
+import { GlButton, GlTooltipDirective } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { getCookie, setCookie, parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from '~/locale';
import { USER_COLLAPSED_GUTTER_COOKIE } from '../constants';
export default {
components: {
- GlIcon,
+ GlButton,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
},
data() {
const userExpanded = !parseBoolean(getCookie(USER_COLLAPSED_GUTTER_COOKIE));
@@ -20,6 +24,20 @@ export default {
isExpanded: userExpanded ? bp.isDesktop() : userExpanded,
};
},
+ computed: {
+ toggleLabel() {
+ return this.isExpanded ? __('Collapse sidebar') : __('Expand sidebar');
+ },
+ toggleIcon() {
+ return this.isExpanded ? 'chevron-double-lg-right' : 'chevron-double-lg-left';
+ },
+ expandedToggleClass() {
+ return this.isExpanded ? 'block' : '';
+ },
+ collapsedToggleClass() {
+ return !this.isExpanded ? 'block' : '';
+ },
+ },
mounted() {
window.addEventListener('resize', this.handleWindowResize);
this.updatePageContainerClass();
@@ -59,23 +77,24 @@ export default {
class="right-sidebar"
aria-live="polite"
>
- <button
- class="toggle-right-sidebar-button js-toggle-right-sidebar-button w-100 gl-text-decoration-none! gl-display-flex gl-outline-0!"
- data-testid="toggle-right-sidebar-button"
- :title="__('Toggle sidebar')"
- @click="toggleSidebar"
- >
- <span v-if="isExpanded" class="collapse-text gl-flex-grow-1 gl-text-left">{{
- __('Collapse sidebar')
- }}</span>
- <gl-icon v-show="isExpanded" data-testid="icon-collapse" name="chevron-double-lg-right" />
- <gl-icon
- v-show="!isExpanded"
- data-testid="icon-expand"
- name="chevron-double-lg-left"
- class="gl-ml-2"
+ <div class="right-sidebar-header" :class="expandedToggleClass">
+ <gl-button
+ v-gl-tooltip.hover.left
+ category="tertiary"
+ size="small"
+ class="gl-float-right gutter-toggle toggle-right-sidebar-button js-toggle-right-sidebar-button gl-shadow-none!"
+ :class="collapsedToggleClass"
+ data-testid="toggle-right-sidebar-button"
+ :icon="toggleIcon"
+ :title="toggleLabel"
+ :aria-label="toggleLabel"
+ @click="toggleSidebar"
/>
- </button>
+ <slot
+ name="right-sidebar-top-items"
+ v-bind="{ sidebarExpanded: isExpanded, toggleSidebar }"
+ ></slot>
+ </div>
<div data-testid="sidebar-items" class="issuable-sidebar">
<slot
name="right-sidebar-items"
diff --git a/app/assets/javascripts/vue_shared/mixins/timeago.js b/app/assets/javascripts/vue_shared/mixins/timeago.js
index 61e45fa5195..438da925937 100644
--- a/app/assets/javascripts/vue_shared/mixins/timeago.js
+++ b/app/assets/javascripts/vue_shared/mixins/timeago.js
@@ -1,4 +1,4 @@
-import { formatDate, getTimeago, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
+import { getTimeago, localeDateFormat, timeagoLanguageCode } from '~/lib/utils/datetime_utility';
/**
* Mixin with time ago methods used in some vue components
@@ -12,7 +12,7 @@ export default {
},
tooltipTitle(time) {
- return formatDate(time);
+ return localeDateFormat.asDateTimeFull.format(time);
},
},
};
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 3412848a9b7..a5c34b4b619 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
@@ -86,11 +86,7 @@ export default {
},
showSuperSidebarToggle() {
- return gon.use_new_navigation && sidebarState.isCollapsed;
- },
-
- topBarClasses() {
- return gon.use_new_navigation ? 'top-bar-fixed container-fluid' : '';
+ return sidebarState.isCollapsed;
},
},
@@ -124,7 +120,7 @@ export default {
<template>
<div>
- <div :class="topBarClasses" data-testid="top-bar">
+ <div class="top-bar-fixed container-fluid" data-testid="top-bar">
<div
class="top-bar-container gl-display-flex gl-align-items-center gl-border-b-1 gl-border-b-gray-100 gl-border-b-solid"
>
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index c1ec39e1545..dccff4a288f 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { featureToMutationMap } from 'ee_else_ce/security_configuration/components/constants';
+import { featureToMutationMap } from 'ee_else_ce/security_configuration/constants';
import { parseErrorMessage } from '~/lib/utils/error_message';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import { sprintf, s__ } from '~/locale';
@@ -110,7 +110,6 @@ export default {
:loading="isLoading"
:variant="variant"
:category="category"
- :data-qa-selector="`${feature.type}_mr_button`"
@click="mutate"
>{{ $options.i18n.buttonLabel }}</gl-button
>
diff --git a/app/assets/javascripts/webhooks/components/form_url_app.vue b/app/assets/javascripts/webhooks/components/form_url_app.vue
index f20d4d9312b..25f994c1e6e 100644
--- a/app/assets/javascripts/webhooks/components/form_url_app.vue
+++ b/app/assets/javascripts/webhooks/components/form_url_app.vue
@@ -1,6 +1,13 @@
<script>
import { cloneDeep, isEmpty } from 'lodash';
-import { GlFormGroup, GlFormInput, GlFormRadio, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import {
+ GlFormGroup,
+ GlFormInput,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLink,
+ GlAlert,
+} from '@gitlab/ui';
import { __, s__ } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
@@ -14,6 +21,7 @@ export default {
GlFormRadio,
GlFormRadioGroup,
GlLink,
+ GlAlert,
},
props: {
initialUrl: {
@@ -40,6 +48,9 @@ export default {
urlState() {
return !this.isValidated || !isEmpty(this.url);
},
+ urlHasChanged() {
+ return this.url !== this.initialUrl;
+ },
maskedUrl() {
if (!this.url) {
return null;
@@ -152,6 +163,9 @@ export default {
urlPlaceholder: 'http://example.com/trigger-ci.json',
urlPreview: s__('Webhooks|URL preview'),
valuePartOfUrl: s__('Webhooks|Must match part of URL'),
+ tokenWillBeCleared: s__(
+ 'Webhooks|Secret token will be cleared on save unless token is updated.',
+ ),
},
};
</script>
@@ -175,6 +189,14 @@ export default {
:placeholder="$options.i18n.urlPlaceholder"
data-testid="form-url"
/>
+ <gl-alert
+ v-if="urlHasChanged"
+ variant="warning"
+ :dismissible="false"
+ class="gl-my-4 gl-form-input-xl"
+ >
+ {{ $options.i18n.tokenWillBeCleared }}
+ </gl-alert>
</gl-form-group>
<div class="gl-mt-5">
<gl-form-radio-group v-model="maskEnabled">
diff --git a/app/assets/javascripts/webpack_non_compiled_placeholder.js b/app/assets/javascripts/webpack_non_compiled_placeholder.js
index c1baa7b8dd3..60f7eb14ffe 100644
--- a/app/assets/javascripts/webpack_non_compiled_placeholder.js
+++ b/app/assets/javascripts/webpack_non_compiled_placeholder.js
@@ -39,6 +39,26 @@ div.innerHTML = `
${reloadMessage}<br />
If it doesn't, please <a href="">reload the page manually</a>.
</p>
+<div class="gl-card gl-layout-w-limited gl-m-auto">
+ <div class="gl-card-body">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 410 404" width="100">
+ <path fill="url(#a)" d="m399.641 59.525-183.998 329.02c-3.799 6.793-13.559 6.833-17.415.073L10.582 59.556C6.38 52.19 12.68 43.266 21.028 44.76l184.195 32.923c1.175.21 2.378.208 3.553-.006l180.343-32.87c8.32-1.517 14.649 7.337 10.522 14.719Z"/>
+ <path fill="url(#b)" d="M292.965 1.574 156.801 28.255a5 5 0 0 0-4.03 4.611l-8.376 141.464c-.197 3.332 2.863 5.918 6.115 5.168l37.91-8.749c3.547-.818 6.752 2.306 6.023 5.873l-11.263 55.153c-.758 3.712 2.727 6.886 6.352 5.785l23.415-7.114c3.63-1.102 7.118 2.081 6.35 5.796l-17.899 86.633c-1.12 5.419 6.088 8.374 9.094 3.728l2.008-3.103 110.954-221.428c1.858-3.707-1.346-7.935-5.418-7.15l-39.022 7.532c-3.667.707-6.787-2.708-5.752-6.296l25.469-88.291c1.036-3.594-2.095-7.012-5.766-6.293Z"/>
+ <defs>
+ <linearGradient id="a" x1="6" x2="235" y1="33" y2="344" gradientUnits="userSpaceOnUse"><stop stop-color="#41D1FF"/><stop offset="1" stop-color="#BD34FE"/></linearGradient>
+ <linearGradient id="b" x1="194.651" x2="236.076" y1="8.818" y2="292.989" gradientUnits="userSpaceOnUse"><stop stop-color="#FFEA83"/><stop offset=".083" stop-color="#FFDD35"/><stop offset="1" stop-color="#FFA800"/></linearGradient>
+ </defs>
+ </svg>
+ <h2>Don't want to see this message anymore?</h2>
+ <p class="gl-text-body">
+ Follow the documentation to switch to using Vite.<br />
+ Vite compiles frontend assets faster and eliminates the need for this message.
+ </p>
+ <a href="https://gitlab.com/gitlab-org/gitlab-development-kit/-/blob/main/doc/configuration.md?ref_type=heads#vite-settings" rel="noopener noreferrer" target="_blank" class="btn btn-confirm btn-md gl-button">
+ <span class="gl-button-text">Switch to Vite</span>
+ </a>
+ </div>
+</div>
`;
document.body.append(div);
diff --git a/app/assets/javascripts/whats_new/index.js b/app/assets/javascripts/whats_new/index.js
index bc9e2d5c3b1..57db8cde110 100644
--- a/app/assets/javascripts/whats_new/index.js
+++ b/app/assets/javascripts/whats_new/index.js
@@ -1,34 +1,22 @@
import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import { mapState } from 'vuex';
-import App from './components/app.vue';
+import WhatsNewApp from './components/app.vue';
import store from './store';
-import { getVersionDigest, setNotification } from './utils/notification';
let whatsNewApp;
-export default (el) => {
+export default (versionDigest) => {
if (whatsNewApp) {
store.dispatch('openDrawer');
} else {
+ const el = document.createElement('div');
+ document.body.append(el);
whatsNewApp = new Vue({
el,
store,
- components: {
- App,
- },
- computed: {
- ...mapState(['open']),
- },
- watch: {
- open() {
- setNotification(el);
- },
- },
render(createElement) {
- return createElement('app', {
+ return createElement(WhatsNewApp, {
props: {
- versionDigest: getVersionDigest(el),
+ versionDigest,
},
});
},
diff --git a/app/assets/javascripts/whats_new/utils/notification.js b/app/assets/javascripts/whats_new/utils/notification.js
index 1621c4d5f27..fb6ce7454dc 100644
--- a/app/assets/javascripts/whats_new/utils/notification.js
+++ b/app/assets/javascripts/whats_new/utils/notification.js
@@ -1,21 +1 @@
export const STORAGE_KEY = 'display-whats-new-notification';
-
-export const getVersionDigest = (appEl) => appEl.dataset.versionDigest;
-
-export const setNotification = (appEl) => {
- const versionDigest = getVersionDigest(appEl);
- const notificationEl = document.querySelector('.header-help');
- if (!notificationEl) return;
-
- let notificationCountEl = notificationEl.querySelector('.js-whats-new-notification-count');
-
- if (localStorage.getItem(STORAGE_KEY) === versionDigest || versionDigest === undefined) {
- notificationEl.classList.remove('with-notifications');
- if (notificationCountEl) {
- notificationCountEl.parentElement.removeChild(notificationCountEl);
- notificationCountEl = null;
- }
- } else {
- notificationEl.classList.add('with-notifications');
- }
-};
diff --git a/app/assets/javascripts/work_items/components/item_title.vue b/app/assets/javascripts/work_items/components/item_title.vue
index 74bcc2717bd..23a9671c914 100644
--- a/app/assets/javascripts/work_items/components/item_title.vue
+++ b/app/assets/javascripts/work_items/components/item_title.vue
@@ -18,6 +18,18 @@ export default {
required: false,
default: false,
},
+ useH1: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ headerClasses() {
+ return this.useH1
+ ? 'gl-w-full gl-font-size-h-display gl-m-0!'
+ : 'gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-w-full';
+ },
},
methods: {
handleBlur({ target }) {
@@ -39,9 +51,10 @@ export default {
</script>
<template>
- <h2
- class="gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-w-full"
- :class="{ 'gl-cursor-text': disabled }"
+ <component
+ :is="useH1 ? 'h1' : 'h2'"
+ class="gl-w-full"
+ :class="[{ 'gl-cursor-text': disabled }, headerClasses]"
aria-labelledby="item-title"
>
<span
@@ -64,5 +77,5 @@ export default {
@keydown.meta.b.prevent
>{{ title }}</span
>
- </h2>
+ </component>
</template>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
index c3b7b7a2953..3636f222c2d 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_add_note.vue
@@ -287,9 +287,9 @@ export default {
v-else
ref="textarea"
rows="1"
- class="reply-placeholder-text-field gl-font-regular!"
+ class="reply-placeholder-text-field"
data-testid="note-reply-textarea"
- :placeholder="__('Reply')"
+ :placeholder="__('Reply…')"
:aria-label="__('Reply to comment')"
@focus="showReplyForm"
@click="showReplyForm"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
index cb9a560f9e1..eb61b5e8e18 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_actions.vue
@@ -33,6 +33,7 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
+ inject: ['isGroup'],
props: {
fullPath: {
type: String,
@@ -152,6 +153,7 @@ export default {
note: this.note,
name,
fullPath: this.fullPath,
+ isGroup: this.isGroup,
workItemIid: this.workItemIid,
}),
});
@@ -207,6 +209,7 @@ export default {
<gl-button
v-if="showEdit"
v-gl-tooltip
+ data-testid="note-actions-edit"
data-track-action="click_button"
data-track-label="edit_button"
category="tertiary"
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
index 75a8a7b29c0..c3b3c0e6db7 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_note_awards_list.vue
@@ -8,6 +8,7 @@ export default {
components: {
AwardsList,
},
+ inject: ['isGroup'],
props: {
fullPath: {
type: String,
@@ -73,6 +74,7 @@ export default {
note: this.note,
name,
fullPath: this.fullPath,
+ isGroup: this.isGroup,
workItemIid: this.workItemIid,
}),
});
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
index 1578c78ac4f..722ba898f80 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_notes_activity_header.vue
@@ -36,6 +36,16 @@ export default {
default: WORK_ITEM_NOTES_FILTER_ALL_NOTES,
required: false,
},
+ useH2: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ },
+ computed: {
+ headerClasses() {
+ return this.useH2 ? 'gl-font-size-h1 gl-m-0' : 'gl-font-base gl-m-0';
+ },
},
methods: {
changeNotesSortOrder(direction) {
@@ -58,7 +68,9 @@ export default {
<div
class="gl-display-flex gl-justify-content-space-between gl-flex-wrap gl-pb-3 gl-align-items-center"
>
- <h3 class="gl-font-base gl-m-0">{{ $options.i18n.activityLabel }}</h3>
+ <component :is="useH2 ? 'h2' : 'h3'" :class="headerClasses">{{
+ $options.i18n.activityLabel
+ }}</component>
<div class="gl-display-flex gl-gap-3">
<work-item-activity-sort-filter
:work-item-type="workItemType"
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
index cbe7de4abcd..503328f7b03 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue
@@ -58,11 +58,6 @@ export default {
default: true,
},
},
- data() {
- return {
- isFocused: false,
- };
- },
computed: {
labels() {
return this.metadataWidgets[WIDGET_TYPE_LABELS]?.labels?.nodes || [];
@@ -117,7 +112,7 @@ export default {
return false;
},
showRemove() {
- return this.canUpdate && this.isFocused;
+ return this.canUpdate;
},
displayLabels() {
return this.showLabels && this.labels.length;
@@ -135,10 +130,6 @@ export default {
<div
class="item-body work-item-link-child gl-relative gl-display-flex gl-flex-grow-1 gl-overflow-break-word gl-min-w-0 gl-pl-3 gl-pr-2 gl-py-2 gl-mx-n2 gl-rounded-base gl-gap-3"
data-testid="links-child"
- @mouseover="isFocused = true"
- @mouseleave="isFocused = false"
- @focusin="isFocused = true"
- @focusout="isFocused = false"
>
<div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
@@ -203,12 +194,14 @@ export default {
</div>
<div v-if="canUpdate">
<gl-button
+ v-gl-tooltip
:class="{ 'gl-visibility-visible': showRemove }"
class="gl-visibility-hidden"
category="tertiary"
size="small"
icon="close"
:aria-label="$options.i18n.remove"
+ :title="$options.i18n.remove"
data-testid="remove-work-item-link"
@click="$emit('removeChild', childItem)"
/>
diff --git a/app/assets/javascripts/work_items/components/update_work_item.js b/app/assets/javascripts/work_items/components/update_work_item.js
deleted file mode 100644
index fc395fa5be3..00000000000
--- a/app/assets/javascripts/work_items/components/update_work_item.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
-
-export function getUpdateWorkItemMutation({ input, workItemParentId }) {
- let mutation = updateWorkItemMutation;
-
- const variables = {
- input,
- };
-
- if (workItemParentId) {
- mutation = updateWorkItemTaskMutation;
- variables.input = {
- id: workItemParentId,
- taskData: input,
- };
- }
-
- return {
- mutation,
- variables,
- };
-}
diff --git a/app/assets/javascripts/work_items/components/work_item_actions.vue b/app/assets/javascripts/work_items/components/work_item_actions.vue
index 0a71fbc9a34..013c9f229ec 100644
--- a/app/assets/javascripts/work_items/components/work_item_actions.vue
+++ b/app/assets/javascripts/work_items/components/work_item_actions.vue
@@ -134,11 +134,6 @@ export default {
required: false,
default: false,
},
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
},
apollo: {
workItemTypes: {
@@ -328,7 +323,6 @@ export default {
:data-testid="$options.stateToggleTestId"
:work-item-id="workItemId"
:work-item-state="workItemState"
- :work-item-parent-id="workItemParentId"
:work-item-type="workItemType"
show-as-dropdown-item
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue
new file mode 100644
index 00000000000..708121ee210
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy.vue
@@ -0,0 +1,127 @@
+<script>
+import uniqueId from 'lodash/uniqueId';
+import { GlIcon, GlTooltip, GlDisclosureDropdown } from '@gitlab/ui';
+import DisclosureHierarchyItem from './disclosure_hierarchy_item.vue';
+
+export default {
+ components: {
+ GlDisclosureDropdown,
+ GlIcon,
+ GlTooltip,
+ DisclosureHierarchyItem,
+ },
+ props: {
+ /**
+ * A list of items in the form:
+ * ```
+ * {
+ * title: String, required
+ * icon: String, optional
+ * }
+ * ```
+ */
+ items: {
+ type: Array,
+ required: false,
+ default: () => [],
+ validator: (items) => {
+ return items.every((item) => Object.keys(item).includes('title'));
+ },
+ },
+ /**
+ * When set, displays only first and last item, and groups the rest under an ellipsis button
+ */
+ withEllipsis: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
+ /**
+ * When set, a tooltip displays when hovering middle ellipsis button
+ */
+ ellipsisTooltipLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ itemUuid: uniqueId('disclosure-hierarchy-'),
+ };
+ },
+ computed: {
+ middleItems() {
+ return this.items.slice(1, -1).map((item) => ({ ...item, text: item.title }));
+ },
+ firstItem() {
+ return this.items[0];
+ },
+ lastItemIndex() {
+ return this.items.length - 1;
+ },
+ lastItem() {
+ return this.items[this.lastItemIndex];
+ },
+ },
+ methods: {
+ itemId(index) {
+ return `${this.itemUuid}-item-${index}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-relative gl-display-flex">
+ <ul class="gl-p-0 gl-m-0 gl-relative gl-list-style-none gl-display-inline-flex gl-w-85p">
+ <template v-if="withEllipsis">
+ <disclosure-hierarchy-item :item="firstItem" :item-id="itemId(0)">
+ <slot :item="firstItem" :item-id="itemId(0)"></slot>
+ </disclosure-hierarchy-item>
+ <li class="disclosure-hierarchy-item">
+ <gl-disclosure-dropdown :items="middleItems">
+ <template #toggle>
+ <button
+ id="disclosure-hierarchy-ellipsis-button"
+ class="disclosure-hierarchy-button"
+ :aria-label="ellipsisTooltipLabel"
+ >
+ <gl-icon name="ellipsis_h" class="gl-ml-3 gl-text-gray-600 gl-z-index-200" />
+ </button>
+ </template>
+ <template #list-item="{ item }">
+ <span class="gl-display-flex">
+ <gl-icon
+ v-if="item.icon"
+ :name="item.icon"
+ class="gl-mr-3 gl-vertical-align-middle gl-text-gray-600 gl-flex-shrink-0"
+ />
+ {{ item.title }}
+ </span>
+ </template>
+ </gl-disclosure-dropdown>
+ </li>
+ <gl-tooltip
+ v-if="ellipsisTooltipLabel"
+ target="disclosure-hierarchy-ellipsis-button"
+ triggers="hover"
+ >
+ {{ ellipsisTooltipLabel }}
+ </gl-tooltip>
+ <disclosure-hierarchy-item :item="lastItem" :item-id="itemId(lastItemIndex)">
+ <slot :item="lastItem" :item-id="itemId(lastItemIndex)"></slot>
+ </disclosure-hierarchy-item>
+ </template>
+ <disclosure-hierarchy-item
+ v-for="(item, index) in items"
+ v-else
+ :key="index"
+ :item="item"
+ :item-id="itemId(index)"
+ >
+ <slot :item="item" :item-id="itemId(index)"></slot>
+ </disclosure-hierarchy-item>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue
new file mode 100644
index 00000000000..8347583f3c5
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/disclosure_hierarchy_item.vue
@@ -0,0 +1,61 @@
+<!-- eslint-disable vue/multi-word-component-names -->
+<script>
+import iconSpriteInfo from '@gitlab/svgs/dist/icons.json';
+import { GlIcon, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlIcon,
+ GlLink,
+ },
+ props: {
+ /**
+ * Path item in the form:
+ * ```
+ * {
+ * title: String, required
+ * icon: String, optional
+ * }
+ * ```
+ */
+ item: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
+ itemId: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ methods: {
+ shouldDisplayIcon(icon) {
+ return icon && iconSpriteInfo.icons.includes(icon);
+ },
+ },
+};
+</script>
+
+<template>
+ <li class="disclosure-hierarchy-item">
+ <gl-link
+ :id="itemId"
+ :href="item.webUrl"
+ class="disclosure-hierarchy-button gl-text-gray-900 gl-hover-text-decoration-none gl-active-text-decoration-none!"
+ >
+ <gl-icon
+ v-if="shouldDisplayIcon(item.icon)"
+ :name="item.icon"
+ class="gl-mx-2 gl-text-gray-600 gl-flex-shrink-0"
+ />
+ <span class="gl-z-index-200 gl-text-truncate">{{ item.title }}</span>
+ </gl-link>
+ <!--
+ @slot Additional content to be displayed in an item.
+ @binding {Object} item The item being rendered.
+ @binding {String} itemId The rendered item's ID.
+ -->
+ <slot :item="item" :item-id="itemId"></slot>
+ </li>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue
new file mode 100644
index 00000000000..bebe5d64761
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_ancestors/work_item_ancestors.vue
@@ -0,0 +1,95 @@
+<script>
+import { GlIcon, GlPopover, GlBadge, GlSprintf } from '@gitlab/ui';
+
+import { createAlert } from '~/alert';
+import { s__ } from '~/locale';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
+import { formatAncestors } from '../../utils';
+import workItemAncestorsQuery from '../../graphql/work_item_ancestors.query.graphql';
+import WorkItemStateBadge from '../work_item_state_badge.vue';
+import DisclosureHierarchy from './disclosure_hierarchy.vue';
+
+export default {
+ i18n: {
+ ancestorLabel: s__('WorkItem|Ancestor'),
+ ancestorsTooltipLabel: s__('WorkItem|Show all ancestors'),
+ },
+ components: {
+ GlIcon,
+ GlPopover,
+ GlBadge,
+ GlSprintf,
+ TimeAgoTooltip,
+ WorkItemStateBadge,
+ DisclosureHierarchy,
+ },
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ ancestors: [],
+ };
+ },
+ apollo: {
+ ancestors: {
+ query: workItemAncestorsQuery,
+ variables() {
+ return {
+ id: this.workItem.id,
+ };
+ },
+ update(data) {
+ return formatAncestors(data.workItem);
+ },
+ skip() {
+ return !this.workItem.id;
+ },
+ error(error) {
+ createAlert({
+ message: s__('Hierarchy|Something went wrong while fetching ancestors.'),
+ captureError: true,
+ error,
+ });
+ },
+ },
+ },
+};
+</script>
+
+<template>
+ <disclosure-hierarchy
+ v-if="ancestors.length > 0"
+ class="gl-mr-auto"
+ :items="ancestors"
+ :with-ellipsis="ancestors.length > 2"
+ :ellipsis-tooltip-label="$options.i18n.ancestorsTooltipLabel"
+ >
+ <template #default="{ item, itemId }">
+ <gl-popover triggers="hover focus" placement="bottom" :target="itemId">
+ <template #title>
+ <gl-badge variant="muted" size="sm">{{ $options.i18n.ancestorLabel }}</gl-badge>
+ <div class="gl-pt-3">
+ {{ item.title }}
+ </div>
+ </template>
+ <div class="gl-pb-3 gl-text-gray-500">
+ <gl-icon v-if="item.icon" :name="item.icon" />
+ <span>{{ item.reference }}</span>
+ </div>
+ <work-item-state-badge v-if="item.state" :work-item-state="item.state" />
+ <span class="gl-text-gray-500">
+ <gl-sprintf v-if="item.createdAt" :message="__('Created %{timeAgo}')">
+ <template #timeAgo>
+ <time-ago-tooltip :time="item.createdAt" />
+ </template>
+ </gl-sprintf>
+ </span>
+ </gl-popover>
+ </template>
+ </disclosure-hierarchy>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
index 7d09a003926..b7206d502a6 100644
--- a/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue
@@ -1,7 +1,6 @@
<script>
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import {
- sprintfWorkItem,
WIDGET_TYPE_ASSIGNEES,
WIDGET_TYPE_HEALTH_STATUS,
WIDGET_TYPE_HIERARCHY,
@@ -19,7 +18,8 @@ import WorkItemDueDate from './work_item_due_date.vue';
import WorkItemAssignees from './work_item_assignees.vue';
import WorkItemLabels from './work_item_labels.vue';
import WorkItemMilestone from './work_item_milestone.vue';
-import WorkItemParent from './work_item_parent.vue';
+import WorkItemParentInline from './work_item_parent_inline.vue';
+import WorkItemParent from './work_item_parent_with_edit.vue';
export default {
components: {
@@ -28,11 +28,17 @@ export default {
WorkItemAssignees,
WorkItemDueDate,
WorkItemParent,
- WorkItemWeight: () => import('ee_component/work_items/components/work_item_weight.vue'),
+ WorkItemParentInline,
+ WorkItemWeightInline: () =>
+ import('ee_component/work_items/components/work_item_weight_inline.vue'),
+ WorkItemWeight: () =>
+ import('ee_component/work_items/components/work_item_weight_with_edit.vue'),
WorkItemProgress: () => import('ee_component/work_items/components/work_item_progress.vue'),
WorkItemIteration: () => import('ee_component/work_items/components/work_item_iteration.vue'),
WorkItemHealthStatus: () =>
- import('ee_component/work_items/components/work_item_health_status.vue'),
+ import('ee_component/work_items/components/work_item_health_status_with_edit.vue'),
+ WorkItemHealthStatusInline: () =>
+ import('ee_component/work_items/components/work_item_health_status_inline.vue'),
},
mixins: [glFeatureFlagMixin()],
props: {
@@ -44,11 +50,6 @@ export default {
type: Object,
required: true,
},
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
},
computed: {
workItemType() {
@@ -60,15 +61,6 @@ export default {
canDelete() {
return this.workItem?.userPermissions?.deleteWorkItem;
},
- canSetWorkItemMetadata() {
- return this.workItem?.userPermissions?.setWorkItemMetadata;
- },
- canAssignUnassignUser() {
- return this.workItemAssignees && this.canSetWorkItemMetadata;
- },
- confidentialTooltip() {
- return sprintfWorkItem(this.$options.i18n.confidentialTooltip, this.workItemType);
- },
workItemAssignees() {
return this.isWidgetPresent(WIDGET_TYPE_ASSIGNEES);
},
@@ -154,16 +146,28 @@ export default {
:can-update="canUpdate"
@error="$emit('error', $event)"
/>
- <work-item-weight
- v-if="workItemWeight"
- class="gl-mb-5"
- :can-update="canUpdate"
- :weight="workItemWeight.weight"
- :work-item-id="workItem.id"
- :work-item-iid="workItem.iid"
- :work-item-type="workItemType"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemWeight">
+ <work-item-weight
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :weight="workItemWeight.weight"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-type="workItemType"
+ @error="$emit('error', $event)"
+ />
+ <work-item-weight-inline
+ v-else
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :weight="workItemWeight.weight"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-type="workItemType"
+ @error="$emit('error', $event)"
+ />
+ </template>
<work-item-progress
v-if="workItemProgress"
class="gl-mb-5"
@@ -184,24 +188,47 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
- <work-item-health-status
- v-if="workItemHealthStatus"
- class="gl-mb-5"
- :health-status="workItemHealthStatus.healthStatus"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :work-item-iid="workItem.iid"
- :work-item-type="workItemType"
- @error="$emit('error', $event)"
- />
- <work-item-parent
- v-if="showWorkItemParent"
- class="gl-mb-5"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :work-item-type="workItemType"
- :parent="workItemParent"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemHealthStatus">
+ <work-item-health-status
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :health-status="workItemHealthStatus.healthStatus"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-type="workItemType"
+ @error="$emit('error', $event)"
+ />
+ <work-item-health-status-inline
+ v-else
+ class="gl-mb-5"
+ :health-status="workItemHealthStatus.healthStatus"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-type="workItemType"
+ @error="$emit('error', $event)"
+ />
+ </template>
+ <template v-if="showWorkItemParent">
+ <work-item-parent
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :parent="workItemParent"
+ @error="$emit('error', $event)"
+ />
+ <work-item-parent-inline
+ v-else
+ class="gl-mb-5"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-type="workItemType"
+ :parent="workItemParent"
+ @error="$emit('error', $event)"
+ />
+ </template>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_detail.vue b/app/assets/javascripts/work_items/components/work_item_detail.vue
index 45d3aa564a5..b74cbc85379 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -1,15 +1,6 @@
<script>
import { isEmpty } from 'lodash';
-import {
- GlAlert,
- GlSkeletonLoader,
- GlLoadingIcon,
- GlIcon,
- GlButton,
- GlTooltipDirective,
- GlEmptyState,
- GlIntersectionObserver,
-} from '@gitlab/ui';
+import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw';
import { s__ } from '~/locale';
import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
@@ -17,7 +8,6 @@ import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isLoggedIn } from '~/lib/utils/common_utils';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import { WORKSPACE_PROJECT } from '~/issues/constants';
import {
i18n,
@@ -27,7 +17,6 @@ import {
WIDGET_TYPE_DESCRIPTION,
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_HIERARCHY,
- WORK_ITEM_TYPE_VALUE_ISSUE,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WIDGET_TYPE_NOTES,
WIDGET_TYPE_LINKED_ITEMS,
@@ -35,7 +24,6 @@ import {
import workItemUpdatedSubscription from '../graphql/work_item_updated.subscription.graphql';
import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
-import updateWorkItemTaskMutation from '../graphql/update_work_item_task.mutation.graphql';
import groupWorkItemByIidQuery from '../graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '../graphql/work_item_by_iid.query.graphql';
import { findHierarchyWidgetChildren } from '../utils';
@@ -51,7 +39,8 @@ import WorkItemNotes from './work_item_notes.vue';
import WorkItemDetailModal from './work_item_detail_modal.vue';
import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
-import WorkItemTypeIcon from './work_item_type_icon.vue';
+import WorkItemStickyHeader from './work_item_sticky_header.vue';
+import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
export default {
i18n,
@@ -62,9 +51,7 @@ export default {
components: {
GlAlert,
GlButton,
- GlLoadingIcon,
GlSkeletonLoader,
- GlIcon,
GlEmptyState,
WorkItemActions,
WorkItemTodos,
@@ -73,14 +60,13 @@ export default {
WorkItemAwardEmoji,
WorkItemTitle,
WorkItemAttributesWrapper,
- WorkItemTypeIcon,
WorkItemTree,
WorkItemNotes,
WorkItemDetailModal,
AbuseCategorySelector,
- GlIntersectionObserver,
- ConfidentialityBadge,
WorkItemRelationships,
+ WorkItemStickyHeader,
+ WorkItemAncestors,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
@@ -95,11 +81,6 @@ export default {
required: false,
default: null,
},
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
},
data() {
return {
@@ -175,9 +156,6 @@ export default {
workItemTypeId() {
return this.workItem.workItemType?.id;
},
- workItemBreadcrumbReference() {
- return this.workItemType ? `#${this.workItem.iid}` : '';
- },
canUpdate() {
return this.workItem.userPermissions?.updateWorkItem;
},
@@ -190,35 +168,15 @@ export default {
canAssignUnassignUser() {
return this.workItemAssignees && this.canSetWorkItemMetadata;
},
- projectFullPath() {
- return this.workItem.namespace?.fullPath;
- },
workItemsMvc2Enabled() {
return this.glFeatures.workItemsMvc2;
},
parentWorkItem() {
return this.isWidgetPresent(WIDGET_TYPE_HIERARCHY)?.parent;
},
- parentWorkItemType() {
- return this.parentWorkItem?.workItemType?.name;
- },
- parentWorkItemIconName() {
- return this.parentWorkItem?.workItemType?.iconName;
- },
parentWorkItemConfidentiality() {
return this.parentWorkItem?.confidential;
},
- parentWorkItemReference() {
- return this.parentWorkItem ? `${this.parentWorkItem.title} #${this.parentWorkItem.iid}` : '';
- },
- parentUrl() {
- // Once more types are moved to have Work Items involved
- // we need to handle this properly.
- if (this.parentWorkItemType === WORK_ITEM_TYPE_VALUE_ISSUE) {
- return `../../-/issues/${this.parentWorkItem?.iid}`;
- }
- return this.parentWorkItem?.webUrl;
- },
workItemIconName() {
return this.workItem.workItemType?.iconName;
},
@@ -235,7 +193,7 @@ export default {
return this.isWidgetPresent(WIDGET_TYPE_CURRENT_USER_TODOS);
},
showWorkItemCurrentUserTodos() {
- return this.$options.isLoggedIn && this.workItemCurrentUserTodos;
+ return Boolean(this.$options.isLoggedIn && this.workItemCurrentUserTodos);
},
currentUserTodos() {
return this.workItemCurrentUserTodos?.currentUserTodos?.nodes;
@@ -284,6 +242,12 @@ export default {
'gl-display-none gl-sm-display-block!': this.parentWorkItem,
};
},
+ headerWrapperClass() {
+ return {
+ 'flex-wrap': this.parentWorkItem,
+ 'gl-display-block gl-md-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-md-flex-direction-row gl-gap-3 gl-pt-3': true,
+ };
+ },
},
mounted() {
if (this.modalWorkItemIid) {
@@ -299,34 +263,21 @@ export default {
},
toggleConfidentiality(confidentialStatus) {
this.updateInProgress = true;
- let updateMutation = updateWorkItemMutation;
- let inputVariables = {
- id: this.workItem.id,
- confidential: confidentialStatus,
- };
-
- if (this.parentWorkItem) {
- updateMutation = updateWorkItemTaskMutation;
- inputVariables = {
- id: this.parentWorkItem.id,
- taskData: {
- id: this.workItem.id,
- confidential: confidentialStatus,
- },
- };
- }
this.$apollo
.mutate({
- mutation: updateMutation,
+ mutation: updateWorkItemMutation,
variables: {
- input: inputVariables,
+ input: {
+ id: this.workItem.id,
+ confidential: confidentialStatus,
+ },
},
})
.then(
({
data: {
- workItemUpdate: { errors, workItem, task },
+ workItemUpdate: { errors, workItem },
},
}) => {
if (errors?.length) {
@@ -334,7 +285,7 @@ export default {
}
this.$emit('workItemUpdated', {
- confidential: workItem?.confidential || task?.confidential,
+ confidential: workItem?.confidential,
});
},
)
@@ -430,38 +381,8 @@ export default {
@click="$emit('close')"
/>
</div>
- <div
- class="gl-display-block gl-sm-display-flex! gl-align-items-flex-start gl-flex-direction-column gl-sm-flex-direction-row gl-gap-3 gl-pt-3"
- >
- <ul
- v-if="parentWorkItem"
- class="list-unstyled gl-display-flex gl-min-w-0 gl-mr-auto gl-mb-0 gl-z-index-0"
- data-testid="work-item-parent"
- >
- <li class="gl-ml-n4 gl-display-flex gl-align-items-center gl-min-w-0">
- <gl-button
- v-gl-tooltip.hover
- class="gl-text-truncate"
- :icon="parentWorkItemIconName"
- category="tertiary"
- :href="parentUrl"
- :title="parentWorkItemReference"
- @click="openInModal({ event: $event, modalWorkItem: parentWorkItem })"
- >{{ parentWorkItemReference }}</gl-button
- >
- <gl-icon name="chevron-right" :size="16" class="gl-flex-shrink-0" />
- </li>
- <li
- class="gl-px-4 gl-py-3 gl-line-height-0 gl-display-flex gl-align-items-center gl-overflow-hidden gl-flex-shrink-0"
- >
- <work-item-type-icon
- :work-item-icon-name="workItemIconName"
- :work-item-type="workItemType"
- show-text
- />
- {{ workItemBreadcrumbReference }}
- </li>
- </ul>
+ <div :class="headerWrapperClass">
+ <work-item-ancestors v-if="parentWorkItem" :work-item="workItem" class="gl-mb-1" />
<div
v-if="!error && !workItemLoading"
:class="titleClassHeader"
@@ -474,17 +395,18 @@ export default {
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
- :work-item-parent-id="workItemParentId"
:can-update="canUpdate"
@error="updateError = $event"
/>
</div>
- <div class="detail-page-header-actions gl-display-flex gl-align-self-start gl-gap-3">
+ <div
+ class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3"
+ >
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-fullpath="projectFullPath"
+ :work-item-fullpath="fullPath"
:current-user-todos="currentUserTodos"
@error="updateError = $event"
/>
@@ -502,7 +424,6 @@ export default {
:work-item-create-note-email="workItem.createNoteEmail"
:is-modal="isModal"
:work-item-state="workItem.state"
- :work-item-parent-id="workItemParentId"
@deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
@toggleWorkItemConfidentiality="toggleConfidentiality"
@error="updateError = $event"
@@ -527,8 +448,8 @@ export default {
:work-item-id="workItem.id"
:work-item-title="workItem.title"
:work-item-type="workItemType"
- :work-item-parent-id="workItemParentId"
:can-update="canUpdate"
+ :use-h1="!isModal"
@error="updateError = $event"
/>
<work-item-created-updated
@@ -537,62 +458,24 @@ export default {
:update-in-progress="updateInProgress"
/>
</div>
- <gl-intersection-observer
+ <work-item-sticky-header
v-if="showIntersectionObserver"
- @appear="hideStickyHeader"
- @disappear="showStickyHeader"
- >
- <transition name="issuable-header-slide">
- <div
- v-if="isStickyHeaderShowing"
- class="issue-sticky-header gl-fixed gl-bg-white gl-border-b gl-z-index-3 gl-py-2"
- data-testid="work-item-sticky-header"
- >
- <div
- class="gl-align-items-center gl-mx-auto gl-px-5 gl-display-flex gl-max-w-container-xl"
- >
- <span class="gl-text-truncate gl-font-weight-bold gl-pr-3 gl-mr-auto">
- {{ workItem.title }}
- </span>
- <gl-loading-icon v-if="updateInProgress" class="gl-mr-3" />
- <confidentiality-badge
- v-if="workItem.confidential"
- class="gl-mr-3"
- :issuable-type="workItemType"
- :workspace-type="$options.WORKSPACE_PROJECT"
- />
- <work-item-todos
- v-if="showWorkItemCurrentUserTodos"
- :work-item-id="workItem.id"
- :work-item-iid="workItemIid"
- :work-item-fullpath="projectFullPath"
- :current-user-todos="currentUserTodos"
- @error="updateError = $event"
- />
- <work-item-actions
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :subscribed-to-notifications="workItemNotificationsSubscribed"
- :work-item-type="workItemType"
- :work-item-type-id="workItemTypeId"
- :can-delete="canDelete"
- :can-update="canUpdate"
- :is-confidential="workItem.confidential"
- :is-parent-confidential="parentWorkItemConfidentiality"
- :work-item-reference="workItem.reference"
- :work-item-create-note-email="workItem.createNoteEmail"
- :is-modal="isModal"
- @deleteWorkItem="
- $emit('deleteWorkItem', { workItemType, workItemId: workItem.id })
- "
- @toggleWorkItemConfidentiality="toggleConfidentiality"
- @error="updateError = $event"
- @promotedToObjective="$emit('promotedToObjective', workItemIid)"
- />
- </div>
- </div>
- </transition>
- </gl-intersection-observer>
+ :current-user-todos="currentUserTodos"
+ :show-work-item-current-user-todos="showWorkItemCurrentUserTodos"
+ :parent-work-item-confidentiality="parentWorkItemConfidentiality"
+ :update-in-progress="updateInProgress"
+ :full-path="fullPath"
+ :is-modal="isModal"
+ :work-item="workItem"
+ :is-sticky-header-showing="isStickyHeaderShowing"
+ :work-item-notifications-subscribed="workItemNotificationsSubscribed"
+ @hideStickyHeader="hideStickyHeader"
+ @showStickyHeader="showStickyHeader"
+ @deleteWorkItem="$emit('deleteWorkItem', { workItemType, workItemId: workItem.id })"
+ @toggleWorkItemConfidentiality="toggleConfidentiality"
+ @error="updateError = $event"
+ @promotedToObjective="$emit('promotedToObjective', workItemIid)"
+ />
<div
data-testid="work-item-overview"
:class="{ 'work-item-overview': workItemsMvc2Enabled }"
@@ -603,7 +486,6 @@ export default {
class="gl-border-b"
:full-path="fullPath"
:work-item="workItem"
- :work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
<work-item-description
@@ -617,7 +499,7 @@ export default {
<work-item-award-emoji
v-if="workItemAwardEmoji"
:work-item-id="workItem.id"
- :work-item-fullpath="projectFullPath"
+ :work-item-fullpath="fullPath"
:award-emoji="workItemAwardEmoji.awardEmoji"
:work-item-iid="workItemIid"
@error="updateError = $event"
@@ -640,7 +522,7 @@ export default {
v-if="showWorkItemLinkedItems"
:work-item-id="workItem.id"
:work-item-iid="workItemIid"
- :work-item-full-path="projectFullPath"
+ :work-item-full-path="fullPath"
:work-item-type="workItem.workItemType.name"
@showModal="openInModal"
/>
@@ -656,6 +538,7 @@ export default {
:report-abuse-path="reportAbusePath"
:is-work-item-confidential="workItem.confidential"
class="gl-pt-5"
+ :use-h2="!isModal"
@error="updateError = $event"
@has-notes="updateHasNotes"
@openReportAbuse="openReportAbuseDrawer"
@@ -677,7 +560,6 @@ export default {
<work-item-attributes-wrapper
:full-path="fullPath"
:work-item="workItem"
- :work-item-parent-id="workItemParentId"
@error="updateError = $event"
/>
</aside>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
deleted file mode 100644
index c5be1a3ead3..00000000000
--- a/app/assets/javascripts/work_items/components/work_item_links/okr_actions_split_button.vue
+++ /dev/null
@@ -1,72 +0,0 @@
-<script>
-import { GlDisclosureDropdown } from '@gitlab/ui';
-
-import { s__, __ } from '~/locale';
-
-const objectiveActionItems = [
- {
- title: s__('OKR|New objective'),
- eventName: 'showCreateObjectiveForm',
- },
- {
- title: s__('OKR|Existing objective'),
- eventName: 'showAddObjectiveForm',
- },
-];
-
-const keyResultActionItems = [
- {
- title: s__('OKR|New key result'),
- eventName: 'showCreateKeyResultForm',
- },
- {
- title: s__('OKR|Existing key result'),
- eventName: 'showAddKeyResultForm',
- },
-];
-
-export default {
- keyResultActionItems,
- objectiveActionItems,
- components: {
- GlDisclosureDropdown,
- },
- computed: {
- objectiveDropdownItems() {
- return {
- name: __('Objective'),
- items: this.$options.objectiveActionItems.map((item) => ({
- text: item.title,
- action: () => this.change(item),
- })),
- };
- },
- keyResultDropdownItems() {
- return {
- name: __('Key result'),
- items: this.$options.keyResultActionItems.map((item) => ({
- text: item.title,
- action: () => this.change(item),
- })),
- };
- },
- dropdownItems() {
- return [this.objectiveDropdownItems, this.keyResultDropdownItems];
- },
- },
- methods: {
- change({ eventName }) {
- this.$emit(eventName);
- },
- },
-};
-</script>
-
-<template>
- <gl-disclosure-dropdown
- :toggle-text="__('Add')"
- size="small"
- placement="right"
- :items="dropdownItems"
- />
-</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_actions_split_button.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_actions_split_button.vue
new file mode 100644
index 00000000000..1290fc49df3
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_actions_split_button.vue
@@ -0,0 +1,24 @@
+<script>
+import { GlDisclosureDropdown } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlDisclosureDropdown,
+ },
+ props: {
+ actions: {
+ type: Array,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ :toggle-text="__('Add')"
+ size="small"
+ placement="right"
+ :items="actions"
+ />
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
index b6ea09edbd4..ca62f3c4693 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_children_wrapper.vue
@@ -6,7 +6,7 @@ import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { s__ } from '~/locale';
-import { defaultSortableOptions } from '~/sortable/constants';
+import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { WORK_ITEM_TYPE_VALUE_OBJECTIVE } from '../../constants';
import { findHierarchyWidgets } from '../../utils';
@@ -77,6 +77,8 @@ export default {
'ghost-class': 'tree-item-drag-active',
'data-parent-id': this.workItemId,
value: this.children,
+ delay: DRAG_DELAY,
+ delayOnTouchOnly: true,
};
return this.canReorder ? options : {};
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
index 49454c3d9f3..f43718c4cb8 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_link_child.vue
@@ -213,10 +213,10 @@ export default {
</script>
<template>
- <li class="tree-item">
+ <li class="tree-item gl-p-0! gl-border-bottom-0!">
<div
class="gl-display-flex gl-align-items-flex-start"
- :class="{ 'gl-ml-6': canHaveChildren && !hasChildren && hasIndirectChildren }"
+ :class="{ 'gl-ml-5 gl-pl-2': canHaveChildren && !hasChildren && hasIndirectChildren }"
>
<gl-button
v-if="hasChildren"
@@ -227,7 +227,7 @@ export default {
category="tertiary"
size="small"
:loading="isLoadingChildren"
- class="gl-px-0! gl-py-3! gl-mr-3"
+ class="gl-px-0! gl-py-3! gl-mr-2"
data-testid="expand-child"
@click="toggleItem"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
index dd0a26c0b9c..1e323d99c93 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links.vue
@@ -201,10 +201,12 @@ export default {
},
},
i18n: {
- title: s__('WorkItem|Tasks'),
- fetchError: s__('WorkItem|Something went wrong when fetching tasks. Please refresh this page.'),
+ title: s__('WorkItem|Child items'),
+ fetchError: s__(
+ 'WorkItem|Something went wrong when fetching child items. Please refresh this page.',
+ ),
emptyStateMessage: s__(
- 'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
+ 'WorkItem|No child items are currently assigned. Use child items to break down this issue into smaller parts.',
),
addChildButtonLabel: s__('WorkItem|Add'),
addChildOptionLabel: s__('WorkItem|Existing task'),
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
index 3d09a90169c..2ba9e1bd3e6 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_tree.vue
@@ -1,15 +1,20 @@
<script>
import { GlToggle } from '@gitlab/ui';
+import { sprintf, s__ } from '~/locale';
import {
FORM_TYPES,
WIDGET_TYPE_HIERARCHY,
WORK_ITEMS_TREE_TEXT_MAP,
+ WORK_ITEM_TYPE_VALUE_MAP,
+ WORK_ITEMS_TYPE_MAP,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
I18N_WORK_ITEM_SHOW_LABELS,
} from '../../constants';
+import { findHierarchyWidgetDefinition } from '../../utils';
+import getAllowedWorkItemChildTypes from '../../graphql/work_item_allowed_children.query.graphql';
import WidgetWrapper from '../widget_wrapper.vue';
-import OkrActionsSplitButton from './okr_actions_split_button.vue';
+import WorkItemActionsSplitButton from './work_item_actions_split_button.vue';
import WorkItemLinksForm from './work_item_links_form.vue';
import WorkItemChildrenWrapper from './work_item_children_wrapper.vue';
@@ -19,7 +24,7 @@ export default {
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
WORK_ITEM_TYPE_ENUM_KEY_RESULT,
components: {
- OkrActionsSplitButton,
+ WorkItemActionsSplitButton,
WidgetWrapper,
WorkItemLinksForm,
WorkItemChildrenWrapper,
@@ -72,8 +77,23 @@ export default {
childType: null,
widgetName: 'tasks',
showLabels: true,
+ allowedChildrenTypes: [],
};
},
+ apollo: {
+ allowedChildrenTypes: {
+ query: getAllowedWorkItemChildTypes,
+ variables() {
+ return {
+ id: this.workItemId,
+ };
+ },
+ update(data) {
+ return findHierarchyWidgetDefinition(data.workItem.workItemType.widgetDefinitions)
+ .allowedChildTypes.nodes;
+ },
+ },
+ },
computed: {
childrenIds() {
return this.children.map((c) => c.id);
@@ -85,8 +105,37 @@ export default {
)
.some((hierarchy) => hierarchy.hasChildren);
},
+ addItemsActions() {
+ const reorderedChildTypes = this.allowedChildrenTypes
+ .slice()
+ .sort((a, b) => a.id.localeCompare(b.id));
+ return reorderedChildTypes.map((type) => {
+ const enumType = WORK_ITEM_TYPE_VALUE_MAP[type.name];
+ return {
+ name: WORK_ITEMS_TYPE_MAP[enumType].name,
+ items: this.genericActionItems(type.name).map((item) => ({
+ text: item.title,
+ action: item.action,
+ })),
+ };
+ });
+ },
},
methods: {
+ genericActionItems(workItem) {
+ const enumType = WORK_ITEM_TYPE_VALUE_MAP[workItem];
+ const workItemName = WORK_ITEMS_TYPE_MAP[enumType].name.toLowerCase();
+ return [
+ {
+ title: sprintf(s__('WorkItem|New %{workItemName}'), { workItemName }),
+ action: () => this.showAddForm(FORM_TYPES.create, enumType),
+ },
+ {
+ title: sprintf(s__('WorkItem|Existing %{workItemName}'), { workItemName }),
+ action: () => this.showAddForm(FORM_TYPES.add, enumType),
+ },
+ ];
+ },
showAddForm(formType, childType) {
this.$refs.wrapper.show();
this.isShownAddForm = true;
@@ -129,56 +178,42 @@ export default {
label-id="relationship-toggle-labels"
@change="showLabels = $event"
/>
- <okr-actions-split-button
- v-if="canUpdate"
- @showCreateObjectiveForm="
- showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
- "
- @showAddObjectiveForm="
- showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_OBJECTIVE)
- "
- @showCreateKeyResultForm="
- showAddForm($options.FORM_TYPES.create, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
- "
- @showAddKeyResultForm="
- showAddForm($options.FORM_TYPES.add, $options.WORK_ITEM_TYPE_ENUM_KEY_RESULT)
- "
- />
+ <work-item-actions-split-button v-if="canUpdate" :actions="addItemsActions" />
</template>
<template #body>
- <div v-if="!isShownAddForm && children.length === 0" data-testid="tree-empty">
- <div class="gl-new-card-content">
+ <div class="gl-new-card-content">
+ <div v-if="!isShownAddForm && children.length === 0" data-testid="tree-empty">
<p class="gl-new-card-empty">
{{ $options.WORK_ITEMS_TREE_TEXT_MAP[workItemType].empty }}
</p>
</div>
+ <work-item-links-form
+ v-if="isShownAddForm"
+ ref="wiLinksForm"
+ data-testid="add-tree-form"
+ :full-path="fullPath"
+ :issuable-gid="workItemId"
+ :work-item-iid="workItemIid"
+ :form-type="formType"
+ :parent-work-item-type="parentWorkItemType"
+ :children-type="childType"
+ :children-ids="childrenIds"
+ :parent-confidential="confidential"
+ @cancel="hideAddForm"
+ @addChild="$emit('addChild')"
+ />
+ <work-item-children-wrapper
+ :children="children"
+ :can-update="canUpdate"
+ :full-path="fullPath"
+ :work-item-id="workItemId"
+ :work-item-iid="workItemIid"
+ :work-item-type="workItemType"
+ :show-labels="showLabels"
+ @error="error = $event"
+ @show-modal="showModal"
+ />
</div>
- <work-item-links-form
- v-if="isShownAddForm"
- ref="wiLinksForm"
- data-testid="add-tree-form"
- :full-path="fullPath"
- :issuable-gid="workItemId"
- :work-item-iid="workItemIid"
- :form-type="formType"
- :parent-work-item-type="parentWorkItemType"
- :children-type="childType"
- :children-ids="childrenIds"
- :parent-confidential="confidential"
- @cancel="hideAddForm"
- @addChild="$emit('addChild')"
- />
- <work-item-children-wrapper
- :children="children"
- :can-update="canUpdate"
- :full-path="fullPath"
- :work-item-id="workItemId"
- :work-item-iid="workItemIid"
- :work-item-type="workItemType"
- :show-labels="showLabels"
- @error="error = $event"
- @show-modal="showModal"
- />
</template>
</widget-wrapper>
</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone.vue
index 9c6fa158169..dbeb3d4d3ff 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone.vue
@@ -47,7 +47,7 @@ export default {
workItemMilestone: {
type: Object,
required: false,
- default: () => {},
+ default: () => ({}),
},
workItemType: {
type: String,
@@ -155,9 +155,6 @@ export default {
},
},
methods: {
- handleMilestoneClick(milestone) {
- this.localMilestone = milestone;
- },
onDropdownShown() {
this.shouldFetch = true;
},
@@ -168,9 +165,6 @@ export default {
setSearchKey(value) {
this.searchTerm = value;
},
- isMilestoneChecked(milestone) {
- return this.localMilestone?.id === milestone?.id;
- },
updateMilestone() {
this.localMilestone =
this.milestones.find(({ id }) => id === this.localMilestoneId) ?? noMilestoneItem;
@@ -234,7 +228,6 @@ export default {
v-model="localMilestoneId"
:items="dropdownGroups"
category="tertiary"
- data-testid="work-item-milestone-dropdown"
class="gl-max-w-full"
:toggle-text="dropdownText"
:loading="updateInProgress"
diff --git a/app/assets/javascripts/work_items/components/work_item_notes.vue b/app/assets/javascripts/work_items/components/work_item_notes.vue
index 6756acd4495..faf43c3d5dd 100644
--- a/app/assets/javascripts/work_items/components/work_item_notes.vue
+++ b/app/assets/javascripts/work_items/components/work_item_notes.vue
@@ -28,6 +28,7 @@ import workItemNoteCreatedSubscription from '~/work_items/graphql/notes/work_ite
import workItemNoteUpdatedSubscription from '~/work_items/graphql/notes/work_item_note_updated.subscription.graphql';
import workItemNoteDeletedSubscription from '~/work_items/graphql/notes/work_item_note_deleted.subscription.graphql';
import deleteNoteMutation from '../graphql/notes/delete_work_item_notes.mutation.graphql';
+import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql';
import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql';
import WorkItemAddNote from './notes/work_item_add_note.vue';
@@ -46,6 +47,7 @@ export default {
WorkItemNotesActivityHeader,
WorkItemHistoryOnlyFilterNote,
},
+ inject: ['isGroup'],
props: {
fullPath: {
type: String,
@@ -87,6 +89,11 @@ export default {
required: false,
default: false,
},
+ useH2: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
data() {
return {
@@ -169,7 +176,9 @@ export default {
},
apollo: {
workItemNotes: {
- query: workItemNotesByIidQuery,
+ query() {
+ return this.isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
@@ -326,6 +335,7 @@ export default {
:disable-activity-filter-sort="disableActivityFilterSort"
:work-item-type="workItemType"
:discussion-filter="discussionFilter"
+ :use-h2="useH2"
@changeSort="changeNotesSortOrder"
@changeFilter="filterDiscussions"
/>
diff --git a/app/assets/javascripts/work_items/components/work_item_parent.vue b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
index ce30f7985cf..0c0842a3e05 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
@@ -211,6 +211,7 @@ export default {
id="work-item-parent-listbox-value"
class="gl-max-w-max-content"
data-testid="work-item-parent-listbox"
+ block
searchable
is-check-centered
category="tertiary"
diff --git a/app/assets/javascripts/work_items/components/work_item_parent_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_parent_with_edit.vue
new file mode 100644
index 00000000000..75c49ed5027
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_parent_with_edit.vue
@@ -0,0 +1,295 @@
+<script>
+import { GlButton, GlForm, GlLink, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import { s__ } from '~/locale';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+
+import { removeHierarchyChild } from '../graphql/cache_utils';
+import groupWorkItemsQuery from '../graphql/group_work_items.query.graphql';
+import projectWorkItemsQuery from '../graphql/project_work_items.query.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ SUPPORTED_PARENT_TYPE_MAP,
+} from '../constants';
+
+export default {
+ inputId: 'work-item-parent-listbox-value',
+ noWorkItemId: 'no-work-item-id',
+ i18n: {
+ assignParentLabel: s__('WorkItem|Assign parent'),
+ parentLabel: s__('WorkItem|Parent'),
+ none: s__('WorkItem|None'),
+ noMatchingResults: s__('WorkItem|No matching results'),
+ unAssign: s__('WorkItem|Unassign'),
+ workItemsFetchError: s__(
+ 'WorkItem|Something went wrong while fetching items. Please try again.',
+ ),
+ },
+ components: {
+ GlButton,
+ GlLoadingIcon,
+ GlLink,
+ GlForm,
+ GlCollapsibleListbox,
+ },
+ inject: ['fullPath', 'isGroup'],
+ props: {
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ search: '',
+ updateInProgress: false,
+ searchStarted: false,
+ availableWorkItems: [],
+ localSelectedItem: this.parent?.id,
+ oldParent: this.parent,
+ };
+ },
+ computed: {
+ hasParent() {
+ return this.parent !== null;
+ },
+ isLoading() {
+ return this.$apollo.queries.availableWorkItems.loading;
+ },
+ listboxText() {
+ return (
+ this.workItems.find(({ value }) => this.localSelectedItem === value)?.text ||
+ this.parent?.title ||
+ this.$options.i18n.none
+ );
+ },
+ workItems() {
+ return this.availableWorkItems.map(({ id, title }) => ({ text: title, value: id }));
+ },
+ parentType() {
+ return SUPPORTED_PARENT_TYPE_MAP[this.workItemType];
+ },
+ },
+ watch: {
+ parent: {
+ handler(newVal) {
+ if (!this.isEditing) {
+ this.localSelectedItem = newVal?.id;
+ }
+ },
+ },
+ },
+ created() {
+ this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
+ },
+ apollo: {
+ availableWorkItems: {
+ query() {
+ return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
+ },
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.search,
+ types: this.parentType,
+ in: this.search ? 'TITLE' : undefined,
+ iid: null,
+ isNumber: false,
+ };
+ },
+ skip() {
+ return !this.searchStarted;
+ },
+ update(data) {
+ return data.workspace.workItems.nodes.filter((wi) => this.workItemId !== wi.id) || [];
+ },
+ error() {
+ this.$emit('error', this.$options.i18n.workItemsFetchError);
+ },
+ },
+ },
+ methods: {
+ blurInput() {
+ this.$refs.input.$el.blur();
+ },
+ handleFocus() {
+ this.isEditing = true;
+ },
+ setSearchKey(value) {
+ this.search = value;
+ },
+ async updateParent() {
+ if (this.parent?.id === this.localSelectedItem) return;
+
+ this.updateInProgress = true;
+ try {
+ const {
+ data: {
+ workItemUpdate: { errors },
+ },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ hierarchyWidget: {
+ parentId:
+ this.localSelectedItem === this.$options.noWorkItemId
+ ? null
+ : this.localSelectedItem,
+ },
+ },
+ },
+ update: (cache) =>
+ removeHierarchyChild({
+ cache,
+ fullPath: this.fullPath,
+ iid: this.oldParent?.iid,
+ isGroup: this.isGroup,
+ workItem: { id: this.workItemId },
+ }),
+ });
+
+ if (errors.length) {
+ this.$emit('error', errors.join('\n'));
+ this.localSelectedItem = this.parent?.id || this.$options.noWorkItemId;
+ }
+ } catch (error) {
+ this.$emit('error', sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType));
+ Sentry.captureException(error);
+ } finally {
+ this.updateInProgress = false;
+ this.isEditing = false;
+ }
+ },
+ handleItemClick(item) {
+ this.localSelectedItem = item;
+ this.searchStarted = false;
+ this.search = '';
+ this.updateParent();
+ },
+ unassignParent() {
+ this.localSelectedItem = this.$options.noWorkItemId;
+ this.isEditing = false;
+ this.updateParent();
+ },
+ onListboxShown() {
+ this.searchStarted = true;
+ },
+ onListboxHide() {
+ this.searchStarted = false;
+ this.search = '';
+ this.isEditing = false;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-align-items-center">
+ <!-- hide header when editing, since we then have a form label. Keep it reachable for screenreader nav -->
+ <h3 :class="{ 'gl-sr-only': isEditing }" class="gl-mb-0! gl-heading-scale-5">
+ {{ __('Parent') }}
+ </h3>
+ <gl-loading-icon
+ v-if="updateInProgress"
+ data-testid="loading-icon-parent"
+ size="sm"
+ inline
+ class="gl-ml-2 gl-my-0"
+ />
+ <gl-button
+ v-if="canUpdate && !isEditing"
+ data-testid="edit-parent"
+ category="tertiary"
+ size="small"
+ class="gl-ml-auto gl-mr-2"
+ :disabled="updateInProgress"
+ @click="isEditing = true"
+ >{{ __('Edit') }}</gl-button
+ >
+ </div>
+ <gl-form v-if="isEditing" class="gl-flex-nowrap" data-testid="work-item-parent-form">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <label :for="$options.inputId" class="gl-mb-0">{{ __('Parent') }}</label>
+ <gl-button
+ data-testid="apply-parent"
+ category="tertiary"
+ size="small"
+ class="gl-mr-2"
+ :disabled="updateInProgress"
+ @click="isEditing = false"
+ >{{ __('Apply') }}</gl-button
+ >
+ </div>
+ <div>
+ <!-- wrapper for the form input so the borders fit inside the sidebar -->
+ <div class="gl-pr-2 gl-relative">
+ <gl-collapsible-listbox
+ id="$options.inputId"
+ ref="input"
+ class="gl-display-block"
+ data-testid="work-item-parent-listbox"
+ block
+ searchable
+ start-opened
+ is-check-centered
+ category="primary"
+ fluid-width
+ :searching="isLoading"
+ :header-text="$options.i18n.assignParentLabel"
+ :no-results-text="$options.i18n.noMatchingResults"
+ :loading="updateInProgress"
+ :items="workItems"
+ :toggle-text="listboxText"
+ :selected="localSelectedItem"
+ :reset-button-label="$options.i18n.unAssign"
+ @reset="unassignParent"
+ @search="debouncedSearchKeyUpdate"
+ @select="handleItemClick"
+ @shown="onListboxShown"
+ @hidden="onListboxHide"
+ >
+ <template #list-item="{ item }">
+ <div @click="handleItemClick(item.value, $event)">
+ {{ item.text }}
+ </div>
+ </template>
+ </gl-collapsible-listbox>
+ </div>
+ </div>
+ </gl-form>
+ <template v-else-if="hasParent">
+ <gl-link
+ data-testid="work-item-parent-link"
+ class="gl-link gl-text-gray-900 gl-display-inline-block gl-max-w-full gl-white-space-nowrap gl-text-overflow-ellipsis gl-overflow-hidden"
+ :href="parent.webUrl"
+ >{{ listboxText }}</gl-link
+ >
+ </template>
+ <template v-else>
+ <div data-testid="work-item-parent-none" class="gl-text-secondary">{{ __('None') }}</div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_state_toggle.vue b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
index 581ef9ec945..69752967efe 100644
--- a/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
+++ b/app/assets/javascripts/work_items/components/work_item_state_toggle.vue
@@ -3,7 +3,6 @@ import { GlButton, GlDisclosureDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import Tracking from '~/tracking';
import { __ } from '~/locale';
-import { getUpdateWorkItemMutation } from '~/work_items/components/update_work_item';
import {
sprintfWorkItem,
I18N_WORK_ITEM_ERROR_UPDATING,
@@ -12,6 +11,7 @@ import {
STATE_EVENT_REOPEN,
TRACKING_CATEGORY_SHOW,
} from '../constants';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
export default {
components: {
@@ -33,11 +33,6 @@ export default {
type: String,
required: true,
},
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
showAsDropdownItem: {
type: Boolean,
required: false,
@@ -75,24 +70,19 @@ export default {
},
methods: {
async updateWorkItem() {
- const input = {
- id: this.workItemId,
- stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
- };
-
this.updateInProgress = true;
try {
this.track('updated_state');
- const { mutation, variables } = getUpdateWorkItemMutation({
- workItemParentId: this.workItemParentId,
- input,
- });
-
const { data } = await this.$apollo.mutate({
- mutation,
- variables,
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ stateEvent: this.isWorkItemOpen ? STATE_EVENT_CLOSE : STATE_EVENT_REOPEN,
+ },
+ },
});
const errors = data.workItemUpdate?.errors;
@@ -102,7 +92,6 @@ export default {
}
} catch (error) {
const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
-
this.$emit('error', msg);
Sentry.captureException(error);
}
diff --git a/app/assets/javascripts/work_items/components/work_item_sticky_header.vue b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue
new file mode 100644
index 00000000000..523b145d9ef
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_sticky_header.vue
@@ -0,0 +1,136 @@
+<script>
+import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
+import { WORKSPACE_PROJECT } from '~/issues/constants';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+import WorkItemActions from './work_item_actions.vue';
+import WorkItemTodos from './work_item_todos.vue';
+
+export default {
+ components: {
+ GlIntersectionObserver,
+ GlLoadingIcon,
+ WorkItemActions,
+ WorkItemTodos,
+ ConfidentialityBadge,
+ },
+ props: {
+ workItem: {
+ type: Object,
+ required: true,
+ },
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ isStickyHeaderShowing: {
+ type: Boolean,
+ required: true,
+ },
+ workItemNotificationsSubscribed: {
+ type: Boolean,
+ required: true,
+ },
+ updateInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ parentWorkItemConfidentiality: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ showWorkItemCurrentUserTodos: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isModal: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ currentUserTodos: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ },
+ computed: {
+ canUpdate() {
+ return this.workItem.userPermissions?.updateWorkItem;
+ },
+ canDelete() {
+ return this.workItem.userPermissions?.deleteWorkItem;
+ },
+ workItemType() {
+ return this.workItem.workItemType?.name;
+ },
+ workItemTypeId() {
+ return this.workItem.workItemType?.id;
+ },
+ projectFullPath() {
+ return this.workItem.namespace?.fullPath;
+ },
+ },
+ WORKSPACE_PROJECT,
+};
+</script>
+
+<template>
+ <gl-intersection-observer
+ @appear="$emit('hideStickyHeader')"
+ @disappear="$emit('showStickyHeader')"
+ >
+ <transition name="issuable-header-slide">
+ <div
+ v-if="isStickyHeaderShowing"
+ class="issue-sticky-header gl-fixed gl-bg-white gl-border-b gl-z-index-3 gl-py-2"
+ data-testid="work-item-sticky-header"
+ >
+ <div
+ class="gl-align-items-center gl-mx-auto gl-px-5 gl-display-flex gl-max-w-container-xl gl-gap-3"
+ >
+ <span class="gl-text-truncate gl-font-weight-bold gl-pr-3 gl-mr-auto">
+ {{ workItem.title }}
+ </span>
+ <gl-loading-icon v-if="updateInProgress" />
+ <confidentiality-badge
+ v-if="workItem.confidential"
+ :issuable-type="workItemType"
+ :workspace-type="$options.WORKSPACE_PROJECT"
+ />
+ <work-item-todos
+ v-if="showWorkItemCurrentUserTodos"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-fullpath="projectFullPath"
+ :current-user-todos="currentUserTodos"
+ @error="$emit('error')"
+ />
+ <work-item-actions
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :subscribed-to-notifications="workItemNotificationsSubscribed"
+ :work-item-type="workItemType"
+ :work-item-type-id="workItemTypeId"
+ :can-delete="canDelete"
+ :can-update="canUpdate"
+ :is-confidential="workItem.confidential"
+ :is-parent-confidential="parentWorkItemConfidentiality"
+ :work-item-reference="workItem.reference"
+ :work-item-create-note-email="workItem.createNoteEmail"
+ :work-item-state="workItem.state"
+ :is-modal="isModal"
+ @deleteWorkItem="$emit('deleteWorkItem')"
+ @toggleWorkItemConfidentiality="
+ $emit('toggleWorkItemConfidentiality', !workItem.confidential)
+ "
+ @error="$emit('error')"
+ @promotedToObjective="$emit('promotedToObjective')"
+ />
+ </div>
+ </div>
+ </transition>
+ </gl-intersection-observer>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_title.vue b/app/assets/javascripts/work_items/components/work_item_title.vue
index 9b5803421dd..0be57e291f4 100644
--- a/app/assets/javascripts/work_items/components/work_item_title.vue
+++ b/app/assets/javascripts/work_items/components/work_item_title.vue
@@ -8,7 +8,7 @@ import {
WORK_ITEM_TITLE_MAX_LENGTH,
I18N_MAX_CHARS_IN_WORK_ITEM_TITLE_MESSAGE,
} from '../constants';
-import { getUpdateWorkItemMutation } from './update_work_item';
+import updateWorkItemMutation from '../graphql/update_work_item.mutation.graphql';
import ItemTitle from './item_title.vue';
export default {
@@ -32,16 +32,16 @@ export default {
required: false,
default: '',
},
- workItemParentId: {
- type: String,
- required: false,
- default: null,
- },
canUpdate: {
type: Boolean,
required: false,
default: false,
},
+ useH1: {
+ type: Boolean,
+ default: false,
+ required: false,
+ },
},
computed: {
tracking() {
@@ -63,24 +63,19 @@ export default {
return;
}
- const input = {
- id: this.workItemId,
- title: updatedTitle,
- };
-
this.updateInProgress = true;
try {
this.track('updated_title');
- const { mutation, variables } = getUpdateWorkItemMutation({
- workItemParentId: this.workItemParentId,
- input,
- });
-
const { data } = await this.$apollo.mutate({
- mutation,
- variables,
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ title: updatedTitle,
+ },
+ },
});
const errors = data.workItemUpdate?.errors;
@@ -101,5 +96,10 @@ export default {
</script>
<template>
- <item-title :title="workItemTitle" :disabled="!canUpdate" @title-changed="updateTitle" />
+ <item-title
+ :title="workItemTitle"
+ :disabled="!canUpdate"
+ :use-h1="useH1"
+ @title-changed="updateTitle"
+ />
</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index daa72204609..41cf5d8932d 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -54,9 +54,6 @@ export const i18n = {
"WorkItem|This work item is not available. It either doesn't exist or you don't have permission to view it.",
),
updateError: s__('WorkItem|Something went wrong while updating the work item. Please try again.'),
- confidentialTooltip: s__(
- 'WorkItem|Only project members with at least the Reporter role, the author, and assignees can view or be notified about this %{workItemType}.',
- ),
};
export const I18N_WORK_ITEM_ERROR_FETCHING_LABELS = s__(
@@ -195,6 +192,11 @@ export const WORK_ITEMS_TYPE_MAP = {
},
};
+export const WORK_ITEM_TYPE_VALUE_MAP = {
+ [WORK_ITEM_TYPE_VALUE_OBJECTIVE]: WORK_ITEM_TYPE_ENUM_OBJECTIVE,
+ [WORK_ITEM_TYPE_VALUE_KEY_RESULT]: WORK_ITEM_TYPE_ENUM_KEY_RESULT,
+};
+
export const WORK_ITEMS_TREE_TEXT_MAP = {
[WORK_ITEM_TYPE_VALUE_OBJECTIVE]: {
title: s__('WorkItem|Child objectives and key results'),
diff --git a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
deleted file mode 100644
index ccfe62cc585..00000000000
--- a/app/assets/javascripts/work_items/graphql/create_work_item_from_task.mutation.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-#import "./work_item.fragment.graphql"
-
-mutation workItemCreateFromTask($input: WorkItemCreateFromTaskInput!) {
- workItemCreateFromTask(input: $input) {
- workItem {
- ...WorkItem
- }
- newWorkItem {
- ...WorkItem
- }
- errors
- }
-}
diff --git a/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql
new file mode 100644
index 00000000000..f86176b2836
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/notes/group_work_item_notes_by_iid.query.graphql
@@ -0,0 +1,32 @@
+#import "~/graphql_shared/fragments/page_info.fragment.graphql"
+#import "ee_else_ce/work_items/graphql/notes/work_item_note.fragment.graphql"
+
+query groupWorkItemNotesByIid($fullPath: ID!, $iid: String, $after: String, $pageSize: Int) {
+ workspace: group(fullPath: $fullPath) {
+ id
+ workItems(iid: $iid) {
+ nodes {
+ id
+ iid
+ widgets {
+ ... on WorkItemWidgetNotes {
+ type
+ discussions(first: $pageSize, after: $after, filter: ALL_NOTES) {
+ pageInfo {
+ ...PageInfo
+ }
+ nodes {
+ id
+ notes {
+ nodes {
+ ...WorkItemNote
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql b/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
deleted file mode 100644
index ad861a60d15..00000000000
--- a/app/assets/javascripts/work_items/graphql/update_work_item_task.mutation.graphql
+++ /dev/null
@@ -1,13 +0,0 @@
-#import "./work_item.fragment.graphql"
-
-mutation workItemUpdateTask($input: WorkItemUpdateTaskInput!) {
- workItemUpdate: workItemUpdateTask(input: $input) {
- workItem {
- id
- descriptionHtml
- }
- task {
- ...WorkItem
- }
- }
-}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_allowed_children.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_allowed_children.query.graphql
new file mode 100644
index 00000000000..cfd21421f16
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_allowed_children.query.graphql
@@ -0,0 +1,20 @@
+query getAllowedWorkItemChildTypes($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ workItemType {
+ id
+ name
+ widgetDefinitions {
+ type
+ ... on WorkItemWidgetDefinitionHierarchy {
+ allowedChildTypes {
+ nodes {
+ id
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql
new file mode 100644
index 00000000000..bfcac11f51f
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_item_ancestors.query.graphql
@@ -0,0 +1,33 @@
+query workItemAncestorsQuery($id: WorkItemID!) {
+ workItem(id: $id) {
+ id
+ title
+ widgets {
+ type
+ ... on WorkItemWidgetHierarchy {
+ type
+ parent {
+ id
+ }
+ ancestors {
+ nodes {
+ id
+ iid
+ confidential
+ workItemType {
+ id
+ name
+ iconName
+ }
+ title
+ state
+ reference(full: true)
+ createdAt
+ closedAt
+ webUrl
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/notes/award_utils.js b/app/assets/javascripts/work_items/notes/award_utils.js
index 5351a22d593..4f35b06a685 100644
--- a/app/assets/javascripts/work_items/notes/award_utils.js
+++ b/app/assets/javascripts/work_items/notes/award_utils.js
@@ -5,6 +5,7 @@ import {
updateCacheAfterAddingAwardEmojiToNote,
updateCacheAfterRemovingAwardEmojiFromNote,
} from '~/work_items/graphql/cache_utils';
+import groupWorkItemNotesByIidQuery from '../graphql/notes/group_work_item_notes_by_iid.query.graphql';
import workItemNotesByIidQuery from '../graphql/notes/work_item_notes_by_iid.query.graphql';
import addAwardEmojiMutation from '../graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
import removeAwardEmojiMutation from '../graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
@@ -32,7 +33,7 @@ export function getMutation({ note, name }) {
};
}
-export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) {
+export function optimisticAwardUpdate({ note, name, fullPath, isGroup, workItemIid }) {
const { mutation } = getMutation({ note, name });
const currentUserId = window.gon.current_user_id;
@@ -40,7 +41,7 @@ export function optimisticAwardUpdate({ note, name, fullPath, workItemIid }) {
return (store) => {
store.updateQuery(
{
- query: workItemNotesByIidQuery,
+ query: isGroup ? groupWorkItemNotesByIidQuery : workItemNotesByIidQuery,
variables: { fullPath, iid: workItemIid },
},
(sourceData) => {
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index ac5d8b32fad..c3c292c3dd9 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -26,6 +26,19 @@ export const findHierarchyWidgets = (widgets) =>
export const findHierarchyWidgetChildren = (workItem) =>
findHierarchyWidgets(workItem?.widgets)?.children?.nodes || [];
+export const findHierarchyWidgetAncestors = (workItem) =>
+ findHierarchyWidgets(workItem?.widgets)?.ancestors?.nodes || [];
+
+export const formatAncestors = (workItem) =>
+ findHierarchyWidgetAncestors(workItem).map((ancestor) => ({
+ ...ancestor,
+ icon: ancestor.workItemType?.iconName,
+ href: ancestor.webUrl,
+ }));
+
+export const findHierarchyWidgetDefinition = (widgetDefinitions) =>
+ widgetDefinitions?.find((widgetDefinition) => widgetDefinition.type === WIDGET_TYPE_HIERARCHY);
+
const autocompleteSourcesPath = (autocompleteType, fullPath, workItemIid) => {
return `${
gon.relative_url_root || ''
diff --git a/app/assets/stylesheets/application_utilities.scss b/app/assets/stylesheets/application_utilities.scss
index 8bec12784ed..817e983a0ec 100644
--- a/app/assets/stylesheets/application_utilities.scss
+++ b/app/assets/stylesheets/application_utilities.scss
@@ -10,5 +10,3 @@
// Gitlab UI util classes
@import '@gitlab/ui/src/scss/utilities';
-
-@import 'tmp_utilities'; \ No newline at end of file
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 2030f2c7095..97f2add4e77 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -335,7 +335,7 @@
border-radius: 3px;
margin-left: 4px;
margin-top: -2px;
- border: 1px solid $black-transparent;
+ border: 1px solid $t-gray-a-24;
background-color: var(--gl-color-chip-color);
}
diff --git a/app/assets/stylesheets/components/detail_page.scss b/app/assets/stylesheets/components/detail_page.scss
index a5fd57f6c57..98ed7f590ea 100644
--- a/app/assets/stylesheets/components/detail_page.scss
+++ b/app/assets/stylesheets/components/detail_page.scss
@@ -38,18 +38,9 @@
.detail-page-header-actions {
flex: 0 0 auto;
- &:not(.is-merge-request) {
- @include media-breakpoint-down(xs) {
- width: 100%;
- margin-top: 10px;
- }
- }
-
- &.is-merge-request {
- @include media-breakpoint-down(sm) {
- width: 100%;
- margin-top: 10px;
- }
+ @include media-breakpoint-down(sm) {
+ width: 100%;
+ margin-top: 10px;
}
}
@@ -62,13 +53,17 @@
margin: 0 0 $gl-spacing-scale-4;
color: $gl-text-color;
padding: 0 0 0.3em;
- border-bottom: 1px solid $white-dark;
}
.description {
@include clearfix;
margin-top: 6px;
+
+ + .edited-text {
+ display: inline-block;
+ margin-top: $gl-spacing-scale-4;
+ }
}
.author-link {
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 4d4144fe9dd..6f4f7a29334 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -26,7 +26,6 @@
@import 'framework/highlight';
@import 'framework/lists';
@import 'framework/logo';
-@import 'framework/job_log';
@import 'framework/markdown_area';
@import 'framework/media_object';
@import 'framework/modal';
@@ -58,7 +57,6 @@
@import 'framework/responsive_tables';
@import 'framework/stacked_progress_bar';
@import 'framework/sortable';
-@import 'framework/feature_highlight';
@import 'framework/read_more';
@import 'framework/system_messages';
@import 'framework/spinner';
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index c93ef2287a8..07200c9b90a 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -92,8 +92,7 @@
@include transition(background-color, border-color, color, box-shadow);
}
-.dropdown-menu-toggle,
-.header-user-avatar {
+.dropdown-menu-toggle {
@include transition(border-color);
}
@@ -102,10 +101,6 @@
@include transition(color);
}
-.notification-dot {
- @include transition(background-color, color, border);
-}
-
.stage-nav-item {
@include transition(background-color, box-shadow);
}
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 28c0c071dc0..e11fa7d8801 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -24,7 +24,7 @@
width: $award-emoji-width;
font-size: 14px;
background-color: $white;
- border: 1px solid $border-white-light;
+ border: 1px solid $border-color;
border-radius: $border-radius-base;
box-shadow: 0 6px 12px $award-emoji-menu-shadow;
pointer-events: none;
@@ -218,7 +218,7 @@
}
.award-control-icon {
- color: $border-gray-normal;
+ color: $gray-100;
svg {
height: $default-icon-size;
@@ -254,11 +254,10 @@
display: contents;
gl-emoji {
- margin-top: -1px;
- margin-bottom: -1px;
+ margin-block: -0.1em;
img {
- top: 0;
+ top: -0.025em;
}
}
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index cae2ea1716c..4249bb372dc 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -25,8 +25,8 @@
background-color: $gray-light;
padding: $gl-padding;
margin-bottom: 0;
- border-top: 1px solid $white-dark;
- border-bottom: 1px solid $white-dark;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
color: $gl-text-color;
&.white {
@@ -76,14 +76,14 @@
.sub-header-block {
background-color: $white;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid $border-color;
padding: 11px 0;
margin-bottom: 11px;
}
.content-block {
padding: $gl-padding 0;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid $border-color;
> .controls {
float: right;
diff --git a/app/assets/stylesheets/framework/brand_logo.scss b/app/assets/stylesheets/framework/brand_logo.scss
index 1bc1ef797a7..95dcb26c0c5 100644
--- a/app/assets/stylesheets/framework/brand_logo.scss
+++ b/app/assets/stylesheets/framework/brand_logo.scss
@@ -1,6 +1,3 @@
-$brand-logo-light-background: #e0dfe5;
-$brand-logo-dark-background: #53515b;
-
.brand-logo {
display: inline-block;
@include gl-rounded-base;
@@ -16,14 +13,4 @@ $brand-logo-dark-background: #53515b;
&:active {
@include gl-focus;
}
-
- &:hover,
- &:focus,
- &:active {
- background-color: $brand-logo-light-background;
-
- .gl-dark & {
- background-color: $brand-logo-dark-background;
- }
- }
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 88509dbc4a1..709c33a2ad8 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -118,7 +118,7 @@
}
@mixin btn-white {
- @include btn-color($white, $border-color, $white-normal, $border-white-normal, $white-dark, $border-white-normal, $gl-text-color);
+ @include btn-color($white, $gray-200, $gray-50, $gray-200, $gray-100, $gray-300, $gl-text-color);
}
@mixin btn-purple {
@@ -276,7 +276,7 @@
.active {
box-shadow: $gl-btn-active-background;
- border: 1px solid $border-white-normal !important;
+ border: 1px solid $gray-100 !important;
background-color: $btn-active-gray-light !important;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 21c252038af..874cfa2fe53 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -58,21 +58,7 @@
}
}
-@include media-breakpoint-up(md) {
- .page-with-contextual-sidebar {
- --application-bar-left: #{$contextual-sidebar-collapsed-width};
- }
-}
-
@include media-breakpoint-up(xl) {
- .page-with-contextual-sidebar {
- --application-bar-left: #{$contextual-sidebar-width};
- }
-
- .page-with-icon-sidebar {
- --application-bar-left: #{$contextual-sidebar-collapsed-width};
- }
-
.page-with-super-sidebar {
--application-bar-left: #{$super-sidebar-width};
}
@@ -333,7 +319,7 @@ li.note {
.progress {
margin-top: 4px;
box-shadow: none;
- background-color: $border-gray-light;
+ background-color: $gray-100;
}
}
@@ -495,7 +481,7 @@ li.note {
width: 4px;
&:hover {
- background-color: $white-normal;
+ background-color: $gray-50;
}
&.is-dragging {
@@ -582,7 +568,7 @@ See https://gitlab.com/gitlab-org/gitlab/issues/36857 for more details.
// used in the Markdown rendering of labels
.scoped-label-tooltip-title {
- color: var(--indigo-300, $indigo-300);
+ color: var(--theme-indigo-300, $theme-indigo-300);
}
.gl-label-scoped {
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index fb9816d1402..4a9f77316e6 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -65,10 +65,6 @@
.avatar-container {
margin: 0 auto;
}
-
- li.active:not(.fly-out-top-item) > a {
- background-color: $indigo-900-alpha-008;
- }
}
@mixin sub-level-items-flyout {
@@ -199,32 +195,6 @@
}
//
-// PAGE-LAYOUT
-//
-
-.page-with-contextual-sidebar {
- transition: padding-left $gl-transition-duration-medium;
-
- @include media-breakpoint-up(md) {
- padding-left: $contextual-sidebar-collapsed-width;
- }
-
- @include media-breakpoint-up(xl) {
- padding-left: $contextual-sidebar-width;
- }
-
- .issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
- padding: 10px 0 15px;
- }
-}
-
-.page-with-icon-sidebar {
- @include media-breakpoint-up(md) {
- padding-left: $contextual-sidebar-collapsed-width;
- }
-}
-
-//
// THE PANEL
//
@@ -446,7 +416,7 @@
&.mobile-nav-open {
display: block;
position: fixed;
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
height: 100%;
width: 100%;
z-index: $zindex-dropdown-menu;
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 8f07ef73554..b948a57ea33 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -1,3 +1,5 @@
+$diff-file-header: 41px;
+
// Common
.diff-file {
margin-bottom: $gl-padding;
@@ -38,6 +40,10 @@
&.is-sidebar-moved {
top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} - #{$gl-border-size-1});
+
+ + .diff-content .md-header-preview {
+ top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height} + #{$diff-file-header} - #{$gl-border-size-1});
+ }
}
&::before {
@@ -683,7 +689,7 @@ table.code {
.note-container {
background-color: $gray-light;
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
// double jagged line divider
.discussion-notes + .discussion-notes::before,
@@ -744,7 +750,7 @@ table.code {
.diff-file .note-container > .new-note,
.note-container .discussion-notes.diff-discussions {
margin-left: 100px;
- border-left: 1px solid $white-normal;
+ border-left: 1px solid $gray-50;
}
.notes.active {
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index a467d9e8c8a..e791a0dbbbd 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -810,14 +810,6 @@
.navbar-gitlab {
li.dropdown {
position: static;
-
- &.user-counter {
- margin-left: 8px !important;
-
- > a {
- padding: 0 4px !important;
- }
- }
}
}
@@ -836,95 +828,6 @@
}
}
-.frequent-items-dropdown-container {
- display: flex;
- flex-direction: row;
- height: $grid-size * 40;
-
- .frequent-items-dropdown-content {
- @include gl-pt-3;
- }
-
- .loading-animation {
- color: $almost-black;
- }
-
- .frequent-items-dropdown-content {
- position: relative;
- width: 70%;
- }
-
- .section-header,
- .frequent-items-list-container li.section-empty {
- color: $gl-text-color-secondary;
- font-size: $gl-font-size;
- }
-
- .frequent-items-list-container {
- padding: 8px 0;
- overflow-y: auto;
-
- li.section-empty.section-failure {
- color: $red-700;
- }
-
- .frequent-items-list-item-container .gl-button {
- &:active,
- &:focus,
- &:focus:active,
- &.is-focused {
- @include gl-focus($inset: true);
- }
- }
- }
-
- .section-header {
- font-weight: 700;
- margin-top: 8px;
- }
-}
-
-.frequent-items-list-item-container {
- .frequent-items-item-metadata-container {
- display: flex;
- flex-shrink: 0;
- flex-direction: column;
- justify-content: center;
- }
-
- .frequent-items-item-title,
- .frequent-items-item-namespace {
- max-width: 220px;
- text-overflow: ellipsis;
- white-space: nowrap;
- overflow: hidden;
- }
-
- .frequent-items-item-title {
- font-size: $gl-font-size;
- font-weight: 400;
- line-height: 16px;
- }
-
- .frequent-items-item-namespace {
- margin-top: 4px;
- font-size: 12px;
- line-height: 12px;
- color: $gl-text-color-secondary;
- }
-
- @include media-breakpoint-down(xs) {
- .frequent-items-item-metadata-container {
- float: none;
- }
-
- .frequent-items-item-title,
- .frequent-items-item-namespace {
- max-width: 250px;
- }
- }
-}
-
.dropdown-content-faded-mask {
position: relative;
diff --git a/app/assets/stylesheets/framework/emojis.scss b/app/assets/stylesheets/framework/emojis.scss
index d3986f31d52..9227028e3da 100644
--- a/app/assets/stylesheets/framework/emojis.scss
+++ b/app/assets/stylesheets/framework/emojis.scss
@@ -27,7 +27,7 @@ gl-emoji {
.emoji-picker-category-header {
@include gl-sticky;
- background-color: $white-transparent;
+ background: linear-gradient(to bottom, $white 50%, transparent 100%);
}
.emoji-picker-emoji {
@@ -35,11 +35,18 @@ gl-emoji {
// Create a width that fits 9 emojis per row
width: 100 / 9 * 1%;
transition: transform 0.15s cubic-bezier(0.3, 0, 0.2, 2) !important;
- will-change: transform;
+ transform: scale(1) !important;
+ mix-blend-mode: normal !important;
&:hover,
&:focus {
- transform: scale(1.3);
+ @include gl-z-index-2;
+ transform: scale(1.3) !important;
+ }
+
+ gl-emoji img {
+ top: auto;
+ max-width: unset;
}
}
@@ -51,16 +58,24 @@ gl-emoji {
border-bottom-color: transparent;
&:hover {
- @include gl-text-gray-900;
+ color: $gray-900 !important;
&:not(.emoji-picker-category-active) {
- @include gl-border-b-gray-200;
+ border-bottom-color: $gray-300;
}
}
+
+ &:focus {
+ z-index: 2;
+ }
}
.emoji-picker-category-active {
- border-bottom-color: $blue-500;
+ border-bottom-color: $blue-500 !important;
+
+ svg {
+ color: $gray-900 !important;
+ }
}
.emoji-picker .gl-dropdown-contents > :last-child {
diff --git a/app/assets/stylesheets/framework/feature_highlight.scss b/app/assets/stylesheets/framework/feature_highlight.scss
deleted file mode 100644
index 36f1b1f2903..00000000000
--- a/app/assets/stylesheets/framework/feature_highlight.scss
+++ /dev/null
@@ -1,53 +0,0 @@
-.feature-highlight {
- &::before {
- content: '';
- display: block;
- top: 6px;
- left: 6px;
- width: 8px;
- height: 8px;
- background-color: $blue-500;
- border-radius: 50%;
- box-shadow: 0 0 0 rgba($blue-500, 0.4);
- animation: pulse-highlight 2s infinite;
- }
-
- &:hover::before,
- &.disable-animation::before {
- animation: none;
- }
-
- &[disabled]::before {
- display: none;
- }
-}
-
-
-.feature-highlight-illustration {
- background-color: $indigo-50;
- border-top-left-radius: 2px;
- border-top-right-radius: 2px;
- border-bottom: 1px solid darken($gray-normal, 8%);
-}
-
-.feature-highlight-popover {
- width: 240px;
-
- .popover-body {
- padding: 0;
- }
-}
-
-@include keyframes(pulse-highlight) {
- 0% {
- box-shadow: 0 0 0 0 rgba($blue-200, 0.4);
- }
-
- 70% {
- box-shadow: 0 0 0 10px transparent;
- }
-
- 100% {
- box-shadow: 0 0 0 0 transparent;
- }
-}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index eb627b036fe..9cb264c992b 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -376,7 +376,7 @@ span.idiff {
border-bottom: 1px $gray-darkest dashed;
&:hover {
- border-bottom-color: $almost-black;
+ border-bottom-color: $gray-950;
}
}
}
@@ -603,6 +603,6 @@ span.idiff {
right: 0;
top: -$gradient-size;
height: $gradient-size;
- background: linear-gradient(to top, $white, transparentize($white, 1));
+ background: linear-gradient(to top, $white, transparent);
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 67e96f08cb0..5949a1b2809 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -110,7 +110,7 @@
}
.operator {
- background-color: $white-normal;
+ background-color: $gray-50;
color: $gl-text-color;
margin-right: 1px;
}
@@ -118,7 +118,7 @@
.value-container {
display: flex;
align-items: center;
- background-color: $white-normal;
+ background-color: $gray-50;
color: $gl-text-color;
border-radius: 0 2px 2px 0;
margin-right: 5px;
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index 66d163f608a..b87a7f15c1c 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -45,6 +45,6 @@
height: 100%;
margin-bottom: 2px;
border-radius: 3px;
- border: 1px solid $black-transparent;
+ border: 1px solid $t-gray-a-24;
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index e269ea68e41..23f40dfe4bf 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -1,6 +1,3 @@
-$search-input-field-min-width: 320px;
-$search-input-field-x-min-width: 200px;
-
.navbar-gitlab {
padding: 0 16px;
z-index: $header-zindex;
@@ -70,252 +67,22 @@ $search-input-field-x-min-width: 200px;
border-bottom-color: $white;
}
}
-
- .navbar-collapse > ul.nav > li:not(.d-none) {
- margin: 0 2px;
- }
- }
-
- .header-search-form {
- min-width: $search-input-field-min-width;
-
- // This is a temporary workaround!
- // the button in GitLab UI Search components need to be updated to not be the small size
- // see in Figma: https://www.figma.com/file/qEddyqCrI7kPSBjGmwkZzQ/Component-library?node-id=43905%3A45540
- .gl-search-box-by-type-clear.btn-sm {
- padding: 0.5rem !important;
- }
-
- @include media-breakpoint-between(md, lg) {
- min-width: $search-input-field-x-min-width;
- }
-
- &.is-searching {
- .in-search-scope-help {
- position: absolute;
- top: $gl-spacing-scale-2;
- right: 2.125rem;
- z-index: 2;
- }
- }
-
- &.is-not-focused {
- .gl-search-box-by-type-clear {
- display: none;
- }
- }
-
- .keyboard-shortcut-helper {
- transform: translateY(calc(50% - 2px));
- box-shadow: none;
- border-color: transparent;
- }
- }
-
- .header-search-dropdown-menu {
- max-height: $dropdown-max-height;
- top: 100%;
- }
-
- .navbar-collapse {
- flex: 0 0 auto;
- border-top: 0;
- padding: 0;
-
- @include media-breakpoint-down(xs) {
- .legacy-top-bar & {
- flex: 1 1 auto;
- }
- }
-
- .nav {
- flex-wrap: nowrap;
-
- > li:not(.d-none) a {
- @include media-breakpoint-down(xs) {
- margin-left: 0;
- }
- }
- }
}
.container-fluid {
padding: 0;
- .user-counter {
- svg {
- margin-right: 3px;
- }
- }
-
- .navbar-toggler {
- position: relative;
- right: -10px;
- border-radius: 0;
- min-width: 45px;
- padding: 0;
- margin: $gl-padding-8 $gl-padding-8 $gl-padding-8 0;
- font-size: 14px;
- text-align: center;
- color: currentColor;
-
- &:hover,
- &:focus,
- &.active {
- color: currentColor;
- background-color: transparent;
- }
- }
-
- .navbar-nav {
- @include media-breakpoint-down(xs) {
- display: flex;
- padding-right: 10px;
- flex-direction: row;
- }
-
- li {
- .badge.badge-pill:not(.gl-badge) {
- box-shadow: none;
- font-weight: $gl-font-weight-bold;
- }
- }
- }
-
.nav > li {
- &.header-user {
- @include media-breakpoint-down(xs) {
- padding-left: 10px;
- }
- }
-
> a {
will-change: color;
margin: 4px 0;
padding: 6px 8px;
height: 32px;
-
- .legacy-top-bar & {
- @include media-breakpoint-down(xs) {
- padding: 0;
- }
- }
-
- &.header-user-dropdown-toggle {
- margin-left: 2px;
-
- .header-user-avatar {
- margin-right: 0;
- }
- }
- }
-
- .header-new-dropdown-toggle {
- margin-right: 0;
- }
-
- .impersonated-user,
- .impersonated-user:hover {
- margin-right: 1px;
- background-color: $white;
- border-top-right-radius: 0;
- border-bottom-right-radius: 0;
- }
-
- .impersonation-btn,
- .impersonation-btn:hover {
- background-color: $white;
- border-top-left-radius: 0;
- border-bottom-left-radius: 0;
-
- svg {
- color: $orange-500;
- }
- }
- }
- }
-}
-
-.navbar-sub-nav,
-.navbar-nav {
- > li {
- > a,
- > button {
- display: flex;
- align-items: center;
- justify-content: center;
- padding: 6px 8px;
- margin: 4px 2px;
- font-size: 12px;
- color: currentColor;
- border-radius: $border-radius-default;
- height: 32px;
- font-weight: $gl-font-weight-bold;
-
- &:hover,
- &:focus {
- text-decoration: none;
- outline: 0;
- color: $white;
- }
-
- &:active,
- &:focus {
- @include gl-focus($focus-ring: $focus-ring-dark);
}
}
-
- .top-nav-toggle,
- > button {
- background: transparent;
- border: 0;
- }
-
- &.line-separator {
- margin: 8px;
- }
- }
-
- .dropdown-menu {
- position: absolute;
}
}
-.navbar-sub-nav {
- display: flex;
- align-items: center;
- height: 100%;
- margin: 0 0 0 6px;
-
- .dropdown-chevron {
- position: relative;
- top: -1px;
- font-size: 10px;
- }
-
- .frequent-items-item-select-holder {
- display: inline;
- }
-
- .impersonation i {
- color: $red-500;
- }
-}
-
-.caret-down,
-.btn .caret-down {
- top: 0;
- height: 11px;
- width: 11px;
- margin-left: 4px;
- fill: currentColor;
-}
-
-.header-user .dropdown-menu,
-.header-new .dropdown-menu {
- margin-top: $dropdown-vertical-offset;
-}
-
.top-bar-container {
min-height: $top-bar-height;
}
@@ -333,12 +100,6 @@ $search-input-field-x-min-width: 200px;
@media (prefers-reduced-motion: no-preference) {
transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
}
-
- .breadcrumbs-list {
- @include media-breakpoint-down(xs) {
- flex-wrap: nowrap;
- }
- }
}
.breadcrumbs {
@@ -353,59 +114,6 @@ $search-input-field-x-min-width: 200px;
border-radius: 50%;
vertical-align: sub;
}
-
- .text-expander {
- margin-left: 0;
- margin-right: 2px;
-
- > i {
- position: relative;
- top: 1px;
- }
- }
-}
-
-.breadcrumbs-list {
- display: flex;
- margin-bottom: 0;
- line-height: 16px;
-
- @include media-breakpoint-down(xs) {
- flex-wrap: wrap;
- }
-
- > li {
- display: flex;
- align-items: center;
- position: relative;
- min-width: 0;
- padding: 2px 0;
-
- &:not(:last-child) {
- padding-right: 20px;
- }
-
- &:last-child {
- > a {
- font-weight: 600;
- line-height: 16px;
- color: $gl-text-color;
- }
- }
-
- > a {
- font-size: 12px;
- color: currentColor;
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
- flex: 0 1 auto;
- }
-
- &:last-of-type > .breadcrumbs-list-angle {
- display: none;
- }
- }
}
.breadcrumb-item-text {
@@ -416,93 +124,6 @@ $search-input-field-x-min-width: 200px;
}
}
-.breadcrumbs-list-angle {
- position: absolute;
- right: 7px;
- top: 50%;
- color: $gl-text-color-tertiary;
- transform: translateY(-50%);
-}
-
-.breadcrumbs-extra {
- display: flex;
- flex: 0 0 auto;
- margin-left: auto;
-}
-
-@include media-breakpoint-down(xs) {
- .navbar-gitlab.legacy-top-bar .container-fluid {
- font-size: 18px;
-
- .navbar-nav {
- table-layout: fixed;
- width: 100%;
- margin: 0;
- text-align: right;
- }
-
- .navbar-collapse {
- margin-left: -8px;
- margin-right: -10px;
-
- .nav > li:not(.d-none) {
- flex: 1;
- }
- }
- }
-
- .header-user-dropdown-toggle {
- text-align: center;
- }
-
- .header-user-avatar {
- float: none;
- }
-}
-
-.header-user {
- &.show .dropdown-menu {
- margin-top: 4px;
- color: var(--gl-text-color, $gl-text-color);
- left: auto;
- max-height: $dropdown-max-height-lg;
-
- .user-status {
- max-width: 240px;
- }
-
- svg {
- vertical-align: text-top;
- }
-
- a.ci-minutes-emoji gl-emoji,
- a.trial-link gl-emoji {
- font-size: $gl-font-size;
- vertical-align: baseline;
- }
- }
-}
-
-.header-user-avatar {
- float: left;
- margin-right: 5px;
- border-radius: 50%;
- border: 1px solid $gray-normal;
-}
-
-.notification-dot {
- background-color: $orange-300;
- height: 12px;
- width: 12px;
- pointer-events: none;
- visibility: hidden;
- top: 3px;
-}
-
-.with-notifications .notification-dot {
- visibility: visible;
-}
-
.navbar-empty {
justify-content: center;
height: var(--header-height);
@@ -552,37 +173,6 @@ $search-input-field-x-min-width: 200px;
@include media-breakpoint-down(xs) { margin-right: 3px; }
}
-.toggle-mobile-nav {
- @include gl-display-none;
-
- @include media-breakpoint-down(sm) {
- @include gl-display-block;
-
- + .breadcrumbs {
- @include gl-pl-4;
- @include gl-border-l-1;
- @include gl-border-l-solid;
- @include gl-border-gray-100;
- }
- }
-}
-
-.top-nav-container-view {
- .gl-dropdown & .gl-search-box-by-type {
- @include gl-m-0;
- }
-
- .frequent-items-list-item-container > a:hover {
- background-color: $nav-active-bg !important;
- }
-}
-
-.top-nav-toggle {
- .dropdown-chevron {
- top: 0;
- }
-}
-
.top-nav-menu-item {
&.active,
&:hover {
@@ -594,51 +184,77 @@ $search-input-field-x-min-width: 200px;
}
}
-.top-nav-responsive {
- @include gl-display-none;
+.header-logged-out {
+ z-index: $header-zindex;
+ min-height: var(--header-height);
+ position: fixed;
+ top: $calc-system-headers-height;
+ left: 0;
+ right: 0;
+ background-color: $brand-charcoal;
}
-.top-nav-responsive-open {
- .more-icon {
- display: none;
- }
-
- .close-icon {
- display: block;
- margin: auto;
- }
+.header-logged-out-nav {
+ position: relative;
+ min-height: var(--header-height);
+}
- @include media-breakpoint-down(xs) {
- .navbar-collapse {
- display: flex;
- }
+.header-logged-out-logo {
+ a {
+ display: flex;
+ align-items: center;
+ padding: 4px;
+ margin-left: -4px;
+ border-radius: $border-radius-default;
- .hide-when-top-nav-responsive-open {
- display: none !important;
+ &:hover,
+ &:focus {
+ background-color: $brand-gray-04;
}
- .top-nav-responsive {
- @include gl-display-block;
+ &:focus,
+ &:active {
+ @include gl-focus;
}
- .navbar-gitlab .header-content .title-container {
- flex: 0;
+ &:active {
+ background-color: $brand-gray-03;
}
}
}
-header.navbar-gitlab.super-sidebar-logged-out {
- background-color: $brand-charcoal !important;
+.header-logged-out-toggle {
+ appearance: none;
+ border: 0;
+ background-color: transparent;
+ border-radius: $border-radius-default;
+}
+
+.header-logged-out-dropdown {
+ position: static;
+
+ .dropdown-menu {
+ position: absolute;
+ width: 100%;
+ min-width: 100%;
+ }
+}
- li.nav-item > button,
- li.nav-item > a {
+.header-logged-out-nav-item {
+ > button,
+ > a {
+ display: inline-block;
+ padding: 6px 8px;
+ height: 32px;
+ border-radius: $border-radius-default;
@include gl-text-gray-100;
@include gl-font-weight-normal;
+ @include gl-font-base;
&:hover,
&:focus,
&:active {
- @include gl-text-white
+ @include gl-text-white;
}
&:hover,
diff --git a/app/assets/stylesheets/framework/icons.scss b/app/assets/stylesheets/framework/icons.scss
index bfd55fbb53d..3399847c201 100644
--- a/app/assets/stylesheets/framework/icons.scss
+++ b/app/assets/stylesheets/framework/icons.scss
@@ -44,7 +44,7 @@
.ci-icon {
// .ci-icon class is used at
- // - app/assets/javascripts/vue_shared/components/ci_icon.vue
+ // - app/assets/javascripts/vue_shared/components/ci_icon/ci_icon.vue
// - app/helpers/ci/status_helper.rb
.ci-icon-gl-icon-wrapper {
@include gl-rounded-full;
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
deleted file mode 100644
index e409facd081..00000000000
--- a/app/assets/stylesheets/framework/job_log.scss
+++ /dev/null
@@ -1,47 +0,0 @@
-.job-log {
- font-family: $monospace-font;
- padding: $gl-padding-8 $input-horizontal-padding;
- margin: 0 0 $gl-padding-8;
- font-size: 13px;
- word-break: break-all;
- word-wrap: break-word;
- color: color-yiq($builds-log-bg);
- border-radius: 0 0 $border-radius-default $border-radius-default;
- min-height: 42px;
- background-color: $builds-log-bg;
-}
-
-.log-line {
- padding: 1px $gl-padding-8 1px $job-log-line-padding;
- min-height: $gl-line-height-20;
-}
-
-.line-number {
- color: $gray-500;
- padding: 0 $gl-padding-8;
- min-width: $job-line-number-width;
- margin-left: -$job-line-number-margin;
- padding-right: 1em;
- user-select: none;
-
- &:hover,
- &:active,
- &:visited {
- text-decoration: underline;
- color: $gray-500;
- }
-}
-
-.collapsible-line {
- &:hover {
- background-color: rgba($white, 0.2);
- }
-
- .arrow {
- margin-left: -$job-arrow-margin;
- }
-}
-
-.loader-animation {
- @include build-loader-animation;
-}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 33c8a0254fd..7ec13c3d54c 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -71,6 +71,11 @@ body {
}
.alert-wrapper {
+ @include gl-media-breakpoint-up(xl) {
+ --gl-alert-padding-x: #{$gl-spacing-scale-3};
+ --gl-broadcast-message-padding-x: #{$gl-spacing-scale-3};
+ }
+
.alert {
margin-bottom: 0;
@@ -149,7 +154,7 @@ body {
overflow: hidden;
> #js-peek,
- > .navbar-gitlab {
+ > .header-logged-out {
position: static;
top: auto;
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index e9a507ebb6b..832b2297673 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -53,7 +53,7 @@
p {
padding-top: 1px;
margin: 0;
- color: $white-normal;
+ color: $gray-50;
img {
position: relative;
@@ -104,7 +104,7 @@ ul.content-list {
padding: 0;
li {
- border-color: $white-normal;
+ border-color: $gray-50;
font-size: $gl-font-size;
color: $gl-text-color;
word-break: break-word;
@@ -165,7 +165,7 @@ ul.content-list {
&.list-placeholder {
background-color: $gray-light;
- border: dotted 1px $white-normal;
+ border: dotted 1px $gray-50;
margin: 1px 0;
min-height: 52px;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index f76a9cf0373..0265820bfe1 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -85,7 +85,7 @@ body.modal-open {
}
.modal {
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
.modal-content {
border-radius: $modal-border-radius;
diff --git a/app/assets/stylesheets/framework/responsive_tables.scss b/app/assets/stylesheets/framework/responsive_tables.scss
index f57d906e73c..5a86a96a96e 100644
--- a/app/assets/stylesheets/framework/responsive_tables.scss
+++ b/app/assets/stylesheets/framework/responsive_tables.scss
@@ -38,7 +38,7 @@
border: 0;
&:not(:last-child) {
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $gray-50;
}
}
}
@@ -69,7 +69,7 @@
min-height: 62px;
&:not(:first-child) {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
}
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index f77a919ef0f..2715c334952 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -349,7 +349,6 @@
&.activities {
display: flex;
border-bottom: 1px solid $border-color;
- overflow: hidden;
align-items: center;
.nav-links {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 168aa704a69..0eecf7bddc1 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -53,12 +53,21 @@
height: $gl-padding;
}
}
+
+ .right-sidebar-header {
+ flex-wrap: wrap;
+ }
}
.right-sidebar-expanded {
padding-right: 0;
z-index: $zindex-dropdown-menu;
+ .right-sidebar-header {
+ padding-block: $gl-spacing-scale-4;
+ margin-left: 20px;
+ }
+
.inline-block {
@include gl-display-inline-block;
}
@@ -99,7 +108,7 @@
}
.right-sidebar {
- border-left: 1px solid $gray-50;
+ border-left: 1px solid $border-color;
&.right-sidebar-merge-requests {
@include media-breakpoint-up(lg) {
@@ -321,7 +330,7 @@
.right-sidebar {
&:not(.right-sidebar-merge-requests) {
@include right-sidebar;
- top: $calc-application-bars-height;
+ top: $calc-application-header-height;
@include media-breakpoint-down(md) {
z-index: 251;
@@ -402,7 +411,7 @@
.issuable-sidebar-header {
@include clearfix;
padding: $gl-spacing-scale-4 0 $gl-spacing-scale-5;
- border-bottom: 1px solid $border-gray-normal;
+ border-bottom: 1px solid $border-color;
// This prevents the mess when resizing the sidebar
// of elements repositioning themselves..
width: $right-sidebar-inner-width;
@@ -481,6 +490,10 @@
width: $right-sidebar-width;
}
+ .issuable-sidebar-header {
+ @include gl-py-5;
+ }
+
.value {
line-height: 1;
}
@@ -578,7 +591,7 @@
}
.participants {
- border-bottom: 1px solid $border-gray-normal;
+ border-bottom: 1px solid $border-color;
}
.hide-collapsed {
@@ -589,7 +602,7 @@
width: 100%;
height: $sidebar-toggle-height;
margin-left: 0;
- border-bottom: 1px solid $border-white-normal;
+ border-bottom: 1px solid $border-color;
border-radius: 0;
}
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 9f8d5d25cb8..edd07dbaafa 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -29,17 +29,8 @@
}
}
-.snippet-header {
- padding: $gl-padding 0;
-}
-
.snippet-title {
color: $gl-text-color;
font-size: 2em;
font-weight: $gl-font-weight-bold;
- min-height: $header-height;
-}
-
-.snippet-scope-menu .btn-success {
- margin-top: 15px;
}
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index fbf9d8c8ca6..84f0612a7b4 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -1,18 +1,9 @@
-@mixin active-toggle {
- background-color: $gray-50 !important;
- mix-blend-mode: multiply;
-
- .gl-dark & {
- mix-blend-mode: screen;
- }
-}
-
$super-sidebar-transition-duration: $gl-transition-duration-medium;
$super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
@mixin notification-dot($color, $size, $top, $left) {
background-color: $color;
- border: 2px solid $gray-10; // Same as the sidebar's background color.
+ border: 2px solid var(--super-sidebar-bg);
position: absolute;
height: $size;
width: $size;
@@ -29,14 +20,53 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
.super-sidebar {
+ --super-sidebar-bg: #{$gray-10};
+ --super-sidebar-border-color: #{$t-gray-a-08};
+ --super-sidebar-primary: #{$blue-500};
+ --super-sidebar-notification-dot: #{$blue-500};
+ --super-sidebar-user-bar-bg: #{$t-gray-a-04};
+
+ --super-sidebar-user-bar-button-bg: #{$gray-10};
+ --super-sidebar-user-bar-button-color: #{$gray-900};
+ --super-sidebar-user-bar-button-border-color: #{$t-gray-a-08};
+ --super-sidebar-user-bar-button-hover-bg: #{$t-gray-a-08};
+ --super-sidebar-user-bar-button-hover-color: #{$gray-900};
+ --super-sidebar-user-bar-button-active-bg: #{$t-gray-a-16};
+
+ --super-sidebar-user-bar-button-icon-color: #{$gray-500};
+ --super-sidebar-user-bar-button-icon-hover-color: #{$gray-700};
+ --super-sidebar-user-bar-button-icon-mix-blend-mode: normal;
+
+ --super-sidebar-nav-item-hover-bg: #{$t-gray-a-08};
+ --super-sidebar-nav-item-active-bg: #{$t-gray-a-16};
+ --super-sidebar-nav-item-current-bg: #{$t-gray-a-08};
+ --super-sidebar-nav-item-icon-color: #{$gray-500};
+
+ .gl-dark & {
+ --super-sidebar-border-color: #{$t-white-a-08};
+ --super-sidebar-user-bar-bg: #{$t-white-a-04};
+
+ --super-sidebar-user-bar-button-bg: #{$gray-10};
+ --super-sidebar-user-bar-button-border-color: #{$t-white-a-08};
+ --super-sidebar-user-bar-button-hover-bg: #{$t-white-a-16};
+ --super-sidebar-user-bar-button-active-bg: #{$t-white-a-24};
+
+ --super-sidebar-user-bar-button-icon-color: #{$gray-600};
+
+ --super-sidebar-nav-item-hover-bg: #{$t-white-a-08};
+ --super-sidebar-nav-item-active-bg: #{$t-white-a-16};
+ --super-sidebar-nav-item-current-bg: #{$t-white-a-08};
+ --super-sidebar-nav-item-icon-color: #{$gray-600};
+ }
+
display: flex;
flex-direction: column;
position: fixed;
top: $calc-application-bars-height;
bottom: $calc-application-footer-height;
left: 0;
- background-color: var(--gray-10, $gray-10);
- border-right: 1px solid $t-gray-a-08;
+ background-color: var(--super-sidebar-bg);
+ border-right: 1px solid var(--super-sidebar-border-color);
transform: translate3d(0, 0, 0);
width: $super-sidebar-width;
z-index: $super-sidebar-z-index;
@@ -55,60 +85,97 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
.user-bar {
- background-color: $t-gray-a-04;
+ background-color: var(--super-sidebar-user-bar-bg);
- .user-bar-item {
- @include gl-rounded-base;
+ .user-bar-dropdown-toggle {
@include gl-p-2;
- @include gl-bg-transparent;
@include gl-border-none;
- &:focus,
- &:active {
- @include gl-focus;
+ &[aria-expanded='true'] {
+ background-color: var(--super-sidebar-user-bar-button-hover-bg);
}
}
- .user-bar-item {
+ .brand-logo,
+ .btn-default-tertiary,
+ .user-bar-button {
+ color: var(--super-sidebar-user-bar-button-color);
+
+ .gl-icon {
+ color: var(--super-sidebar-user-bar-button-icon-color) !important;
+ mix-blend-mode: var(--super-sidebar-user-bar-button-icon-mix-blend-mode);
+ }
+
+ &:active,
&:hover,
+ &:focus {
+ background-color: var(--super-sidebar-user-bar-button-hover-bg);
+ color: var(--super-sidebar-user-bar-button-hover-color);
+
+ .gl-icon {
+ color: var(--super-sidebar-user-bar-button-icon-hover-color);
+ }
+ }
+
+ &:active {
+ background-color: var(--super-sidebar-user-bar-button-active-bg) !important;
+ }
+
&:focus,
&:active {
- @include active-toggle;
+ @include gl-focus;
+ }
+ }
+
+ .btn-default-tertiary {
+ mix-blend-mode: normal;
+ }
+
+ .user-bar-button {
+ background-color: var(--super-sidebar-user-bar-button-bg);
+ box-shadow: inset 0 0 0 $gl-border-size-1 var(--super-sidebar-user-bar-button-border-color);
+
+ &[aria-expanded='true'] {
+ background-color: var(--super-sidebar-user-bar-button-hover-bg);
+ color: var(--super-sidebar-user-bar-button-hover-color);
}
}
- }
- .counter .gl-icon,
- .item-icon {
- color: var(--gray-600, $gray-500);
+ .gl-new-dropdown-toggle[aria-expanded='true'] {
+ background-color: var(--super-sidebar-user-bar-button-hover-bg);
+ color: var(--super-sidebar-user-bar-button-hover-color);
+ }
}
- .counter:hover,
- .counter:focus,
- .counter[aria-expanded='true'] {
- background-color: $gray-50;
- border-color: transparent;
- mix-blend-mode: multiply;
+ .super-sidebar-nav-item {
+ &:hover,
+ &:focus {
+ background-color: var(--super-sidebar-nav-item-hover-bg);
+ }
- .gl-dark & {
- mix-blend-mode: screen;
+ &.super-sidebar-nav-item-current {
+ background-color: var(--super-sidebar-nav-item-current-bg);
}
- .gl-icon {
- color: var(--gray-700, $gray-700);
+ &:active,
+ &:focus:active {
+ background-color: var(--super-sidebar-nav-item-active-bg);
}
}
- .counter:hover,
- .counter[aria-expanded='true'] {
- box-shadow: none;
+ .super-sidebar-nav-item-icon {
+ color: var(--super-sidebar-nav-item-icon-color);
+ }
+
+ .active-indicator {
+ background-color: var(--super-sidebar-primary);
}
.btn-with-notification {
position: relative;
.notification-dot-info {
- @include notification-dot($blue-500, 9px, 5px, 22px);
+ @include notification-dot(var(--super-sidebar-notification-dot), 9px, 5px, 22px);
}
.notification-dot-warning {
@@ -118,23 +185,13 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
&:hover,
&:focus {
.notification {
- border-color: $gray-50; // Same as the button's hover background color.
+ background-color: var(--super-sidebar-user-bar-button-hover-bg);
}
}
}
- .gl-new-dropdown-toggle[aria-expanded='true'] {
- @include active-toggle;
- }
-
- .gl-new-dropdown-custom-toggle {
- .btn-with-notification {
- mix-blend-mode: unset; // Our tertiary buttons otherwise use another mix-blend mode, making border-color semi-transparent.
- }
-
- [aria-expanded='true'] {
- @include active-toggle;
- }
+ .super-sidebar-help-center-toggle[aria-expanded='true'] {
+ background-color: $gray-50 !important;
}
#trial-status-sidebar-widget:hover {
@@ -191,7 +248,7 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
bottom: 0;
left: 0;
right: 0;
- background-color: $black-transparent;
+ background-color: $t-gray-a-24;
z-index: $super-sidebar-z-index - 1;
@include media-breakpoint-up(md) {
@@ -311,7 +368,7 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
&:hover,
&:focus-within {
.show-on-focus-or-hover--control {
- @include gl-bg-t-gray-a-08;
+ background-color: var(--super-sidebar-nav-item-hover-bg);
}
.show-on-focus-or-hover--target {
@@ -350,3 +407,57 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
}
+
+
+// Styles for the ScrollScrim component.
+// Should eventually be moved to gitlab-ui.
+// See https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1869
+
+$scroll-scrim-height: 2.25rem;
+
+.gl-scroll-scrim {
+ .top-scrim-wrapper,
+ .bottom-scrim-wrapper {
+ height: $scroll-scrim-height;
+ opacity: 0;
+ position: sticky;
+ z-index: 1;
+ display: block;
+ left: 0;
+ right: 0;
+ pointer-events: none;
+ transition: opacity 0.1s;
+ }
+
+ .top-scrim-wrapper {
+ top: 0;
+ margin-bottom: -$scroll-scrim-height;
+
+ .top-scrim {
+ background: linear-gradient(180deg, var(--super-sidebar-bg, $gray-10) 0%, $transparent-rgba 100%);
+ }
+ }
+
+ .bottom-scrim-wrapper {
+ bottom: 0;
+ margin-top: -$scroll-scrim-height;
+
+ .bottom-scrim {
+ background: linear-gradient(180deg, $transparent-rgba 0%, var(--super-sidebar-bg, $gray-10));
+ }
+ }
+
+ .top-scrim,
+ .bottom-scrim {
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+ }
+
+ &.top-scrim-visible .top-scrim-wrapper,
+ &.bottom-scrim-visible .bottom-scrim-wrapper {
+ opacity: 1;
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 25542a86e8c..eefdbda8f4f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -89,7 +89,7 @@
font-weight: $gl-font-weight-bold;
margin: 24px 0 16px;
padding-bottom: 0.3em;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid $gray-200;
color: $gl-text-color;
&:first-child {
@@ -102,7 +102,7 @@
font-weight: $gl-font-weight-bold;
margin: 24px 0 16px;
padding-bottom: 0.3em;
- border-bottom: 1px solid $white-dark;
+ border-bottom: 1px solid $gray-200;
color: $gl-text-color;
}
@@ -138,7 +138,7 @@
&:dir(rtl) {
border-left: 0;
- border-right: 3px solid $white-dark;
+ border-right: 3px solid $gray-100;
}
p {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index ab8547c3fef..31948762972 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -1,3 +1,5 @@
+@import '@gitlab/ui/dist/tokens/scss/tokens';
+
/*
* Layout
*/
@@ -85,85 +87,10 @@ $size-scale: (
// Color schema
$darken-normal-factor: 7% !default;
$darken-dark-factor: 10% !default;
-$darken-border-factor: 5% !default;
-$darken-border-dashed-factor: 25% !default;
$purple: #6d49cb !default;
$purple-light: #ede8fb !default;
-$green-50: #ecf4ee !default;
-$green-100: #c3e6cd !default;
-$green-200: #91d4a8 !default;
-$green-300: #52b87a !default;
-$green-400: #2da160 !default;
-$green-500: #108548 !default;
-$green-600: #217645 !default;
-$green-700: #24663b !default;
-$green-800: #0d532a !default;
-$green-900: #0a4020 !default;
-$green-950: #072b15 !default;
-
-$blue-50: #e9f3fc !default;
-$blue-100: #cbe2f9 !default;
-$blue-200: #9dc7f1 !default;
-$blue-300: #63a6e9 !default;
-$blue-400: #428fdc !default;
-$blue-500: #1f75cb !default;
-$blue-600: #1068bf !default;
-$blue-700: #0b5cad !default;
-$blue-800: #064787 !default;
-$blue-900: #033464 !default;
-$blue-950: #002850 !default;
-
-$orange-50: #fdf1dd !default;
-$orange-100: #f5d9a8 !default;
-$orange-200: #e9be74 !default;
-$orange-300: #d99530 !default;
-$orange-400: #c17d10 !default;
-$orange-500: #ab6100 !default;
-$orange-600: #9e5400 !default;
-$orange-700: #8f4700 !default;
-$orange-800: #703800 !default;
-$orange-900: #5c2900 !default;
-$orange-950: #421f00 !default;
-
-$red-50: #fcf1ef !default;
-$red-100: #fdd4cd !default;
-$red-200: #fcb5aa !default;
-$red-300: #f57f6c !default;
-$red-400: #ec5941 !default;
-$red-500: #dd2b0e !default;
-$red-600: #c91c00 !default;
-$red-700: #ae1800 !default;
-$red-800: #8d1300 !default;
-$red-900: #660e00 !default;
-$red-950: #4d0a00 !default;
-
-$purple-50: #f4f0ff !default;
-$purple-100: #e1d8f9 !default;
-$purple-200: #cbbbf2 !default;
-$purple-300: #ac93e6 !default;
-$purple-400: #9475db !default;
-$purple-500: #7b58cf !default;
-$purple-600: #694cc0 !default;
-$purple-700: #5943b6 !default;
-$purple-800: #453894 !default;
-$purple-900: #2f2a6b !default;
-$purple-950: #232150 !default;
-
-$gray-10: #fbfafd !default;
-$gray-50: #ececef !default;
-$gray-100: #dcdcde !default;
-$gray-200: #bfbfc3 !default;
-$gray-300: #a4a3a8 !default;
-$gray-400: #89888d !default;
-$gray-500: #737278 !default;
-$gray-600: #626168 !default;
-$gray-700: #535158 !default;
-$gray-800: #434248 !default;
-$gray-900: #333238 !default;
-$gray-950: #1f1e24 !default;
-
$gray-lightest: lighten($gray-10, 1) !default;
$gray-light: $gray-10 !default;
$gray-lighter: lighten($gray-50, 4) !default;
@@ -172,211 +99,20 @@ $gray-dark: darken($gray-light, $darken-dark-factor) !default;
$gray-darker: $gray-50 !default;
$gray-darkest: $gray-200 !default;
-$t-gray-a-02: rgba($gray-950, 0.02) !default;
-$t-gray-a-04: rgba($gray-950, 0.04) !default;
-$t-gray-a-06: rgba($gray-950, 0.06) !default;
-$t-gray-a-08: rgba($gray-950, 0.08) !default;
-$t-gray-a-16: rgba($gray-950, 0.16) !default;
-$t-gray-a-24: rgba($gray-950, 0.24) !default;
-
-$white: #fff !default;
-$white-normal: $gray-50 !default;
-$white-dark: darken($gray-50, 2) !default;
-$white-transparent: rgba($white, 0.8) !default;
-
-$black: #000 !default;
-$black-transparent: $t-gray-a-24 !default;
-$almost-black: $gray-950 !default;
-
-$greens: (
- '50': $green-50,
- '100': $green-100,
- '200': $green-200,
- '300': $green-300,
- '400': $green-400,
- '500': $green-500,
- '600': $green-600,
- '700': $green-700,
- '800': $green-800,
- '900': $green-900,
- '950': $green-950
-);
-
-$blues: (
- '50': $blue-50,
- '100': $blue-100,
- '200': $blue-200,
- '300': $blue-300,
- '400': $blue-400,
- '500': $blue-500,
- '600': $blue-600,
- '700': $blue-700,
- '800': $blue-800,
- '900': $blue-900,
- '950': $blue-950
-);
-
-$oranges: (
- '50': $orange-50,
- '100': $orange-100,
- '200': $orange-200,
- '300': $orange-300,
- '400': $orange-400,
- '500': $orange-500,
- '600': $orange-600,
- '700': $orange-700,
- '800': $orange-800,
- '900': $orange-900,
- '950': $orange-950
-);
-
-$reds: (
- '50': $red-50,
- '100': $red-100,
- '200': $red-200,
- '300': $red-300,
- '400': $red-400,
- '500': $red-500,
- '600': $red-600,
- '700': $red-700,
- '800': $red-800,
- '900': $red-900,
- '950': $red-950
-);
-
-$purples: (
- '50': $purple-50,
- '100': $purple-100,
- '200': $purple-200,
- '300': $purple-300,
- '400': $purple-400,
- '500': $purple-500,
- '600': $purple-600,
- '700': $purple-700,
- '800': $purple-800,
- '900': $purple-900,
- '950': $purple-950
-);
-
-$grays: (
- '10': $gray-10,
- '50': $gray-50,
- '100': $gray-100,
- '200': $gray-200,
- '300': $gray-300,
- '400': $gray-400,
- '500': $gray-500,
- '600': $gray-600,
- '700': $gray-700,
- '800': $gray-800,
- '900': $gray-900,
- '950': $gray-950
-);
-
-$color-ranges: (
- 'primary': $blues,
- 'secondary': $grays,
- 'success': $greens,
- 'warning': $oranges,
- 'danger': $reds
-);
+$t-white-a-02: rgba(255, 255, 255, 0.02) !default;
+$t-white-a-04: rgba(255, 255, 255, 0.04) !default;
+$t-white-a-06: rgba(255, 255, 255, 0.06) !default;
+$t-white-a-08: rgba(255, 255, 255, 0.08) !default;
+$t-white-a-16: rgba(255, 255, 255, 0.16) !default;
+$t-white-a-24: rgba(255, 255, 255, 0.24) !default;
+$t-white-a-36: rgba(255, 255, 255, 0.36) !default;
-// GitLab themes
-
-$indigo-50: #f1f1ff;
-$indigo-100: #dbdbf8;
-$indigo-200: #c7c7f2;
-$indigo-300: #a2a2e6;
-$indigo-400: #8181d7;
-$indigo-500: #6666c4;
-$indigo-600: #5252b5;
-$indigo-700: #41419f;
-$indigo-800: #303083;
-$indigo-900: #222261;
-$indigo-950: #14143d;
-// To do this variant right for darkmode, we need to create a variable for it.
-$indigo-900-alpha-008: rgba($indigo-900, 0.08);
-
-$theme-blue-50: #cdd8e3;
-$theme-blue-100: #b9cadc;
-$theme-blue-200: #a6bdd5;
-$theme-blue-300: #81a5c9;
-$theme-blue-400: #628eb9;
-$theme-blue-500: #4977a5;
-$theme-blue-600: #346596;
-$theme-blue-700: #235180;
-$theme-blue-800: #153c63;
-$theme-blue-900: #0b2640;
-$theme-blue-950: #04101c;
-
-$theme-light-blue-50: #dde6ee;
-$theme-light-blue-100: #c1d4e6;
-$theme-light-blue-200: #a0bedc;
-$theme-light-blue-300: #74a3d3;
-$theme-light-blue-400: #4f8bc7;
-$theme-light-blue-500: #3476b9;
-$theme-light-blue-600: #2268ae;
-$theme-light-blue-700: #145aa1;
-$theme-light-blue-800: #0e4d8d;
-$theme-light-blue-900: #0c4277;
-$theme-light-blue-950: #0a3764;
-
-$theme-green-50: #dde9de;
-$theme-green-100: #b1d6b5;
-$theme-green-200: #8cc497;
-$theme-green-300: #69af7d;
-$theme-green-400: #499767;
-$theme-green-500: #308258;
-$theme-green-600: #25744c;
-$theme-green-700: #1b653f;
-$theme-green-800: #155635;
-$theme-green-900: #0e4328;
-$theme-green-950: #052e19;
-
-$theme-red-50: #f4e9e7;
-$theme-red-100: #ecd3d0;
-$theme-red-200: #e3bab5;
-$theme-red-300: #d59086;
-$theme-red-400: #c66e60;
-$theme-red-500: #ad4a3b;
-$theme-red-600: #a13322;
-$theme-red-700: #8f2110;
-$theme-red-800: #761405;
-$theme-red-900: #580d02;
-$theme-red-950: #380700;
-
-$theme-light-red-50: #faf2f1;
-$theme-light-red-100: #f6d9d5;
-$theme-light-red-200: #ebada2;
-$theme-light-red-300: #e07f6f;
-$theme-light-red-400: #d36250;
-$theme-light-red-500: #c24b38;
-$theme-light-red-600: #b53a26;
-$theme-light-red-700: #a02e1c;
-$theme-light-red-800: #8b2212;
-$theme-light-red-900: #751709;
-$theme-light-red-950: #5c1105;
-
-// Data visualization color palette
-
-$data-viz-blue-50: #e9ebff !default;
-$data-viz-blue-100: #d2dcff !default;
-$data-viz-blue-200: #b7c6ff !default;
-$data-viz-blue-300: #97acff !default;
-$data-viz-blue-400: #7992f5 !default;
-$data-viz-blue-500: #617ae2 !default;
-$data-viz-blue-600: #4e65cd !default;
-$data-viz-blue-700: #3f51ae !default;
-$data-viz-blue-800: #374291 !default;
-$data-viz-blue-900: #303470 !default;
-$data-viz-blue-950: #2a2b59 !default;
-
-$border-white-light: darken($white, $darken-border-factor) !default;
-$border-white-normal: darken($white-normal, $darken-border-factor) !default;
-
-$border-gray-light: darken($gray-light, $darken-border-factor);
-$border-gray-normal: darken($gray-normal, $darken-border-factor);
-$border-gray-normal-dashed: darken($gray-normal, $darken-border-dashed-factor);
+$t-gray-a-02: rgba(31, 30, 36, 0.02) !default;
+$t-gray-a-04: rgba(31, 30, 36, 0.04) !default;
+$t-gray-a-06: rgba(31, 30, 36, 0.06) !default;
+$t-gray-a-08: rgba(31, 30, 36, 0.08) !default;
+$t-gray-a-16: rgba(31, 30, 36, 0.16) !default;
+$t-gray-a-24: rgba(31, 30, 36, 0.24) !default;
/*
* UI elements
@@ -388,7 +124,6 @@ $shadow-color: $t-gray-a-08;
$well-expand-item: #e8f2f7 !default;
$well-inner-border: #eef0f2 !default;
$well-light-border: #f1f1f1;
-$well-light-text-color: #5b6169;
$nav-active-bg: $t-gray-a-08;
/*
@@ -409,7 +144,6 @@ $gl-text-color-disabled: $gray-400;
$link-color: $blue-500 !default;
$link-hover-color: $blue-500 !default;
$gl-grayish-blue: #7f8fa4;
-$gl-header-color: #4c4e54;
$gl-font-size-12: 12px;
$gl-font-size-14: 14px;
$gl-font-size-16: 16px;
@@ -472,14 +206,11 @@ $limited-layout-width: 1006px;
$fixed-layout-width: 1296px;
$container-margin: $gl-padding;
$container-margin-xl: $gl-padding-24;
-$container-text-max-width: 540px;
$border-radius-default: 4px;
$border-radius-small: 2px;
$border-radius-large: 8px;
$default-icon-size: 16px;
-$layout-link-gray: #7e7c7c;
$btn-side-margin: $grid-size;
-$count-arrow-border: #dce0e5;
$general-hover-transition-duration: 100ms;
$general-hover-transition-curve: linear;
$highlight-changes-color: rgb(235, 255, 232);
@@ -488,10 +219,8 @@ $system-header-height: 16px;
$system-footer-height: $system-header-height;
$mr-sticky-header-height: 72px;
$mr-review-bar-height: calc(2rem + 16px);
-$flash-height: 52px;
-$context-header-height: 60px;
$top-bar-height: 48px;
-$home-panel-title-row-height: 64px;
+$home-panel-title-row-height: 48px;
$home-panel-avatar-mobile-size: 24px;
$issuable-title-max-width: 350px;
$milestone-title-max-width: 75px;
@@ -501,8 +230,6 @@ $gl-line-height-20: 20px;
$gl-line-height-24: 24px;
$gl-line-height-14: 14px;
-$pages-group-name-color: #4c4e54;
-
/*
* Calculated heights
*/
@@ -512,12 +239,6 @@ $calc-application-header-height: calc(#{$calc-application-bars-height} + var(--t
$calc-application-footer-height: var(--system-footer-height);
$calc-application-viewport-height: calc(100vh - #{$calc-application-header-height} - #{$calc-application-footer-height});
-/*
-* Common component specific colors
-*/
-$user-mention-bg: rgba($blue-500, 0.044);
-$user-mention-bg-hover: rgba($blue-500, 0.15);
-
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
@@ -528,7 +249,6 @@ $tanuki-yellow: #fca326;
*/
$green-500-focus: rgba($green-500, 0.4);
$gl-btn-active-background: rgba(0, 0, 0, 0.16);
-$gl-btn-active-gradient: inset 0 2px 3px $gl-btn-active-background;
/*
* Commit Diff Colors
@@ -539,6 +259,7 @@ $line-added: #ecfdf0;
$line-added-dark: #c7f0d2 !default;
$line-removed: #fbe9eb;
$line-removed-dark: #fac5cd !default;
+
/*
* The transparent colors are used in Monaco editor. Using full opacity colors
* would hide other layers (selected text, matching brackets).
@@ -557,19 +278,13 @@ $line-removed-transparent: rgba(235, 145, 155, 0.2);
$line-removed-dark-transparent: rgba(246, 53, 85, 0.2);
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
-$line-number-select: #fbf2da;
-$line-number-commented: #dae5fb;
$line-target-blue: $blue-50;
-$line-select-yellow: #fcf8e7;
-$line-select-yellow-dark: #f0e2bd;
-$line-commented-blue: #e8effc;
-$line-commented-blue-dark: #bccef0;
$dark-diff-match-bg: rgba($white, 0.3);
$dark-diff-match-color: rgba($white, 0.1);
$diff-image-info-color: #808080;
$diff-view-modes-color: #808080;
$diff-view-modes-border: #c1c1c1;
-$diff-jagged-border-gradient-color: darken($white-normal, 8%);
+$diff-jagged-border-gradient-color: darken($gray-50, 8%);
/*
* Fonts
@@ -592,12 +307,9 @@ $dropdown-min-height: 40px;
$dropdown-max-height: 312px;
$dropdown-max-height-lg: 445px;
$dropdown-vertical-offset: 4px;
-$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-shadow-color: rgba(#000, 0.1);
$dropdown-title-btn-color: #bfbfbf;
-$dropdown-input-focus-shadow: rgba($blue-300, 0.4);
$dropdown-loading-bg: rgba($white, 0.6);
-$dropdown-chevron-size: 10px;
$dropdown-toggle-active-border-color: darken($border-color, 14%);
$dropdown-fade-mask-height: 32px;
$dropdown-member-form-control-width: 163px;
@@ -610,13 +322,10 @@ $filtered-search-term-shadow-color: rgba(0, 0, 0, 0.09);
/*
* Contextual Sidebar
*/
-$link-active-background: rgba($black, 0.04);
$link-hover-background: rgba($gray-900, 0.06);
-$inactive-badge-background: rgba($black, 0.08);
$sidebar-toggle-height: 60px;
$sidebar-toggle-width: 40px;
$sidebar-milestone-toggle-bottom-margin: 10px;
-$sidebar-avatar-size: 32px;
$sidebar-top-item-lr-margin: 8px;
$sidebar-top-item-tb-margin: 1px;
@@ -639,20 +348,6 @@ $gl-btn-small-line-height: 18px;
$badge-bg: rgba($black, 0.07);
/*
-* Pagination
-*/
-$pagination-padding-y: 6px;
-$pagination-padding-x: 16px;
-$pagination-line-height: 20px;
-$pagination-disabled-color: #cdcdcd;
-
-/*
- * Status icons
- */
-$status-icon-size: 22px;
-
-
-/*
* Social Icons
*/
$discord: #5865f2;
@@ -664,7 +359,6 @@ $skype: #0078d7;
* Award emoji
*/
$award-emoji-menu-shadow: rgba(0, 0, 0, 0.175);
-$award-emoji-positive-add-bg: #fed159;
$award-emoji-positive-add-lines: #bb9c13;
$award-emoji-width: 376px;
$award-emoji-width-xs: 90%;
@@ -672,7 +366,6 @@ $award-emoji-width-xs: 90%;
/*
* Search Box
*/
-$search-input-border-color: rgba($blue-400, 0.8);
$search-input-width: 200px;
$search-input-xl-width: 320px;
@@ -680,8 +373,6 @@ $search-input-xl-width: 320px;
* Notes
*/
$note-disabled-comment-color: #b2b2b2;
-$note-targe3-outside: #fffff0;
-$note-targe3-inside: #ffffd3;
/*
* Calendar
@@ -689,14 +380,8 @@ $note-targe3-inside: #ffffd3;
$calendar-user-contrib-text: #959494;
/*
-* CI
-*/
-$ci-skipped-color: #888;
-
-/*
* Boards
*/
-$issue-boards-font-size: 14px;
$issue-boards-card-shadow: rgba(0, 0, 0, 0.1);
/*
@@ -716,7 +401,6 @@ $blame-blue: #254e77;
* Builds
*/
$builds-log-bg: #111;
-$job-log-highlight-height: 18px;
$job-log-line-padding: 63px;
$job-line-number-width: 50px;
$job-line-number-margin: 51px;
@@ -760,20 +444,12 @@ $input-md-width: 240px;
$input-lg-width: 320px;
/*
-* Help
-*/
-$document-index-color: #888;
-$help-shortcut-header-color: #333;
-
-/*
* Label
*/
$label-font-size: 12px;
$label-padding: 7px;
-$label-padding-modal: 10px;
$label-gray-bg: #f8fafc;
$label-inverse-bg: #333;
-$label-remove-border: rgba(0, 0, 0, 0.1);
$label-border-radius: 100px;
/*
@@ -789,37 +465,17 @@ $fade-mask-transition-curve: ease-in-out;
$login-brand-holder-color: #888;
/*
-Stat Graph
-*/
-$stat-graph-common-bg: #f3f3f3;
-$stat-graph-selection-fill: #333;
-$stat-graph-selection-stroke: #333;
-
-/*
* Typography
*/
$body-text-shadow: rgba(255, 255, 255, 0.01);
/*
-* UI Dev Kit
-*/
-$ui-dev-kit-example-color: #bbb;
-
-/*
Pipeline Graph
*/
-$ci-action-icon-size: 22px;
-$ci-action-icon-size-lg: 24px;
-$pipeline-dropdown-line-height: 20px;
$ci-action-dropdown-button-size: 24px;
$ci-action-dropdown-svg-size: 16px;
/*
-CI variable lists
-*/
-$ci-variable-remove-button-width: calc(1em + #{2 * $gl-padding});
-
-/*
GitLab Plans
*/
$gl-ultimate-plan: #d4af37;
@@ -827,15 +483,10 @@ $gl-premium-plan: #91a1ab;
$gl-bronze-plan: #cd7f32;
/*
-Cross-project Pipelines
- */
-$linked-project-column-margin: 60px;
-
-/*
Performance Bar
*/
$perf-bar-production: $gray-950;
-$perf-bar-staging: $indigo-950;
+$perf-bar-staging: $theme-indigo-950;
$perf-bar-development: $red-900;
$perf-bar-bucket-bg: $black;
$perf-bar-bucket-box-shadow-from: rgba($white, 0.2);
@@ -875,11 +526,6 @@ $popup-triangle-border-size: 1px;
$popup-box-shadow-color: rgba(90, 90, 90, 0.05);
/*
-Multi file editor
-*/
-$border-color-settings: #e1e1e1;
-
-/*
Drawers
*/
$wide-drawer: 500px;
@@ -888,7 +534,6 @@ $wide-drawer: 500px;
Modals
*/
$modal-body-height: 80px;
-$modal-border-color: #e9ecef;
$modal-border-radius: 0.25rem;
$priority-label-empty-state-width: 114px;
@@ -900,11 +545,6 @@ $popover-max-width: 384px;
$popover-box-shadow: 0 2px 3px 1px $gray-100;
/*
-Issue Analytics
-*/
-$issues-analytics-popover-boarder-color: rgba(0, 0, 0, 0.15);
-
-/*
Merge requests
*/
$mr-tabs-height: 48px;
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 085e25a0cdc..23fa1326881 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -132,7 +132,7 @@
}
}
-@mixin line-hover-bg($color: $white-normal) {
+@mixin line-hover-bg($color: $gray-50) {
&:hover,
&:focus-within {
background-color: darken($color, 10);
diff --git a/app/assets/stylesheets/highlight/themes/none.scss b/app/assets/stylesheets/highlight/themes/none.scss
index f36eaa663e5..c2bc35ec91a 100644
--- a/app/assets/stylesheets/highlight/themes/none.scss
+++ b/app/assets/stylesheets/highlight/themes/none.scss
@@ -5,8 +5,8 @@
@import '../common';
@mixin match-line {
- color: $black-transparent;
- background-color: $white-normal;
+ color: $t-gray-a-24;
+ background-color: $gray-50;
}
:root {
@@ -40,13 +40,13 @@
.diff-line-num,
.diff-line-num a {
- color: $black-transparent;
+ color: $t-gray-a-24;
}
// Code itself
pre.code,
.diff-line-num {
- border-color: $white-normal;
+ border-color: $gray-50;
}
&,
@@ -86,7 +86,7 @@
&.new,
&.new-nomappinginraw,
&.old-nomappinginraw {
- background-color: $white-normal;
+ background-color: $gray-50;
}
}
@@ -137,27 +137,27 @@
.line_content {
&.old, &.old-nomappinginraw {
- background-color: $white-normal;
+ background-color: $gray-50;
&::before {
color: $gl-text-color;
}
span.idiff {
- background-color: $white-normal;
+ background-color: $gray-50;
text-decoration: underline;
}
}
&.new:not(.hll), &.new-nomappinginraw:not(.hll) {
- background-color: $white-normal;
+ background-color: $gray-50;
&::before {
color: $gl-text-color;
}
span.idiff {
- background-color: $white-normal;
+ background-color: $gray-50;
text-decoration: underline;
}
}
@@ -170,7 +170,7 @@
// Search result highlight
span.highlight_word {
- background-color: $white-normal;
+ background-color: $gray-50;
}
// Links to URLs, emails, or dependencies
diff --git a/app/assets/stylesheets/highlight/themes/solarized-light.scss b/app/assets/stylesheets/highlight/themes/solarized-light.scss
index b3aa10c3ace..c902d9357e8 100644
--- a/app/assets/stylesheets/highlight/themes/solarized-light.scss
+++ b/app/assets/stylesheets/highlight/themes/solarized-light.scss
@@ -101,7 +101,7 @@ $solarized-light-il: #2aa198;
}
@mixin match-line {
- color: $black-transparent;
+ color: $t-gray-a-24;
background: $solarized-light-matchline-bg;
}
diff --git a/app/assets/stylesheets/highlight/white_base.scss b/app/assets/stylesheets/highlight/white_base.scss
index 2631055706f..89d6d93614f 100644
--- a/app/assets/stylesheets/highlight/white_base.scss
+++ b/app/assets/stylesheets/highlight/white_base.scss
@@ -73,7 +73,7 @@ $white-gc-bg: #eaf2f5;
@mixin match-line {
- color: $black-transparent;
+ color: $t-gray-a-24;
background-color: $gray-light;
}
diff --git a/app/assets/stylesheets/mailers/highlighted_diff_email.scss b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
index fd212d14e30..c42b7baec39 100644
--- a/app/assets/stylesheets/mailers/highlighted_diff_email.scss
+++ b/app/assets/stylesheets/mailers/highlighted_diff_email.scss
@@ -93,8 +93,8 @@ $highlighted-gc-bg: #eaf2f5;
text-align: right;
width: 35px;
background-color: $gray-light;
- color: $black-transparent;
- border-right: 1px solid $white-normal;
+ color: $t-gray-a-24;
+ border-right: 1px solid $gray-50;
&.old {
background-color: $line-number-old;
@@ -130,7 +130,7 @@ $highlighted-gc-bg: #eaf2f5;
}
&.match {
- color: $black-transparent;
+ color: $t-gray-a-24;
background-color: $gray-light;
}
}
@@ -144,7 +144,7 @@ blockquote,
color: $gl-grayish-blue;
padding: 0 0 0 15px;
margin: 0;
- border-left: 3px solid $white-dark;
+ border-left: 3px solid $gray-100;
}
span.highlight_word {
diff --git a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
index d93b4f75d77..013e9e020fc 100644
--- a/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_monaco_overrides.scss
@@ -75,7 +75,7 @@
.diffOverview {
background-color: $white;
- border-left: 1px solid $white-dark;
+ border-left: 1px solid $border-color;
cursor: ns-resize;
}
@@ -92,7 +92,7 @@
}
.line-numbers {
- color: $black-transparent;
+ color: $t-gray-a-24;
}
.view-overlays {
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index c584bbaac09..5c8e9bce0e7 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -67,7 +67,7 @@
}
.drag-handle:hover {
- background-color: var(--ide-dropdown-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-hover-background, $gray-50);
}
.card-header {
@@ -147,7 +147,7 @@
.nav-links,
.gl-tabs-nav,
.common-note-form .md-area.is-focused .nav-links {
- border-color: var(--ide-border-color-alt, $white-dark);
+ border-color: var(--ide-border-color-alt, $border-color);
}
pre {
@@ -221,7 +221,7 @@
.filtered-search-token .value-container,
.filtered-search-term .value-container {
- background-color: var(--ide-dropdown-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-hover-background, $gray-50);
color: var(--ide-text-color, $gl-text-color);
&:hover {
@@ -291,14 +291,14 @@
&:hover,
&:focus {
- border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important;
- background-color: var(--ide-btn-default-background, $white-normal) !important;
+ border-color: var(--ide-btn-default-hover-border, $border-color) !important;
+ background-color: var(--ide-btn-default-background, $gray-50) !important;
}
&:active,
.active {
- border-color: var(--ide-btn-default-hover-border, $border-white-normal) !important;
- background-color: var(--ide-btn-default-background, $white-dark) !important;
+ border-color: var(--ide-btn-default-hover-border, $border-color) !important;
+ background-color: var(--ide-btn-default-background, $border-color) !important;
}
}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 22e42d0a7f7..66482ef42b5 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -131,6 +131,7 @@
height: $gl-padding-24;
border-radius: $gl-padding-24;
font-size: $gl-font-size-xs;
+ position: relative;
@include media-breakpoint-down(md) {
min-width: auto;
@@ -140,15 +141,9 @@
}
}
- .user-avatar-link:not(:only-child) {
- margin-left: -$gl-padding;
-
- &:nth-of-type(1) {
- z-index: 2;
- }
-
- &:nth-of-type(2) {
- z-index: 1;
+ .user-avatar-link {
+ &:not(:last-of-type) {
+ @include gl-mr-n3;
}
}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index 6165ee6e8b4..379f1470b20 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -82,6 +82,7 @@
.right-sidebar.build-sidebar {
padding: 0;
+ top: $calc-application-header-height;
@include media-breakpoint-up(lg) {
@include gl-border-l-0;
@@ -92,9 +93,7 @@
}
.sidebar-container {
- @include gl-sticky;
- top: #{$top-bar-height - 1px};
- max-height: calc(100vh - #{$top-bar-height - 1px} - var(--performance-bar-height));
+ max-height: 100%;
overflow-y: scroll;
overflow-x: hidden;
-webkit-overflow-scrolling: touch;
@@ -166,3 +165,64 @@
margin-bottom: 0;
}
}
+
+.job-log {
+ font-family: $monospace-font;
+ padding: $gl-padding-8 $input-horizontal-padding;
+ margin: 0 0 $gl-padding-8;
+ font-size: 13px;
+ word-break: break-all;
+ word-wrap: break-word;
+ color: color-yiq($builds-log-bg);
+ border-radius: 0 0 $border-radius-default $border-radius-default;
+ min-height: 42px;
+ background-color: $builds-log-bg;
+}
+
+.build-log-container:fullscreen {
+ overflow-y: scroll;
+
+ .top-bar {
+ top: 0 !important;
+ }
+}
+
+.job-log-line {
+ padding: 1px $gl-padding-8 1px $job-log-line-padding;
+ min-height: $gl-line-height-20;
+}
+
+.job-log-line-number {
+ color: $gray-500;
+ padding: 0 1em 0 $gl-padding-8;
+ min-width: $job-line-number-width;
+ margin-left: -$job-line-number-margin;
+ user-select: none;
+ display: inline-block;
+ text-align: right;
+
+ &:hover,
+ &:active,
+ &:visited {
+ text-decoration: underline;
+ color: $gray-500;
+ }
+}
+
+.job-log-line-header {
+ display: flex;
+ position: relative;
+ align-items: flex-start;
+
+ &:hover {
+ background-color: rgba($white, 0.2);
+ }
+
+ .arrow {
+ margin-left: -$job-arrow-margin;
+ }
+}
+
+.loader-animation {
+ @include build-loader-animation;
+}
diff --git a/app/assets/stylesheets/page_bundles/group.scss b/app/assets/stylesheets/page_bundles/group.scss
index 5086cdbf9bc..ae49993d3df 100644
--- a/app/assets/stylesheets/page_bundles/group.scss
+++ b/app/assets/stylesheets/page_bundles/group.scss
@@ -2,41 +2,12 @@
.group-home-panel {
.home-panel-avatar {
- width: $home-panel-title-row-height;
- height: $home-panel-title-row-height;
flex-basis: $home-panel-title-row-height;
}
.home-panel-title {
.icon {
- vertical-align: -1px;
- }
- }
-
- .home-panel-title-row {
- @include media-breakpoint-down(sm) {
- .home-panel-avatar {
- width: $home-panel-avatar-mobile-size;
- height: $home-panel-avatar-mobile-size;
- flex-basis: $home-panel-avatar-mobile-size;
-
- .avatar {
- font-size: 20px;
- line-height: 46px;
- }
- }
-
- .home-panel-title {
- margin-top: 4px;
- margin-bottom: 2px;
- font-size: $gl-font-size;
- line-height: $gl-font-size-large;
- }
-
-
- .home-panel-metadata {
- font-size: $gl-font-size-small;
- }
+ vertical-align: 1px;
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 7f8068e5d56..7e2bf4a03a3 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -9,6 +9,9 @@
@import './ide_themes/solarized-dark';
@import './ide_themes/monokai';
+// This whole file is for the legacy Web IDE
+// See: https://gitlab.com/groups/gitlab-org/-/epics/7683
+
$search-list-icon-width: 18px;
$ide-activity-bar-width: 60px;
$ide-context-header-padding: 10px;
@@ -18,6 +21,10 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
$ide-commit-row-height: 32px;
$ide-commit-header-height: 48px;
+.web-ide-loader {
+ padding-top: 1rem;
+}
+
.project-refs-form,
.project-refs-target-form {
display: inline-block;
@@ -67,15 +74,15 @@ $ide-commit-header-height: 48px;
display: flex;
flex-direction: column;
flex: 1;
- border-left: 1px solid var(--ide-border-color, $white-dark);
- border-right: 1px solid var(--ide-border-color, $white-dark);
+ border-left: 1px solid var(--ide-border-color, $border-color);
+ border-right: 1px solid var(--ide-border-color, $border-color);
overflow: hidden;
}
.multi-file-tabs {
display: flex;
background-color: var(--ide-background, $gray-light);
- box-shadow: inset 0 -1px var(--ide-border-color, $white-dark);
+ box-shadow: inset 0 -1px var(--ide-border-color, $border-color);
> ul {
display: flex;
@@ -87,8 +94,8 @@ $ide-commit-header-height: 48px;
align-items: center;
padding: $grid-size $gl-padding;
background-color: var(--ide-background-hover, $gray-normal);
- border-right: 1px solid var(--ide-border-color, $white-dark);
- border-bottom: 1px solid var(--ide-border-color, $white-dark);
+ border-right: 1px solid var(--ide-border-color, $border-color);
+ border-bottom: 1px solid var(--ide-border-color, $border-color);
&.active,
.gl-tab-nav-item-active {
@@ -129,12 +136,12 @@ $ide-commit-header-height: 48px;
font-weight: normal !important;
background-color: var(--ide-background-hover, $gray-normal);
- border-right: 1px solid var(--ide-border-color, $white-dark);
- border-bottom: 1px solid var(--ide-border-color, $white-dark);
+ border-right: 1px solid var(--ide-border-color, $border-color);
+ border-bottom: 1px solid var(--ide-border-color, $border-color);
&.gl-tab-nav-item-active {
background-color: var(--ide-highlight-background, $white);
- border-color: var(--ide-border-color, $white-dark);
+ border-color: var(--ide-border-color, $border-color);
border-bottom-color: transparent;
}
@@ -238,7 +245,7 @@ $ide-commit-header-height: 48px;
}
.ide-mode-tabs {
- border-bottom: 1px solid var(--ide-border-color, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color, $border-color);
li a {
padding: $gl-padding-8 $gl-padding;
@@ -253,7 +260,7 @@ $ide-commit-header-height: 48px;
.ide-status-bar {
color: var(--ide-text-color, $gl-text-color);
- border-top: 1px solid var(--ide-border-color, $white-dark);
+ border-top: 1px solid var(--ide-border-color, $border-color);
padding: 2px $gl-padding-8 0;
background-color: var(--ide-footer-background, $white);
display: flex;
@@ -351,8 +358,8 @@ $ide-commit-header-height: 48px;
flex: 1;
flex-direction: column;
background-color: var(--ide-highlight-background, $white);
- border-left: 1px solid var(--ide-border-color, $white-dark);
- border-top: 1px solid var(--ide-border-color, $white-dark);
+ border-left: 1px solid var(--ide-border-color, $border-color);
+ border-top: 1px solid var(--ide-border-color, $border-color);
border-top-left-radius: $border-radius-small;
min-height: 0; // firefox fix
}
@@ -377,7 +384,7 @@ $ide-commit-header-height: 48px;
.multi-file-commit-panel-header {
height: $ide-commit-header-height;
- border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color-alt, $border-color);
padding: 12px 0;
}
@@ -436,7 +443,7 @@ $ide-commit-header-height: 48px;
}
&.is-active {
- background-color: var(--ide-background, $white-normal);
+ background-color: var(--ide-background, $gray-50);
}
svg {
@@ -458,7 +465,7 @@ $ide-commit-header-height: 48px;
.multi-file-commit-form {
position: relative;
background-color: var(--ide-highlight-background, $white);
- border-left: 1px solid var(--ide-border-color, $white-dark);
+ border-left: 1px solid var(--ide-border-color, $border-color);
transition: all 0.3s ease;
> form,
@@ -466,7 +473,7 @@ $ide-commit-header-height: 48px;
padding: $gl-padding 0;
margin-left: $gl-padding;
margin-right: $gl-padding;
- border-top: 1px solid var(--ide-border-color-alt, $white-dark);
+ border-top: 1px solid var(--ide-border-color-alt, $border-color);
}
.btn {
@@ -517,7 +524,6 @@ $ide-commit-header-height: 48px;
.ide-empty-state {
display: flex;
- height: 100vh;
align-items: center;
justify-content: center;
background-color: var(--ide-empty-state-background, transparent);
@@ -526,6 +532,7 @@ $ide-commit-header-height: 48px;
.ide {
overflow: hidden;
flex: 1;
+ height: calc(100vh - var(--top-bar-height))
}
.ide-commit-list-container {
@@ -537,7 +544,7 @@ $ide-commit-header-height: 48px;
margin-right: $gl-padding;
&.is-first {
- border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color-alt, $border-color);
}
}
@@ -545,7 +552,7 @@ $ide-commit-header-height: 48px;
width: $ide-commit-row-height;
height: $ide-commit-row-height;
color: inherit;
- background-color: var(--ide-background, $white-normal);
+ background-color: var(--ide-background, $gray-50);
}
.ide-commit-options {
@@ -596,8 +603,8 @@ $ide-commit-header-height: 48px;
width: calc(100% + 1px);
padding-right: $gl-padding + 1px;
background-color: var(--ide-highlight-background, $white);
- border-top-color: var(--ide-border-color, $white-dark);
- border-bottom-color: var(--ide-border-color, $white-dark);
+ border-top-color: var(--ide-border-color, $border-color);
+ border-bottom-color: var(--ide-border-color, $border-color);
&::after {
content: '';
@@ -707,7 +714,7 @@ $ide-commit-header-height: 48px;
padding: 12px 0;
margin-left: $ide-tree-padding;
margin-right: $ide-tree-padding;
- border-bottom: 1px solid var(--ide-border-color-alt, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color-alt, $border-color);
svg {
color: var(--ide-text-color-secondary, $gray-500);
@@ -740,7 +747,7 @@ $ide-commit-header-height: 48px;
background-color: var(--ide-input-background, transparent);
&:hover {
- background-color: var(--ide-dropdown-btn-hover-background, $white-normal);
+ background-color: var(--ide-dropdown-btn-hover-background, $gray-50);
}
svg {
@@ -899,7 +906,7 @@ $ide-commit-header-height: 48px;
.multi-file-commit-panel-inner {
padding: $grid-size 0;
background-color: var(--ide-highlight-background, $white);
- border-right: 1px solid var(--ide-border-color, $white-dark);
+ border-right: 1px solid var(--ide-border-color, $border-color);
}
.ide-right-sidebar-jobs-detail {
@@ -1063,7 +1070,7 @@ $ide-commit-header-height: 48px;
&:active,
&:focus {
- color: $white-normal;
+ color: $gray-50;
background-color: var(--ide-link-color, $blue-500);
outline: 0;
}
@@ -1077,7 +1084,7 @@ $ide-commit-header-height: 48px;
}
.dropdown.show .ide-entry-dropdown-toggle {
- color: $white-normal;
+ color: $gray-50;
background-color: var(--ide-link-color, $blue-500);
}
}
@@ -1085,7 +1092,7 @@ $ide-commit-header-height: 48px;
.ide-file-templates {
padding: $grid-size $gl-padding;
background-color: var(--ide-background, $gray-light);
- border-bottom: 1px solid var(--ide-border-color, $white-dark);
+ border-bottom: 1px solid var(--ide-border-color, $border-color);
.dropdown {
min-width: 180px;
@@ -1100,7 +1107,7 @@ $ide-commit-header-height: 48px;
height: 65px;
padding: 8px 16px;
background-color: var(--ide-background, $gray-10);
- box-shadow: inset 0 -1px var(--ide-border-color, $white-dark);
+ box-shadow: inset 0 -1px var(--ide-border-color, $border-color);
}
.ide-commit-list-changed-icon {
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 05563f8e314..8b353b42f58 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -105,3 +105,8 @@
@include gl-font-weight-normal;
}
}
+
+[data-page="projects:issues:show"] .top-bar-fixed,
+[data-page="groups:epics:show"] .top-bar-fixed {
+ width: 100%;
+}
diff --git a/app/assets/stylesheets/page_bundles/merge_request.scss b/app/assets/stylesheets/page_bundles/merge_request.scss
index 8dc4401e72c..70aeedb10bf 100644
--- a/app/assets/stylesheets/page_bundles/merge_request.scss
+++ b/app/assets/stylesheets/page_bundles/merge_request.scss
@@ -250,6 +250,14 @@ $comparison-empty-state-height: 62px;
}
}
+.diffs.tab-pane {
+ @include media-breakpoint-up(md) {
+ // ensure consistent page height when selected file is loading
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/426250
+ min-height: 100vh;
+ }
+}
+
// Wrap MR tabs/buttons so you don't have to scroll on desktop
@include media-breakpoint-down(md) {
.merge-request-tabs-container {
@@ -296,7 +304,7 @@ $comparison-empty-state-height: 62px;
.merge-request-details .file-finder-overlay.diff-file-finder {
position: fixed;
z-index: 99999;
- background: $black-transparent;
+ background: $t-gray-a-24;
}
.mr-compare {
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 847cd3f2ff4..d112fd83ebf 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -487,11 +487,6 @@ $tabs-holder-z-index: 250;
background: var(--white, $white);
> .mr-widget-section {
- > :first-child {
- border-top-left-radius: $border-radius-default - 1px;
- border-top-right-radius: $border-radius-default - 1px;
- }
-
> :last-child,
.deploy-heading:last-child {
border-bottom-left-radius: $border-radius-default - 1px;
@@ -552,8 +547,13 @@ $tabs-holder-z-index: 250;
.mr-widget-section:not(:first-child) > div,
.mr-widget-section:not(:first-child) > section,
- .mr-widget-section .mr-widget-section > div {
+ .mr-widget-section .mr-widget-section > div:not(:first-child) {
border-top: solid 1px var(--border-color, $border-color);
+ // Avoid two lines being rendered
+ // instead of exessively adressing those
+ // edge cases we can use this as a boring
+ // solution
+ margin-top: -1px;
}
.mr-widget-alert-container + .mr-widget-section {
@@ -1056,7 +1056,7 @@ $tabs-holder-z-index: 250;
}
.merge-request-sticky-header {
- z-index: 204;
+ z-index: $top-bar-z-index;
height: $mr-sticky-header-height;
}
@@ -1115,10 +1115,6 @@ $tabs-holder-z-index: 250;
border-top: 1px solid var(--border-color, $border-color);
transition: padding $gl-transition-duration-medium;
- .page-with-icon-sidebar & {
- padding-left: $contextual-sidebar-collapsed-width;
- }
-
@media (max-width: map-get($grid-breakpoints, sm)-1) {
padding-left: 0;
padding-right: 0;
@@ -1126,6 +1122,10 @@ $tabs-holder-z-index: 250;
.submit-review-dropdown {
margin-left: $grid-size;
+
+ .md-header {
+ top: -$gl-spacing-scale-2;
+ }
}
}
@@ -1214,3 +1214,7 @@ $tabs-holder-z-index: 250;
@include gl-rounded-top-right-none;
}
}
+
+.merge-request-overview .md-header {
+ top: calc(#{$calc-application-header-height} + #{$mr-sticky-header-height});
+}
diff --git a/app/assets/stylesheets/page_bundles/milestone.scss b/app/assets/stylesheets/page_bundles/milestone.scss
index 8dc07715989..7a9c7487a7e 100644
--- a/app/assets/stylesheets/page_bundles/milestone.scss
+++ b/app/assets/stylesheets/page_bundles/milestone.scss
@@ -90,7 +90,7 @@
}
.reference {
- border-top: 1px solid $border-gray-normal;
+ border-top: 1px solid $border-color;
}
}
}
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index aaec277cf08..9bab5d65b59 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -218,7 +218,7 @@
min-width: 195px;
left: 100%;
top: -10px;
- box-shadow: 0 1px 5px $black-transparent;
+ box-shadow: 0 1px 5px $t-gray-a-24;
}
.codequality-report {
@@ -303,7 +303,7 @@
.pipeline-show-container,
.pipeline-links-container {
@media (max-width: $breakpoint-sm) {
- width: 100%;
+ flex-basis: 100%;
}
}
diff --git a/app/assets/stylesheets/page_bundles/pipelines.scss b/app/assets/stylesheets/page_bundles/pipelines.scss
index bcc0ad112ac..d61e3f85995 100644
--- a/app/assets/stylesheets/page_bundles/pipelines.scss
+++ b/app/assets/stylesheets/page_bundles/pipelines.scss
@@ -24,7 +24,7 @@
.btn.btn-retry:hover,
.btn.btn-retry:focus {
border-color: $dropdown-toggle-active-border-color;
- background-color: $white-normal;
+ background-color: $gray-50;
}
svg path {
@@ -42,8 +42,8 @@
}
.btn-group.open .btn-default {
- background-color: $white-normal;
- border-color: $border-white-normal;
+ background-color: $gray-50;
+ border-color: $gray-100;
}
.btn .text-center {
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 2c08db048fd..9a8eeb9c9d6 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -234,7 +234,7 @@
color: $skype;
}
-.twitter-icon {
+.x-icon {
color: var(--gl-text-color, $gl-text-color);
}
diff --git a/app/assets/stylesheets/page_bundles/profiles/preferences.scss b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
index d09ad42a722..502674deec8 100644
--- a/app/assets/stylesheets/page_bundles/profiles/preferences.scss
+++ b/app/assets/stylesheets/page_bundles/profiles/preferences.scss
@@ -13,11 +13,11 @@
margin-bottom: $gl-padding-8;
&.ui-indigo {
- background-color: $indigo-900;
+ background-color: $theme-indigo-900;
}
&.ui-light-indigo {
- background-color: $indigo-700;
+ background-color: $theme-indigo-700;
}
&.ui-blue {
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index 8d8da10268a..c2ecf3702f9 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -7,7 +7,7 @@
.home-panel-title {
.icon {
- vertical-align: -1px;
+ vertical-align: 1px;
}
.home-panel-topic-list {
@@ -17,28 +17,6 @@
}
}
- .home-panel-title-row {
- @include media-breakpoint-down(sm) {
- .home-panel-avatar {
- width: $home-panel-avatar-mobile-size;
- height: $home-panel-avatar-mobile-size;
- flex-basis: $home-panel-avatar-mobile-size;
-
- .avatar {
- font-size: 20px;
- line-height: 46px;
- }
- }
-
- .home-panel-title {
- margin-top: 4px;
- margin-bottom: 2px;
- font-size: $gl-font-size;
- line-height: $gl-font-size-large;
- }
- }
- }
-
.home-panel-description {
@include media-breakpoint-up(md) {
font-size: $gl-font-size-large;
@@ -53,7 +31,7 @@
}
}
- .project-clone-holder {
+ .project-code-holder {
display: inline-block;
margin: $gl-padding 0 0;
@@ -134,7 +112,7 @@
.stat-text,
.stat-link {
- padding: $gl-btn-vert-padding 0;
+ padding: $gl-btn-vert-padding;
background-color: transparent;
font-size: $gl-font-size;
line-height: $gl-btn-line-height;
@@ -149,7 +127,6 @@
&:hover,
&:focus {
text-decoration: underline;
- border-bottom: 0;
}
.project-stat-value {
@@ -159,13 +136,6 @@
.icon {
color: var(--gray-500, $gl-text-color-secondary);
}
-
- .add-license-link {
- &,
- .icon {
- color: var(--blue-600, $blue-600);
- }
- }
}
.btn {
@@ -186,3 +156,58 @@
color: var(--gl-text-color, $gl-text-color);
}
}
+
+// FF :project_overview_reorg enabled
+.project-page-indicator:not(.hidden) + .project-page-layout {
+ @include media-breakpoint-up(lg) {
+ display: grid;
+ grid-template-columns: auto $right-sidebar-width;
+ gap: 2rem;
+
+ .project-page-layout-content,
+ .project-page-layout-sidebar {
+ min-width: 1px;
+ }
+
+ .project-page-layout-sidebar {
+ order: 2;
+ overflow-x: clip;
+ margin-right: -$gl-padding-8;
+ }
+
+ .project-page-sidebar {
+ position: sticky;
+ top: calc(#{$calc-application-header-height} + #{$gl-spacing-scale-4});
+ width: calc(100% + 100px);
+ height: calc(
+ #{$calc-application-viewport-height} - #{$gl-spacing-scale-4}
+ );
+ padding-inline: $gl-padding-4;
+ overflow-y: scroll;
+ overflow-x: hidden;
+ -webkit-overflow-scrolling: touch;
+
+ .project-page-sidebar-block {
+ width: $right-sidebar-width - 1px;
+
+ &:first-of-type {
+ padding-top: $gl-spacing-scale-1;
+ }
+ }
+
+ .nav {
+ > li {
+ width: 100%;
+ }
+
+ .btn {
+ justify-content: flex-start;
+
+ &:not(.btn-dashed) {
+ box-shadow: none;
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/projects.scss b/app/assets/stylesheets/page_bundles/projects.scss
index d252afd0b29..bfa350097fa 100644
--- a/app/assets/stylesheets/page_bundles/projects.scss
+++ b/app/assets/stylesheets/page_bundles/projects.scss
@@ -235,8 +235,7 @@
}
.repository-languages-bar {
- height: 8px;
- margin-bottom: $gl-padding;
+ height: 0.5rem;
background-color: var(--white, $white);
border-radius: $border-radius-default;
@@ -407,18 +406,6 @@
}
@include media-breakpoint-down(md) {
- .avatar-container {
- @include avatar-size(40px, 10px);
- min-height: 40px;
- min-width: 40px;
-
- .identicon.s64 {
- font-size: 16px;
- }
- }
- }
-
- @include media-breakpoint-down(md) {
.updated-note {
@include gl-mt-3;
@include gl-text-right;
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index a3a62b44e98..b145d046fa4 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -31,26 +31,6 @@ $language-filter-max-height: 20rem;
}
}
-.search-sidebar {
- @include media-breakpoint-down(lg) {
- max-width: 100%;
- }
-
- @include media-breakpoint-down(xl) {
- min-width: $search-sidebar-min-width;
- max-width: $search-sidebar-min-width;
- }
-
- @include media-breakpoint-up(xl) {
- min-width: $search-sidebar-max-width;
- max-width: $search-sidebar-max-width;
- }
-
- .language-filter-max-height {
- max-height: $language-filter-max-height;
- }
-}
-
.issue-filters {
.label-filter {
list-style: none;
diff --git a/app/assets/stylesheets/page_bundles/terms.scss b/app/assets/stylesheets/page_bundles/terms.scss
index 139627072be..727cdcf0627 100644
--- a/app/assets/stylesheets/page_bundles/terms.scss
+++ b/app/assets/stylesheets/page_bundles/terms.scss
@@ -26,16 +26,6 @@
justify-content: space-between;
line-height: $line-height-base;
- .navbar-collapse {
- padding-right: 0;
- flex-grow: 0;
- flex-basis: auto;
-
- .navbar-nav {
- margin: 0;
- }
- }
-
.nav li {
float: none;
}
diff --git a/app/assets/stylesheets/page_bundles/tree.scss b/app/assets/stylesheets/page_bundles/tree.scss
index 66d828ed87d..5266849bb30 100644
--- a/app/assets/stylesheets/page_bundles/tree.scss
+++ b/app/assets/stylesheets/page_bundles/tree.scss
@@ -78,10 +78,6 @@
.btn-group {
width: 100%;
}
-
- .btn {
- margin-top: 10px;
- }
}
}
diff --git a/app/assets/stylesheets/page_bundles/users.scss b/app/assets/stylesheets/page_bundles/users.scss
index d4cd28504fc..76d60593c8a 100644
--- a/app/assets/stylesheets/page_bundles/users.scss
+++ b/app/assets/stylesheets/page_bundles/users.scss
@@ -1,48 +1,7 @@
@import 'mixins_and_variables_and_functions';
-.user-search-form {
- position: relative;
-
- @include media-breakpoint-up(sm) {
- float: right;
- }
-
- .dropdown {
- width: 100%;
- margin-top: 5px;
-
- .dropdown-menu-toggle {
- vertical-align: middle;
- width: 100%;
- }
-
- @include media-breakpoint-up(sm) {
- margin-top: 0;
- width: 155px;
- }
- }
-
- .form-control {
- padding-right: 35px;
- }
-
- .search-control-wrap,
- .form-control {
- width: 100%;
-
- @include media-breakpoint-up(sm) {
- width: 250px;
- }
- }
-}
-
.user-search-btn {
- position: absolute;
- right: 4px;
- top: 0;
- height: 35px;
- padding-left: 10px;
- padding-right: 10px;
+ top: 1px;
color: $gray-darkest;
background: transparent;
border: 0;
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 81e6b4c1191..ed2c7662a98 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -90,8 +90,6 @@
}
a {
- color: var(--gray-400, $gray-400);
-
&:hover,
&.active {
text-decoration: none;
@@ -103,18 +101,16 @@
}
.active > .wiki-list {
- a,
- .wiki-list-expand-button,
- .wiki-list-collapse-button {
- color: $black;
- }
+ background-color: $gray-50;
}
.wiki-list {
- min-height: $gl-spacing-scale-8;
+ padding: $gl-spacing-scale-2 $gl-spacing-scale-3;
+ margin-bottom: $gl-spacing-scale-1;
+ @include gl-rounded-base;
&:hover {
- background: $gray-10;
+ background: $gray-50;
.wiki-list-create-child-button {
display: block;
@@ -148,8 +144,7 @@
margin: 0;
}
- ul.wiki-pages ul,
- ul.wiki-pages li:not(.wiki-directory){
+ ul.wiki-pages ul {
padding-left: 20px;
}
@@ -162,16 +157,6 @@
}
}
-.right-sidebar.wiki-sidebar {
- .active > .wiki-list {
- a,
- .wiki-list-expand-button,
- .wiki-list-collapse-button {
- color: $white;
- }
- }
-}
-
ul.wiki-pages-list.content-list {
a {
color: var(--blue-600, $blue-600);
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index ec73f27ed09..b9ab2450ff9 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -215,3 +215,130 @@ $work-item-sticky-header-height: 52px;
font-weight: normal;
}
}
+
+// Disclosure hierarchy component, used for Ancestors widget
+
+$disclosure-hierarchy-chevron-dimension: 1.2rem;
+
+@mixin hierarchy-active-item-color {
+ background-color: var(--gray-50, $gray-50);
+
+ &::after {
+ background-color: var(--gray-50, $gray-50);
+ }
+}
+
+@mixin hierarchy-path-chevron {
+ content: '';
+ @include gl-absolute;
+ @include gl-reset-bg;
+ top: 0.39rem;
+ right: px-to-rem(-9px);
+ width: $disclosure-hierarchy-chevron-dimension;
+ height: $disclosure-hierarchy-chevron-dimension;
+ transform: rotate(45deg) skew(14deg, 14deg);
+}
+
+.disclosure-hierarchy-button {
+ @include gl-pl-4;
+ @include gl-py-3;
+ @include gl-display-flex;
+ @include gl-relative;
+ @include gl-font-sm;
+ border: 1px solid var(--gray-100, $gray-100);
+ @include gl-border-r-none;
+ @include gl-border-l-none;
+ @include gl-line-height-normal;
+ padding-right: $grid-size;
+ max-width: $gl-spacing-scale-20;
+ background: var(--gray-10, $white);
+
+ @include media-breakpoint-up(sm) {
+ max-width: $gl-spacing-scale-48;
+ }
+
+ &::before,
+ &::after {
+ @include hierarchy-path-chevron;
+ border: 1px solid var(--gray-100, $gray-100);
+ border-color: inherit;
+ @include gl-border-b-transparent;
+ @include gl-border-l-transparent;
+ @include gl-reset-bg;
+ @include gl-rounded-top-left-small;
+ @include gl-rounded-bottom-right-small;
+ }
+
+ &::before {
+ background: var(--gray-10, $white);
+ left: -10px;
+ z-index: 1;
+ }
+
+ &::after {
+ z-index: 0;
+ }
+
+ .disclosure-hierarchy-item:first-child & {
+ @include gl-pl-3;
+ border-left: 1px solid var(--gray-100, $gray-100);
+ @include gl-rounded-top-left-base;
+ @include gl-rounded-bottom-left-base;
+
+ &::before {
+ @include gl-display-none;
+ }
+
+ &:active,
+ &:focus,
+ &:focus:active {
+ // Custom focus
+ box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400, -1px 1px 0 1px $blue-400, -1px -1px 0 1px $blue-400 !important;
+ }
+ }
+
+ .disclosure-hierarchy-item:last-child & {
+ @include gl-pr-4;
+ border-right: 1px solid var(--gray-100, $gray-100);
+ @include gl-rounded-top-right-base;
+ @include gl-rounded-bottom-right-base;
+
+ &::after {
+ display: none;
+ }
+ }
+
+ &[disabled] {
+ color: $gl-text-color-disabled;
+ @include gl-cursor-not-allowed;
+ }
+
+ &:not([disabled]):hover {
+ @include gl-border-gray-400;
+ @include hierarchy-active-item-color;
+ color: var(--gray-900, $gray-900);
+
+ &::after {
+ border-left: 1px solid var(--gray-50, $gray-50);
+ border-bottom: 1px solid var(--gray-50, $gray-50);
+ z-index: 3;
+ }
+ }
+
+ &:active,
+ &:focus,
+ &:focus:active {
+ // Custom focus
+ box-shadow: 1px 1px 0 1px $blue-400, 2px -1px 0 1px $blue-400 !important;
+ outline: none;
+ border-top: 1px solid var(--gray-400, $gray-400);
+ border-bottom: 1px solid var(--gray-400, $gray-400);
+ @include hierarchy-active-item-color;
+ z-index: 2;
+ @include gl-rounded-small;
+
+ &::before, &::after {
+ box-shadow: 2px -2px 0 1px $blue-400;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 72ea586979f..f1055590539 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -7,7 +7,7 @@
background: none;
word-break: normal;
overflow-x: auto;
- border-left: 3px solid $white-dark;
+ border-left: 3px solid $gray-100;
color: $gl-text-color-secondary;
}
@@ -79,7 +79,7 @@
.commits-row {
+ .commits-row {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
+ .commits-empty {
@@ -93,7 +93,7 @@
color: $gl-text-color-secondary;
padding: 1px $gl-padding-4;
cursor: pointer;
- border: 1px solid $border-white-normal;
+ border: 1px solid $gray-100;
border-radius: $border-radius-default;
margin-left: 5px;
font-size: 12px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index f6c79a4eca2..cfb964e6227 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -5,7 +5,7 @@
.event-item {
font-size: $gl-font-size;
padding: $gl-padding 0 $gl-padding $gl-spacing-scale-8;
- border-bottom: 1px solid $white-normal;
+ border-bottom: 1px solid $gray-50;
color: $gl-text-color-secondary;
position: relative;
line-height: $gl-line-height-20;
@@ -158,36 +158,6 @@
}
}
-@include media-breakpoint-down(xs) {
- .event-item {
- padding-left: 0;
-
- .event-user-info {
- margin-bottom: $gl-padding-4;
- }
-
- .event-title {
- white-space: normal;
- overflow: visible;
- max-width: 100%;
- }
-
- .system-note-image {
- display: none;
- }
-
- .event-body {
- margin-top: $gl-padding-4;
- margin-right: 0;
- padding-left: 0;
- }
-
- .event-item-timestamp {
- display: none;
- }
- }
-}
-
// hide event scope (namespace + project) where it is not necessary
.project-activity {
.event-scope {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 2e1bb9b9eac..d01286bd209 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -141,7 +141,7 @@ table.pipeline-project-metrics tr td {
top: 5px;
bottom: 0;
left: -16px;
- border-left: 2px solid $border-white-normal;
+ border-left: 2px solid $border-color;
}
.group-row {
@@ -152,7 +152,7 @@ table.pipeline-project-metrics tr td {
display: block;
width: 10px;
height: 0;
- border-top: 2px solid $border-white-normal;
+ border-top: 2px solid $border-color;
position: absolute;
top: 30px;
left: -16px;
@@ -179,7 +179,7 @@ table.pipeline-project-metrics tr td {
}
&:first-child {
- border-top: 1px solid $white-normal;
+ border-top: 1px solid $gray-50;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index e82a689fe5d..9748983d1ae 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -29,7 +29,7 @@
.issue-token:hover &,
.issue-token-link:focus > & {
- background-color: $border-gray-normal;
+ background-color: $gray-100;
}
}
@@ -41,7 +41,7 @@
&:focus,
.issue-token:hover &,
.issue-token-link:focus + & {
- background-color: $border-gray-normal;
+ background-color: $gray-100;
outline: none;
}
}
@@ -268,3 +268,25 @@ ul.related-merge-requests > li gl-emoji {
.issuable-header-slide-leave-to {
transform: translateY(-100%);
}
+
+.issuable-sticky-header-visible {
+ --issuable-sticky-header-height: 40px;
+}
+
+.md-header-preview {
+ z-index: 1;
+ position: sticky;
+ top: calc(#{$calc-application-header-height} + var(--issuable-sticky-header-height, 0px));
+}
+
+.detail-page-description .md-header {
+ top: $calc-application-header-height;
+}
+
+.gl-drawer .md-header {
+ top: 0;
+}
+
+.gl-modal .md-header {
+ top: -$gl-padding-8;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 38686d5e713..5d644d63666 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -56,7 +56,7 @@
&.is-dropzone-hover {
border-color: $green-500;
- box-shadow: 0 0 2px $black-transparent,
+ box-shadow: 0 0 2px $t-gray-a-24,
0 0 4px $green-500-focus;
.comment-toolbar,
@@ -326,7 +326,6 @@ table {
.discussion-reply-holder {
.reply-placeholder-text-field {
- @include gl-font-monospace;
border-radius: $gl-border-radius-base;
width: 100%;
resize: none;
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8e0fab04ab2..8792c7f9a72 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -264,11 +264,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
display: block;
position: relative;
- .timeline-discussion-body {
- overflow-x: auto;
- overflow-y: hidden;
- }
-
.diff-content {
overflow: visible;
padding: 0;
@@ -330,8 +325,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.note-body {
padding: 0 $gl-padding-8 $gl-padding-8;
- overflow-x: auto;
- overflow-y: hidden;
.note-text {
word-wrap: break-word;
@@ -747,8 +740,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
.timeline-content {
- overflow-x: auto;
- overflow-y: hidden;
border-radius: $gl-border-radius-base;
padding: $gl-padding-8 !important;
@include gl-border;
@@ -995,7 +986,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.disabled-comment {
background-color: $gray-light;
border-radius: $border-radius-base;
- border: 1px solid $border-gray-normal;
+ border: 1px solid $border-color;
color: $note-disabled-comment-color;
padding: $gl-padding-8 0;
@@ -1136,10 +1127,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
.user-activity-content {
- &::before {
- @include vertical-line(80px, 25px);
- background: var(--gray-50, $gray-50);
- }
+ @include gl-relative;
.system-note-image {
@include gl--flex-center;
@@ -1153,6 +1141,16 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
+.user-profile-activity {
+ @include gl-relative;
+
+ &:not(:last-child)::before {
+ @include vertical-line(16px, 10px);
+ height: 100%;
+ background: var(--gray-50, $gray-50);
+ }
+}
+
//This needs to be deleted when Snippet/Commit comments are convered to Vue
// See https://gitlab.com/gitlab-org/gitlab-foss/issues/53918#note_117038785
.unstyled-comments {
@@ -1164,3 +1162,17 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
padding: $gl-padding;
}
}
+
+.project-activity-item:not(:last-of-type) {
+ position: relative;
+
+ &::before {
+ // Avatar width is 32px, connecting line width is 2px.
+ // To center the line relatively to the avatar it should be positioned 15px from the left:
+ // (32px (avatar size) - 2px (line thickness)) / 2 = 15px
+ // stylelint-disable length-zero-no-unit
+ @include vertical-line(0px, 15px);
+ top: auto; // Override top to auto align
+ background: var(--gray-50, $gray-50);
+ }
+}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 3015cfec34f..315b9c829a7 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -39,7 +39,7 @@
content: ' ';
height: 100%;
width: 4px;
- background-color: $white-dark;
+ background-color: $gray-100;
}
position: relative;
@@ -57,8 +57,6 @@
header,
nav,
-nav.navbar-collapse,
-nav.navbar-collapse.collapse,
.nav-sidebar,
.super-sidebar,
.profiler-results,
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index e249ecbd10b..91b381462be 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -82,12 +82,12 @@
font-size: $code-font-size;
line-height: $code-line-height;
white-space: nowrap;
- color: $black-transparent;
+ color: $t-gray-a-24;
min-width: 30px;
}
.diff-line-num:hover {
- color: $almost-black;
+ color: $gray-950;
cursor: pointer;
}
}
@@ -158,8 +158,8 @@
border-right: 0;
&:hover {
- background-color: $white-normal;
- border-color: $border-white-normal;
+ background-color: $gray-50;
+ border-color: $gray-100;
text-decoration: none;
}
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index 36fa457f244..cb0da7e782d 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -1,15 +1,4 @@
-$gray-10: #1f1e24;
-$gray-50: #333238;
-$gray-100: #434248;
-$gray-200: #535158;
-$gray-300: #626168;
-$gray-400: #737278;
-$gray-500: #89888d;
-$gray-600: #a4a3a8;
-$gray-700: #bfbfc3;
-$gray-800: #dcdcde;
-$gray-900: #ececef;
-$gray-950: #fbfafd;
+@import '@gitlab/ui/dist/tokens/scss/tokens.dark';
$gray-lightest: lighten($gray-10, 1);
$gray-light: lighten($gray-10, 2);
@@ -19,108 +8,14 @@ $gray-dark: darken($gray-100, 2);
$gray-darker: darken($gray-200, 2);
$gray-darkest: $gray-700;
-// Some of the other $t-gray-a variables are used
-// for borders and some other places, so we cannot override
-// them. These are used only for box shadows so we can
-$t-gray-a-16: rgba($gray-10, 0.16);
-$t-gray-a-24: rgba($gray-10, 0.24);
+// Used for border and background in a couple instances where inverting between modes is desirable
+// once migrated to suitable color values this can be removed
+$t-gray-a-08: rgba($gray-950, 0.08);
-$black: #fff;
$black-normal: $gray-900;
-$white: $gray-50;
-$white-normal: $gray-50;
-$white-dark: $gray-100;
-
-$green-50: #0a4020;
-$green-100: #0d532a;
-$green-200: #24663b;
-$green-300: #217645;
-$green-400: #108548;
-$green-500: #2da160;
-$green-600: #52b87a;
-$green-700: #91d4a8;
-$green-800: #c3e6cd;
-$green-900: #ecf4ee;
-$green-950: #f1fdf6;
-
-$blue-50: #033464;
-$blue-100: #064787;
-$blue-200: #0b5cad;
-$blue-300: #1068bf;
-$blue-400: #1f75cb;
-$blue-500: #428fdc;
-$blue-600: #63a6e9;
-$blue-700: #9dc7f1;
-$blue-800: #cbe2f9;
-$blue-900: #e9f3fc;
-$blue-950: #f2f9ff;
-
-$orange-50: #5c2900;
-$orange-100: #703800;
-$orange-200: #8f4700;
-$orange-300: #9e5400;
-$orange-400: #ab6100;
-$orange-500: #c17d10;
-$orange-600: #d99530;
-$orange-700: #e9be74;
-$orange-800: #f5d9a8;
-$orange-900: #fdf1dd;
-$orange-950: #fff4e1;
-
-$red-50: #660e00;
-$red-100: #8d1300;
-$red-200: #ae1800;
-$red-300: #c91c00;
-$red-400: #dd2b0e;
-$red-500: #ec5941;
-$red-600: #f57f6c;
-$red-700: #fcb5aa;
-$red-800: #fdd4cd;
-$red-900: #fcf1ef;
-$red-950: #fff4f3;
-
-$indigo-50: #1a1a40;
-$indigo-100: #292961;
-$indigo-200: #393982;
-$indigo-300: #4b4ba3;
-$indigo-400: #5b5bbd;
-$indigo-500: #6666c4;
-$indigo-600: #7c7ccc;
-$indigo-700: #a6a6de;
-$indigo-800: #d1d1f0;
-$indigo-900: #ebebfa;
-$indigo-950: #f7f7ff;
-
-$purple-50: #232150;
-$purple-100: #2f2a6b;
-$purple-200: #453894;
-$purple-300: #5943b6;
-$purple-400: #694cc0;
-$purple-500: #7b58cf;
-$purple-600: #9475db;
-$purple-700: #ac93e6;
-$purple-800: #cbbbf2;
-$purple-900: #e1d8f9;
-$purple-950: #f4f0ff;
-
-$theme-indigo-50: #1a1a40;
$border-color: #4f4f4f;
-$data-viz-blue-50: #2a2b59;
-$data-viz-blue-100: #303470;
-$data-viz-blue-200: #374291;
-$data-viz-blue-300: #3f51ae;
-$data-viz-blue-400: #4e65cd;
-$data-viz-blue-500: #617ae2;
-$data-viz-blue-600: #7992f5;
-$data-viz-blue-700: #97acff;
-$data-viz-blue-800: #b7c6ff;
-$data-viz-blue-900: #d2dcff;
-$data-viz-blue-950: #e9ebff;
-
-$border-white-normal: $border-color;
-
$gl-text-color-secondary: $gray-700;
$body-bg: $gray-10;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index c0eced48171..3ab3e195b06 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -1,93 +1,10 @@
@import './themes/dark';
+@import '@gitlab/ui/dist/tokens/css/tokens.dark';
@import 'page_bundles/mixins_and_variables_and_functions';
@import './themes/theme_helper';
:root {
color-scheme: dark;
- --gray-10: #{$gray-10};
- --gray-50: #{$gray-50};
- --gray-100: #{$gray-100};
- --gray-200: #{$gray-200};
- --gray-300: #{$gray-300};
- --gray-400: #{$gray-400};
- --gray-500: #{$gray-500};
- --gray-600: #{$gray-600};
- --gray-700: #{$gray-700};
- --gray-800: #{$gray-800};
- --gray-900: #{$gray-900};
- --gray-950: #{$gray-950};
-
- --green-50: #{$green-50};
- --green-100: #{$green-100};
- --green-200: #{$green-200};
- --green-300: #{$green-300};
- --green-400: #{$green-400};
- --green-500: #{$green-500};
- --green-600: #{$green-600};
- --green-700: #{$green-700};
- --green-800: #{$green-800};
- --green-900: #{$green-900};
- --green-950: #{$green-950};
-
- --blue-50: #{$blue-50};
- --blue-100: #{$blue-100};
- --blue-200: #{$blue-200};
- --blue-300: #{$blue-300};
- --blue-400: #{$blue-400};
- --blue-500: #{$blue-500};
- --blue-600: #{$blue-600};
- --blue-700: #{$blue-700};
- --blue-800: #{$blue-800};
- --blue-900: #{$blue-900};
- --blue-950: #{$blue-950};
-
- --orange-50: #{$orange-50};
- --orange-100: #{$orange-100};
- --orange-200: #{$orange-200};
- --orange-300: #{$orange-300};
- --orange-400: #{$orange-400};
- --orange-500: #{$orange-500};
- --orange-600: #{$orange-600};
- --orange-700: #{$orange-700};
- --orange-800: #{$orange-800};
- --orange-900: #{$orange-900};
- --orange-950: #{$orange-950};
-
- --red-50: #{$red-50};
- --red-100: #{$red-100};
- --red-200: #{$red-200};
- --red-300: #{$red-300};
- --red-400: #{$red-400};
- --red-500: #{$red-500};
- --red-600: #{$red-600};
- --red-700: #{$red-700};
- --red-800: #{$red-800};
- --red-900: #{$red-900};
- --red-950: #{$red-950};
-
- --indigo-50: #{$indigo-50};
- --indigo-100: #{$indigo-100};
- --indigo-200: #{$indigo-200};
- --indigo-300: #{$indigo-300};
- --indigo-400: #{$indigo-400};
- --indigo-500: #{$indigo-500};
- --indigo-600: #{$indigo-600};
- --indigo-700: #{$indigo-700};
- --indigo-800: #{$indigo-800};
- --indigo-900: #{$indigo-900};
- --indigo-950: #{$indigo-950};
-
- --purple-50: #{$purple-50};
- --purple-100: #{$purple-100};
- --purple-200: #{$purple-200};
- --purple-300: #{$purple-300};
- --purple-400: #{$purple-400};
- --purple-500: #{$purple-500};
- --purple-600: #{$purple-600};
- --purple-700: #{$purple-700};
- --purple-800: #{$purple-800};
- --purple-900: #{$purple-900};
- --purple-950: #{$purple-950};
--dark-icon-color-purple-1: #524a68;
--dark-icon-color-purple-2: #715bae;
@@ -98,8 +15,6 @@
--gl-text-color: #{$gray-900};
--border-color: #{$border-color};
- --white: #{$white};
- --black: #{$black};
--gray-light: #{$gray-50};
--svg-status-bg: #{$white};
@@ -209,13 +124,6 @@
border-color: $gray-800;
}
-.nav-sidebar,
-.toggle-sidebar-button,
-.close-nav-button {
- background-color: darken($gray-50, 4%);
- border-right: 1px solid $gray-50;
-}
-
.gl-avatar:not(.gl-avatar-identicon),
.avatar-container,
.avatar {
@@ -227,83 +135,17 @@
box-shadow: inset 0 0 0 1px rgba($gray-950, $gl-avatar-border-opacity);
}
-.nav-sidebar {
- .sidebar-sub-level-items.fly-out-list {
- box-shadow: none;
- border: 1px solid $border-color;
- }
-}
-
aside.right-sidebar:not(.right-sidebar-merge-requests) {
background-color: $gray-10;
- border-left-color: $gray-50;
}
:root.gl-dark {
- @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-900, $white);
-
.terms {
.logo-text {
fill: var(--black);
}
}
-
- .navbar.navbar-gitlab {
- background-color: var(--gray-50);
- box-shadow: 0 1px 0 0 var(--gray-100);
-
- .navbar-sub-nav,
- .navbar-nav {
- li {
- > a:hover,
- > a:focus,
- > button:hover,
- > button:focus {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
- }
- }
-
- li.active,
- li.dropdown.show {
- > a,
- > button {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
- }
- }
- }
-
- .header-search-form {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--border-color) !important;
-
- &:active,
- &:hover {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--blue-200) !important;
- }
- }
-
- .search {
- form {
- background-color: var(--gray-100);
- box-shadow: inset 0 0 0 1px var(--border-color);
-
- &:active,
- &:hover {
- background-color: var(--gray-100);
- box-shadow: inset 0 0 0 1px var(--blue-200);
- }
-
- .search-input {
- color: var(--gl-text-color);
- }
- }
- }
- }
-
.md :not(pre.code) > code {
background-color: $gray-200;
}
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
index 749120a0ecb..1a373fbfeda 100644
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ b/app/assets/stylesheets/themes/theme_blue.scss
@@ -2,18 +2,10 @@
:root {
&.ui-blue {
- @include gitlab-theme(
- $theme-blue-200,
- $theme-blue-500,
- $theme-blue-700,
- $theme-blue-900,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-blue-50,
- $theme-blue-200,
+ $theme-blue-100,
$theme-blue-900,
$theme-blue-900,
);
diff --git a/app/assets/stylesheets/themes/theme_gray.scss b/app/assets/stylesheets/themes/theme_gray.scss
index 70611e692cd..9a24142f286 100644
--- a/app/assets/stylesheets/themes/theme_gray.scss
+++ b/app/assets/stylesheets/themes/theme_gray.scss
@@ -2,14 +2,6 @@
:root {
&.ui-gray {
- @include gitlab-theme(
- $gray-200,
- $gray-300,
- $gray-500,
- $gray-900,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$gray-50,
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
index ae969873692..a766fdddc78 100644
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ b/app/assets/stylesheets/themes/theme_green.scss
@@ -2,18 +2,10 @@
:root {
&.ui-green {
- @include gitlab-theme(
- $theme-green-200,
- $theme-green-500,
- $theme-green-700,
- $theme-green-900,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-green-50,
- $theme-green-200,
+ $theme-green-100,
$theme-green-900,
$theme-green-900,
);
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index db20034419a..c94a32891f6 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -2,342 +2,35 @@
/**
* Styles the GitLab application with a specific color theme
*/
-@mixin gitlab-theme(
- $search-and-nav-links,
- $accent,
- $border-and-box-shadow,
- $navbar-theme-color,
- $navbar-theme-contrast-color
-) {
- // Set custom properties
-
- --gl-theme-accent: #{$accent};
-
- $search-and-nav-links-a20: rgba($search-and-nav-links, 0.2);
- $search-and-nav-links-a30: rgba($search-and-nav-links, 0.3);
- $search-and-nav-links-a40: rgba($search-and-nav-links, 0.4);
- $search-and-nav-links-a80: rgba($search-and-nav-links, 0.8);
-
- // Header
-
- .navbar-gitlab:not(.super-sidebar-logged-out) {
- background-color: $navbar-theme-color;
-
- .navbar-collapse {
- color: $search-and-nav-links;
- }
-
- .container-fluid {
- .navbar-toggler {
- border-left: 1px solid lighten($border-and-box-shadow, 10%);
- color: $search-and-nav-links;
- }
- }
-
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a,
- > button {
- &:hover,
- &:focus {
- background-color: $search-and-nav-links-a20;
- }
- }
-
- &.active,
- &.dropdown.show {
- > a,
- > button {
- color: $navbar-theme-color;
- background-color: $navbar-theme-contrast-color;
- }
- }
-
- &.line-separator {
- border-left: 1px solid $search-and-nav-links-a20;
- }
- }
- }
-
- .navbar-sub-nav {
- color: $search-and-nav-links;
- }
-
- .nav {
- > li {
- color: $search-and-nav-links;
-
- &.header-search {
- color: $gray-900;
- }
-
- > a {
- .notification-dot {
- border: 2px solid $navbar-theme-color;
- }
-
- &.header-help-dropdown-toggle {
- .notification-dot {
- background-color: $search-and-nav-links;
- }
- }
-
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $search-and-nav-links;
- }
- }
-
- &:hover,
- &:focus {
- @include media-breakpoint-up(sm) {
- background-color: $search-and-nav-links-a20;
- }
-
- svg {
- fill: currentColor;
- }
-
- .notification-dot {
- will-change: border-color, background-color;
- border-color: adjust-color($navbar-theme-color, $red: 33, $green: 33, $blue: 33);
- }
-
- &.header-help-dropdown-toggle .notification-dot {
- background-color: $white;
- }
- }
- }
-
- &.active > a,
- &.dropdown.show > a {
- color: $navbar-theme-color;
- background-color: $navbar-theme-contrast-color;
-
- &:hover {
- svg {
- fill: $navbar-theme-color;
- }
- }
-
- .notification-dot {
- border-color: $white;
- }
-
- &.header-help-dropdown-toggle {
- .notification-dot {
- background-color: $navbar-theme-color;
- }
- }
- }
-
- .impersonated-user,
- .impersonated-user:hover {
- svg {
- fill: $navbar-theme-color;
- }
- }
- }
- }
- }
-
- .navbar .title {
- > a {
- &:hover,
- &:focus {
- background-color: $search-and-nav-links-a20;
- }
- }
- }
-
- .header-search-form {
- background-color: $search-and-nav-links-a20 !important;
- border-radius: $border-radius-default;
-
- &:hover {
- background-color: $search-and-nav-links-a30 !important;
- }
-
- &.is-focused {
- input {
- background-color: $white;
- color: $gl-text-color !important;
- box-shadow: inset 0 0 0 1px $gray-900;
-
- &:focus {
- box-shadow: inset 0 0 0 1px $gray-900, 0 0 0 1px $white, 0 0 0 3px $blue-400;
- }
-
- &::placeholder {
- color: $gray-400;
- }
- }
- }
-
- svg.gl-search-box-by-type-search-icon {
- color: $search-and-nav-links-a80;
- }
-
- input {
- background-color: transparent;
- color: $search-and-nav-links-a80;
- box-shadow: inset 0 0 0 1px $search-and-nav-links-a40;
-
- &::placeholder {
- color: $search-and-nav-links-a80;
- }
-
- &:focus,
- &:active {
- &::placeholder {
- color: $gray-400;
- }
- }
- }
-
- .keyboard-shortcut-helper {
- color: $search-and-nav-links;
- background-color: $search-and-nav-links-a20;
- }
- }
-
- .search {
- form {
- background-color: $search-and-nav-links-a20;
-
- &:hover {
- background-color: $search-and-nav-links-a30;
- }
- }
-
- .search-input::placeholder {
- color: $search-and-nav-links-a80;
- }
-
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- fill: $search-and-nav-links-a80;
- }
- }
-
- &.search-active {
- form {
- background-color: $white;
- }
-
- .search-input-wrap {
- .search-icon {
- fill: $search-and-nav-links-a80;
- }
- }
- }
- }
-
- .search-sidebar {
- .nav-link {
- &.active,
- &:hover {
- background-color: rgba($gray-50, 0.8);
- color: $gray-900;
- }
- }
- }
-
- // Sidebar
- .nav-sidebar li.active > a {
- color: $gray-900;
- }
-
- .nav-sidebar {
- .fly-out-top-item {
- a,
- a:hover,
- &.active a,
- .fly-out-top-item-container {
- background-color: var(--gray-100, $gray-50);
- color: var(--gray-900, $gray-900);
- }
- }
- }
-
- .branch-header-title {
- color: $border-and-box-shadow;
- }
-
- .ide-sidebar-link {
- &.active {
- color: $border-and-box-shadow;
-
- &.is-right {
- box-shadow: inset -3px 0 $border-and-box-shadow;
- }
- }
- }
-}
-
@mixin gitlab-theme-super-sidebar(
$theme-color-lightest,
$theme-color-light,
$theme-color,
$theme-color-darkest,
) {
- --sidebar-background: #{mix(white, $theme-color-lightest, 50%)};
- --transparent-white-16: rgba(255, 255, 255, 0.16);
- --transparent-white-24: rgba(255, 255, 255, 0.24);
-
.super-sidebar {
- background-color: var(--sidebar-background);
- }
-
- .super-sidebar .user-bar {
- background-color: $theme-color;
-
- .counter {
- background-color: var(--transparent-white-16) !important;
- }
-
- .brand-logo,
- .btn-default-tertiary,
- .counter {
- color: $theme-color-lightest;
- mix-blend-mode: normal;
-
- &:hover,
- &:focus {
- background-color: var(--transparent-white-24) !important;
- color: $white;
- }
-
- .gl-icon {
- color: $theme-color-light;
- }
- }
- }
-
- .super-sidebar hr {
- mix-blend-mode: multiply;
- }
-
- .btn-with-notification {
- &:hover,
- &:focus {
+ --super-sidebar-bg: #{mix(white, $theme-color-lightest, 50%)};
+ --super-sidebar-user-bar-bg: #{$theme-color};
+ --super-sidebar-primary: #{$theme-color};
+ --super-sidebar-notification-dot: #{$theme-color-darkest};
+
+ --super-sidebar-user-bar-button-bg: #{$t-white-a-16};
+ --super-sidebar-user-bar-button-color: #{$theme-color-lightest};
+ --super-sidebar-user-bar-button-border-color: #{$t-white-a-16};
+ --super-sidebar-user-bar-button-hover-bg: #{$t-white-a-24};
+ --super-sidebar-user-bar-button-hover-color: #{$white};
+ --super-sidebar-user-bar-button-active-bg: #{$t-white-a-36};
+
+ --super-sidebar-user-bar-button-icon-color: #{$theme-color-light};
+ --super-sidebar-user-bar-button-icon-hover-color: #{$theme-color-light};
+ --super-sidebar-user-bar-button-icon-mix-blend-mode: screen;
+
+ hr {
mix-blend-mode: multiply;
}
- .notification-dot-info {
- background-color: $theme-color-darkest;
- border-color: $theme-color-lightest;
-
+ .super-sidebar-context-header {
+ color: var(--super-sidebar-primary);
}
}
-
- .active-indicator {
- background-color: $theme-color;
- }
-
- .super-sidebar-context-header {
- color: $theme-color;
- }
}
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
index d7e8ddadf46..d0a8d597b59 100644
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_indigo.scss
@@ -2,18 +2,10 @@
:root {
&.ui-indigo {
- @include gitlab-theme(
- $indigo-200,
- $indigo-500,
- $indigo-700,
- $indigo-900,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-indigo-50,
- $theme-indigo-200,
+ $theme-indigo-100,
$theme-indigo-900,
$theme-indigo-900,
);
diff --git a/app/assets/stylesheets/themes/theme_light_blue.scss b/app/assets/stylesheets/themes/theme_light_blue.scss
index 430960f563f..e712b6ae859 100644
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ b/app/assets/stylesheets/themes/theme_light_blue.scss
@@ -2,18 +2,10 @@
:root {
&.ui-light-blue {
- @include gitlab-theme(
- $theme-light-blue-200,
- $theme-light-blue-500,
- $theme-light-blue-500,
- $theme-light-blue-700,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-light-blue-50,
- $theme-light-blue-200,
+ $theme-light-blue-100,
$theme-light-blue-700,
$theme-light-blue-900,
);
diff --git a/app/assets/stylesheets/themes/theme_light_gray.scss b/app/assets/stylesheets/themes/theme_light_gray.scss
index f63da3f22f1..5cb9bee37b0 100644
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ b/app/assets/stylesheets/themes/theme_light_gray.scss
@@ -1,102 +1,2 @@
-@import './theme_helper';
-
-:root {
- &.ui-light-gray {
- @include gitlab-theme(
- $gray-500,
- $gray-700,
- $gray-500,
- $gray-50,
- $gray-500
- );
-
- .navbar-gitlab:not(.super-sidebar-logged-out) {
- background-color: $gray-50;
- box-shadow: 0 1px 0 0 $border-color;
-
- .logo-text {
- fill: #171321;
- }
-
- .navbar-sub-nav,
- .navbar-nav {
- > li {
- > a:hover,
- > a:focus,
- > button:hover {
- color: $gray-900;
- }
-
- &.active > a,
- &.active > a:hover,
- &.active > button {
- color: $white;
- }
-
- > a,
- > button {
- &:active,
- &:focus {
- @include gl-focus;
- }
- }
- }
- }
-
- .container-fluid {
- .navbar-toggler,
- .navbar-toggler:hover {
- color: $gray-500;
- border-left: 1px solid $gray-100;
- }
- }
- }
-
- .header-search-form {
- background-color: $white !important;
- box-shadow: inset 0 0 0 1px $border-color !important;
- border-radius: $border-radius-default;
-
- &:hover {
- background-color: $white !important;
- box-shadow: inset 0 0 0 1px $blue-200 !important;
- }
- }
-
- .search {
- form {
- background-color: $white;
- box-shadow: inset 0 0 0 1px $border-color;
-
- &:hover {
- background-color: $white;
- box-shadow: inset 0 0 0 1px $blue-200;
- }
- }
-
- .search-input-wrap {
- .search-icon {
- fill: $gray-100;
- }
-
- .search-input {
- color: $gl-text-color;
- }
- }
- }
-
- .nav-sidebar li.active {
- > a {
- color: $gray-900;
- }
-
- svg {
- fill: $gray-900;
- }
- }
-
- .sidebar-top-level-items > li.active .badge.badge-pill {
- color: $gray-900;
- }
- }
-}
+// "Light gray" is the default unthemed state of the sidebar.
+// Nothing to do here.
diff --git a/app/assets/stylesheets/themes/theme_light_green.scss b/app/assets/stylesheets/themes/theme_light_green.scss
index 05adc56c36a..44e19b02e36 100644
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ b/app/assets/stylesheets/themes/theme_light_green.scss
@@ -2,18 +2,10 @@
:root {
&.ui-light-green {
- @include gitlab-theme(
- $theme-green-200,
- $theme-green-500,
- $theme-green-500,
- $theme-green-700,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-green-50,
- $theme-green-200,
+ $theme-green-100,
$theme-green-700,
$theme-green-900,
);
diff --git a/app/assets/stylesheets/themes/theme_light_indigo.scss b/app/assets/stylesheets/themes/theme_light_indigo.scss
index 04bcfaf8366..ab299ca9d84 100644
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ b/app/assets/stylesheets/themes/theme_light_indigo.scss
@@ -2,18 +2,10 @@
:root {
&.ui-light-indigo {
- @include gitlab-theme(
- $indigo-200,
- $indigo-500,
- $indigo-500,
- $indigo-700,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-indigo-50,
- $theme-indigo-200,
+ $theme-indigo-100,
$theme-indigo-700,
$theme-indigo-900,
);
diff --git a/app/assets/stylesheets/themes/theme_light_red.scss b/app/assets/stylesheets/themes/theme_light_red.scss
index c4952b8e155..499cdace772 100644
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ b/app/assets/stylesheets/themes/theme_light_red.scss
@@ -2,18 +2,10 @@
:root {
&.ui-light-red {
- @include gitlab-theme(
- $theme-light-red-200,
- $theme-light-red-500,
- $theme-light-red-500,
- $theme-light-red-700,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-light-red-50,
- $theme-light-red-200,
+ $theme-light-red-100,
$theme-light-red-700,
$theme-light-red-900,
);
diff --git a/app/assets/stylesheets/themes/theme_red.scss b/app/assets/stylesheets/themes/theme_red.scss
index 536963e12ef..9a17f98aa80 100644
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ b/app/assets/stylesheets/themes/theme_red.scss
@@ -2,18 +2,10 @@
:root {
&.ui-red {
- @include gitlab-theme(
- $theme-red-200,
- $theme-red-500,
- $theme-red-700,
- $theme-red-900,
- $white
- );
-
.page-with-super-sidebar {
@include gitlab-theme-super-sidebar(
$theme-red-50,
- $theme-red-200,
+ $theme-red-100,
$theme-red-900,
$theme-red-900,
);
diff --git a/app/assets/stylesheets/tmp_utilities.scss b/app/assets/stylesheets/tmp_utilities.scss
deleted file mode 100644
index 96464aa5a39..00000000000
--- a/app/assets/stylesheets/tmp_utilities.scss
+++ /dev/null
@@ -1,32 +0,0 @@
-/**
- * DISCLAIMER
- * This is a temporary stylesheet meant to assist in migrating away from desktop-first responsive
- * CSS utilities.
- * DO NOT add utils in here unless you are actively taking part in in the migration.
- * We needed this new file for temporary utils to be defined _after_ the main, non-responsive
- * GitLab UI util.
- * This file is scheduled to be removed by the end of 2023.
- */
- .gl-sm-w-25p {
- @include gl-media-breakpoint-up(sm) {
- width: 25%;
- }
-}
-
-.gl-sm-w-30p {
- @include gl-media-breakpoint-up(sm) {
- width: 30%;
- }
-}
-
-.gl-sm-w-40p {
- @include gl-media-breakpoint-up(sm) {
- width: 40%;
- }
-}
-
-.gl-sm-w-75p {
- @include gl-media-breakpoint-up(sm) {
- width: 75%;
- }
-}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 347b8e20ab4..79ea8d3cc70 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -37,7 +37,7 @@
.border-color-default { border-color: $border-color; }
.border-radius-default { border-radius: $border-radius-default; }
.border-radius-small { border-radius: $border-radius-small; }
-.box-shadow-default { box-shadow: 0 2px 4px 0 $black-transparent; }
+.box-shadow-default { box-shadow: 0 2px 4px 0 $t-gray-a-24; }
// Override Bootstrap class with offset for system-header and
// performance bar when present
diff --git a/app/assets/stylesheets/vendors/atwho.scss b/app/assets/stylesheets/vendors/atwho.scss
index cf7dc79c5f5..6a2f37beed0 100644
--- a/app/assets/stylesheets/vendors/atwho.scss
+++ b/app/assets/stylesheets/vendors/atwho.scss
@@ -1,27 +1,24 @@
.atwho-view {
overflow-y: auto;
overflow-x: hidden;
- max-width: calc(100% - 6px);
+ min-width: $gl-new-dropdown-min-width;
+ max-width: $gl-new-dropdown-max-width;
+
@include gl-border-b-1;
@include gl-border-b-solid;
@include gl-border-b-gray-100;
@include gl-rounded-lg;
@include gl-shadow-md;
- .name,
- small.aliases,
- small.params {
- float: left;
- }
- small.aliases,
- small.params {
- padding: 2px 5px;
+ small {
+ @include gl-font-sm;
}
small.description {
- float: right;
- padding: 3px 5px;
+ display: block;
+ width: auto;
+ @include gl-mt-2;
}
.avatar-inline {
@@ -42,24 +39,22 @@
}
}
- ul > li {
- @include clearfix;
- white-space: nowrap;
- }
-
// TODO: fallback to global style
.atwho-view-ul {
- @include gl-p-2;
+ @include gl-py-2;
max-height: $gl-max-dropdown-max-height;
li {
- @include gl-px-3;
- padding-top: $gl-padding-6;
- padding-bottom: $gl-padding-6;
border: 0;
- @include gl-rounded-base;
+ padding: $gl-padding-6;
+
+ @include gl-my-2;
+ @include gl-mx-3;
+ @include gl-rounded-small;
+ @include gl-line-height-normal;
&.cur {
+ @include gl-focus;
background-color: $gray-darker;
color: $gl-text-color;
@@ -78,16 +73,18 @@
align-items: center;
}
- .center {
- line-height: 14px;
- }
-
strong {
color: $gl-text-color;
}
gl-emoji {
@include gl-mr-2;
+ vertical-align: text-top;
+
+ img {
+ margin-block: -0.1em;
+ top: 0.05em;
+ }
}
.dropdown-label-box {