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>2024-01-16 13:42:19 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-16 13:42:19 +0300
commit84d1bd786125c1c14a3ba5f63e38a4cc736a9027 (patch)
treef550fa965f507077e20dbb6d61a8269a99ef7107 /app/assets
parent3a105e36e689f7b75482236712f1a47fd5a76814 (diff)
Add latest changes from gitlab-org/gitlab@16-8-stable-eev16.8.0-rc42
Diffstat (limited to 'app/assets')
-rw-r--r--app/assets/images/mailers/in_product_marketing/admin_verify-0.pngbin30421 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-0.pngbin10275 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-1.pngbin39565 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/create-2.pngbin15793 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/experience-0.pngbin66492 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team-0.pngbin42448 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team-1.pngbin62019 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/team.png (renamed from app/assets/images/mailers/in_product_marketing/team-2.png)bin54468 -> 54468 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-0.pngbin50665 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-1.pngbin8676 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/trial-2.pngbin47411 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify-0.pngbin15366 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify-1.pngbin60722 -> 0 bytes
-rw-r--r--app/assets/images/mailers/in_product_marketing/verify.png (renamed from app/assets/images/mailers/in_product_marketing/verify-2.png)bin57506 -> 57506 bytes
-rw-r--r--app/assets/javascripts/admin/abuse_report/components/user_details.vue36
-rw-r--r--app/assets/javascripts/admin/abuse_report/constants.js6
-rw-r--r--app/assets/javascripts/api/alert_management_alerts_api.js4
-rw-r--r--app/assets/javascripts/badges/components/badge_list.vue15
-rw-r--r--app/assets/javascripts/behaviors/secret_values.js47
-rw-r--r--app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue4
-rw-r--r--app/assets/javascripts/blob/openapi/index.js6
-rw-r--r--app/assets/javascripts/boards/components/board_app.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_card_move_to_position.vue15
-rw-r--r--app/assets/javascripts/boards/components/board_content.vue9
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue26
-rw-r--r--app/assets/javascripts/boards/components/config_toggle.vue7
-rw-r--r--app/assets/javascripts/boards/components/project_select.vue10
-rw-r--r--app/assets/javascripts/boards/constants.js6
-rw-r--r--app/assets/javascripts/boards/index.js25
-rw-r--r--app/assets/javascripts/boards/stores/actions.js925
-rw-r--r--app/assets/javascripts/boards/stores/getters.js67
-rw-r--r--app/assets/javascripts/boards/stores/index.js20
-rw-r--r--app/assets/javascripts/boards/stores/mutation_types.js47
-rw-r--r--app/assets/javascripts/boards/stores/mutations.js319
-rw-r--r--app/assets/javascripts/boards/stores/state.js44
-rw-r--r--app/assets/javascripts/branches/components/graph_bar.vue2
-rw-r--r--app/assets/javascripts/breadcrumb.js2
-rw-r--r--app/assets/javascripts/captcha/captcha_modal.vue44
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_components.vue4
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_header.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/details/ci_resource_readme.vue2
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_header.vue3
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_search.vue26
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/catalog_tabs.vue67
-rw-r--r--app/assets/javascripts/ci/catalog/components/list/ci_resources_list_item.vue6
-rw-r--r--app/assets/javascripts/ci/catalog/components/pages/ci_resources_page.vue75
-rw-r--r--app/assets/javascripts/ci/catalog/constants.js6
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql4
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_create.mutation.graphql (renamed from app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_create.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_destroy.mutation.graphql (renamed from app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql)0
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql2
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql2
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources.query.graphql3
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources_count.query.graphql8
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_settings.query.graphql (renamed from app/assets/javascripts/pages/projects/shared/permissions/graphql/queries/get_ci_catalog_settings.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/catalog/graphql/settings.js2
-rw-r--r--app/assets/javascripts/ci/ci_environments_dropdown/ci_environments_dropdown.vue (renamed from app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue)37
-rw-r--r--app/assets/javascripts/ci/ci_environments_dropdown/constants.js14
-rw-r--r--app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/group_environments.query.graphql (renamed from app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/project_environments.query.graphql (renamed from app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql)0
-rw-r--r--app/assets/javascripts/ci/ci_environments_dropdown/utils.js (renamed from app/assets/javascripts/ci/ci_variable_list/utils.js)13
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_drawer.vue38
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue6
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue2
-rw-r--r--app/assets/javascripts/ci/ci_variable_list/constants.js9
-rw-r--r--app/assets/javascripts/ci/common/private/ci_environments_dropdown.js9
-rw-r--r--app/assets/javascripts/ci/pipeline_details/constants.js2
-rw-r--r--app/assets/javascripts/ci/pipeline_details/stores/test_reports/utils.js12
-rw-r--r--app/assets/javascripts/ci/pipeline_details/test_reports/test_reports.vue23
-rw-r--r--app/assets/javascripts/ci/pipeline_editor/components/validate/ci_validate.vue4
-rw-r--r--app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue9
-rw-r--r--app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue6
-rw-r--r--app/assets/javascripts/clusters/agents/components/show.vue2
-rw-r--r--app/assets/javascripts/comment_templates/components/form.vue46
-rw-r--r--app/assets/javascripts/commit/components/signature_badge.vue2
-rw-r--r--app/assets/javascripts/commit/constants.js17
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/image.vue13
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/playable.vue62
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/table_cell_base.vue24
-rw-r--r--app/assets/javascripts/content_editor/extensions/copy_paste.js2
-rw-r--r--app/assets/javascripts/content_editor/extensions/playable.js7
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_cell.js11
-rw-r--r--app/assets/javascripts/content_editor/extensions/table_header.js36
-rw-r--r--app/assets/javascripts/content_editor/extensions/task_item.js32
-rw-r--r--app/assets/javascripts/content_editor/extensions/video.js1
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_serializer.js6
-rw-r--r--app/assets/javascripts/content_editor/services/markdown_sourcemap.js30
-rw-r--r--app/assets/javascripts/content_editor/services/serialization_helpers.js10
-rw-r--r--app/assets/javascripts/content_editor/services/upload_helpers.js6
-rw-r--r--app/assets/javascripts/deploy_keys/components/action_btn.vue23
-rw-r--r--app/assets/javascripts/deploy_keys/components/app.vue280
-rw-r--r--app/assets/javascripts/deploy_keys/components/key.vue110
-rw-r--r--app/assets/javascripts/deploy_keys/components/keys_panel.vue10
-rw-r--r--app/assets/javascripts/deploy_keys/graphql/resolvers.js35
-rw-r--r--app/assets/javascripts/deploy_keys/index.js32
-rw-r--r--app/assets/javascripts/deploy_keys/service/index.js19
-rw-r--r--app/assets/javascripts/deploy_keys/store/index.js9
-rw-r--r--app/assets/javascripts/diffs/components/app.vue30
-rw-r--r--app/assets/javascripts/diffs/components/commit_item.vue4
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue15
-rw-r--r--app/assets/javascripts/diffs/components/diff_file_header.vue34
-rw-r--r--app/assets/javascripts/diffs/components/diff_row.vue6
-rw-r--r--app/assets/javascripts/diffs/components/diff_row_utils.js18
-rw-r--r--app/assets/javascripts/diffs/components/tree_list.vue54
-rw-r--r--app/assets/javascripts/diffs/index.js1
-rw-r--r--app/assets/javascripts/diffs/store/actions.js103
-rw-r--r--app/assets/javascripts/diffs/store/getters.js15
-rw-r--r--app/assets/javascripts/diffs/store/modules/diff_state.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js2
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js21
-rw-r--r--app/assets/javascripts/diffs/store/utils.js28
-rw-r--r--app/assets/javascripts/dropzone_input.js3
-rw-r--r--app/assets/javascripts/editor/schema/ci.json63
-rw-r--r--app/assets/javascripts/entrypoints/analytics.js4
-rw-r--r--app/assets/javascripts/entrypoints/sandboxed_swagger.js1
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_status_bar.vue17
-rw-r--r--app/assets/javascripts/environments/components/kubernetes_tabs.vue54
-rw-r--r--app/assets/javascripts/environments/components/new_environment_item.vue1
-rw-r--r--app/assets/javascripts/environments/components/stop_environment_modal.vue8
-rw-r--r--app/assets/javascripts/environments/constants.js3
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js13
-rw-r--r--app/assets/javascripts/environments/graphql/client.js4
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql1
-rw-r--r--app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql1
-rw-r--r--app/assets/javascripts/environments/helpers/k8s_integration_helper.js63
-rw-r--r--app/assets/javascripts/error_tracking/components/error_details_info.vue2
-rw-r--r--app/assets/javascripts/error_tracking/components/error_tracking_list.vue11
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js29
-rw-r--r--app/assets/javascripts/graphql_shared/issuable_client.js184
-rw-r--r--app/assets/javascripts/graphql_shared/possible_types.json2
-rw-r--r--app/assets/javascripts/graphql_shared/utils.js8
-rw-r--r--app/assets/javascripts/groups/components/app.vue2
-rw-r--r--app/assets/javascripts/groups/components/group_item.vue4
-rw-r--r--app/assets/javascripts/groups/components/group_name_and_path.vue18
-rw-r--r--app/assets/javascripts/groups/components/overview_tabs.vue27
-rw-r--r--app/assets/javascripts/groups/init_overview_tabs.js2
-rw-r--r--app/assets/javascripts/groups/settings/api/access_dropdown_api.js7
-rw-r--r--app/assets/javascripts/groups/settings/components/access_dropdown.vue6
-rw-r--r--app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue17
-rw-r--r--app/assets/javascripts/ide/components/file_alert.vue26
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue33
-rw-r--r--app/assets/javascripts/ide/index.js1
-rw-r--r--app/assets/javascripts/ide/lib/alerts/environments.vue33
-rw-r--r--app/assets/javascripts/ide/lib/alerts/index.js21
-rw-r--r--app/assets/javascripts/ide/services/index.js16
-rw-r--r--app/assets/javascripts/ide/stores/actions.js1
-rw-r--r--app/assets/javascripts/ide/stores/actions/alert.js18
-rw-r--r--app/assets/javascripts/ide/stores/getters.js2
-rw-r--r--app/assets/javascripts/ide/stores/getters/alert.js3
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js5
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js2
-rw-r--r--app/assets/javascripts/ide/stores/mutations/alert.js21
-rw-r--r--app/assets/javascripts/ide/stores/state.js2
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_status.vue7
-rw-r--r--app/assets/javascripts/import_entities/import_groups/components/import_table.vue2
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_modal.vue17
-rw-r--r--app/assets/javascripts/invite_members/components/invite_members_trigger.vue1
-rw-r--r--app/assets/javascripts/invite_members/components/invite_modal_base.vue1
-rw-r--r--app/assets/javascripts/invite_members/utils/member_utils.js4
-rw-r--r--app/assets/javascripts/issues/list/components/issues_list_app.vue3
-rw-r--r--app/assets/javascripts/issues/list/index.js1
-rw-r--r--app/assets/javascripts/issues/show/components/header_actions.vue12
-rw-r--r--app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue76
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue7
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue19
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue9
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/constants.js44
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/client.js92
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js56
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql18
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql18
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql17
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js130
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js30
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue84
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue80
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue69
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/constants.js6
-rw-r--r--app/assets/javascripts/kubernetes_dashboard/router/routes.js34
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js2
-rw-r--r--app/assets/javascripts/lib/utils/constants.js5
-rw-r--r--app/assets/javascripts/lib/utils/headers.js2
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js45
-rw-r--r--app/assets/javascripts/lib/utils/secret_detection.js8
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js2
-rw-r--r--app/assets/javascripts/logo.js2
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_app.vue6
-rw-r--r--app/assets/javascripts/merge_requests/components/compare_dropdown.vue6
-rw-r--r--app/assets/javascripts/merge_requests/components/sticky_header.vue3
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index.js3
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/index_ml_models.vue34
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/new_ml_model.vue127
-rw-r--r--app/assets/javascripts/ml/model_registry/apps/show_ml_model.vue15
-rw-r--r--app/assets/javascripts/ml/model_registry/components/actions_dropdown.vue34
-rw-r--r--app/assets/javascripts/ml/model_registry/components/candidate_list.vue79
-rw-r--r--app/assets/javascripts/ml/model_registry/components/model_version_list.vue76
-rw-r--r--app/assets/javascripts/ml/model_registry/components/searchable_list.vue79
-rw-r--r--app/assets/javascripts/ml/model_registry/graphql/mutations/create_model.mutation.graphql11
-rw-r--r--app/assets/javascripts/ml/model_registry/translations.js9
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue2
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue6
-rw-r--r--app/assets/javascripts/observability/client.js21
-rw-r--r--app/assets/javascripts/organizations/groups_and_projects/components/app.vue2
-rw-r--r--app/assets/javascripts/organizations/mock_data.js6
-rw-r--r--app/assets/javascripts/organizations/new/components/app.vue10
-rw-r--r--app/assets/javascripts/organizations/new/index.js5
-rw-r--r--app/assets/javascripts/organizations/settings/general/components/organization_settings.vue29
-rw-r--r--app/assets/javascripts/organizations/settings/general/index.js10
-rw-r--r--app/assets/javascripts/organizations/shared/components/groups_view.vue12
-rw-r--r--app/assets/javascripts/organizations/shared/components/new_edit_form.vue82
-rw-r--r--app/assets/javascripts/organizations/shared/components/organization_url_field.vue4
-rw-r--r--app/assets/javascripts/organizations/shared/components/projects_view.vue12
-rw-r--r--app/assets/javascripts/organizations/shared/constants.js2
-rw-r--r--app/assets/javascripts/organizations/show/components/app.vue4
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_avatar.vue1
-rw-r--r--app/assets/javascripts/organizations/show/components/organization_description.vue24
-rw-r--r--app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue7
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue26
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue115
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/components/registry_settings_app.vue10
-rw-r--r--app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql13
-rw-r--r--app/assets/javascripts/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue21
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js13
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/page.js11
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/index/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/ml/models/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js11
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/components/ci_catalog_settings.vue6
-rw-r--r--app/assets/javascripts/pages/projects/shared/permissions/index.js3
-rw-r--r--app/assets/javascripts/pages/sessions/new/index.js2
-rw-r--r--app/assets/javascripts/pages/sessions/new/oauth_remember_me.js4
-rw-r--r--app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js4
-rw-r--r--app/assets/javascripts/pages/shared/mount_badge_settings.js2
-rw-r--r--app/assets/javascripts/pages/shared/mount_runner_instructions.js27
-rw-r--r--app/assets/javascripts/pages/users/index.js2
-rw-r--r--app/assets/javascripts/pages/users/user_tabs.js2
-rw-r--r--app/assets/javascripts/performance_bar/components/request_warning.vue1
-rw-r--r--app/assets/javascripts/persistent_user_callouts.js2
-rw-r--r--app/assets/javascripts/profile/preferences/components/profile_preferences.vue7
-rw-r--r--app/assets/javascripts/profile/preferences/profile_preferences_bundle.js3
-rw-r--r--app/assets/javascripts/projects/commit/components/commit_comments_button.vue42
-rw-r--r--app/assets/javascripts/projects/commit/index.js2
-rw-r--r--app/assets/javascripts/projects/commit/init_commit_comments_button.js18
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue2
-rw-r--r--app/assets/javascripts/projects/commit_box/info/components/refs_list.vue8
-rw-r--r--app/assets/javascripts/projects/new/components/new_project_url_select.vue162
-rw-r--r--app/assets/javascripts/projects/project_new.js3
-rw-r--r--app/assets/javascripts/projects/settings/api/access_dropdown_api.js3
-rw-r--r--app/assets/javascripts/projects/settings/components/access_dropdown.vue9
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/app.vue125
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/constants.js10
-rw-r--r--app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql8
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_create.js1
-rw-r--r--app/assets/javascripts/protected_tags/protected_tag_edit.vue1
-rw-r--r--app/assets/javascripts/releases/components/app_edit_new.vue3
-rw-r--r--app/assets/javascripts/releases/components/app_index.vue56
-rw-r--r--app/assets/javascripts/releases/components/tag_field_new.vue3
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/actions.js15
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/getters.js12
-rw-r--r--app/assets/javascripts/releases/stores/modules/edit_new/state.js1
-rw-r--r--app/assets/javascripts/repository/components/upload_blob_modal.vue4
-rw-r--r--app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue8
-rw-r--r--app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue1
-rw-r--r--app/assets/javascripts/search/store/actions.js51
-rw-r--r--app/assets/javascripts/security_configuration/constants.js208
-rw-r--r--app/assets/javascripts/security_configuration/index.js6
-rw-r--r--app/assets/javascripts/security_configuration/utils.js36
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue10
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue50
-rw-r--r--app/assets/javascripts/sidebar/components/labels/labels_select_widget/labels_select_root.vue16
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_color_picker.vue75
-rw-r--r--app/assets/javascripts/sidebar/components/sidebar_editable_item.vue33
-rw-r--r--app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue6
-rw-r--r--app/assets/javascripts/sidebar/constants.js6
-rw-r--r--app/assets/javascripts/sidebar/queries/constants.js6
-rw-r--r--app/assets/javascripts/super_sidebar/components/pinned_section.vue6
-rw-r--r--app/assets/javascripts/super_sidebar/components/user_menu.vue54
-rw-r--r--app/assets/javascripts/tracking/constants.js2
-rw-r--r--app/assets/javascripts/tracking/internal_events.js8
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue67
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue51
-rw-r--r--app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue33
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue11
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/widget/widget_content_row.vue9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js1
-rw-r--r--app/assets/javascripts/vue_shared/components/file_row.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/daterange_token.vue158
-rw-r--r--app/assets/javascripts/vue_shared/components/gl_countdown.vue5
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue107
-rw-r--r--app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.stories.js35
-rw-r--r--app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.vue44
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/system_note.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/queries/blame_data.query.graphql10
-rw-r--r--app/assets/javascripts/vue_shared/components/source_viewer/source_viewer_new.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue1
-rw-r--r--app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue112
-rw-r--r--app/assets/javascripts/vue_shared/components/user_select/user_select.vue6
-rw-r--r--app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/web_ide_link.vue16
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue29
-rw-r--r--app/assets/javascripts/vue_shared/issuable/list/components/issuable_list_root.vue4
-rw-r--r--app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue2
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_link_child_contents.vue2
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue194
-rw-r--r--app/assets/javascripts/work_items/components/shared/work_item_token_input.vue108
-rw-r--r--app/assets/javascripts/work_items/components/widget_wrapper.vue21
-rw-r--r--app/assets/javascripts/work_items/components/work_item_attributes_wrapper.vue77
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description.vue58
-rw-r--r--app/assets/javascripts/work_items/components/work_item_description_rendered.vue26
-rw-r--r--app/assets/javascripts/work_items/components/work_item_detail.vue100
-rw-r--r--app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue5
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone_inline.vue (renamed from app/assets/javascripts/work_items/components/work_item_milestone.vue)0
-rw-r--r--app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue203
-rw-r--r--app/assets/javascripts/work_items/components/work_item_parent_inline.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue2
-rw-r--r--app/assets/javascripts/work_items/components/work_item_state_toggle.vue17
-rw-r--r--app/assets/javascripts/work_items/components/work_item_title_with_edit.vue45
-rw-r--r--app/assets/javascripts/work_items/constants.js15
-rw-r--r--app/assets/javascripts/work_items/graphql/project_work_items.query.graphql7
-rw-r--r--app/assets/javascripts/work_items/graphql/work_item.fragment.graphql1
-rw-r--r--app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql10
-rw-r--r--app/assets/javascripts/work_items/utils.js11
-rw-r--r--app/assets/stylesheets/application.scss10
-rw-r--r--app/assets/stylesheets/components/_index.scss11
-rw-r--r--app/assets/stylesheets/components/content_editor.scss9
-rw-r--r--app/assets/stylesheets/emoji_sprites.scss4
-rw-r--r--app/assets/stylesheets/fonts.scss8
-rw-r--r--app/assets/stylesheets/framework.scss3
-rw-r--r--app/assets/stylesheets/framework/badges.scss53
-rw-r--r--app/assets/stylesheets/framework/breadcrumbs.scss13
-rw-r--r--app/assets/stylesheets/framework/diffs.scss27
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss38
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/flash.scss8
-rw-r--r--app/assets/stylesheets/framework/header.scss124
-rw-r--r--app/assets/stylesheets/framework/labels.scss56
-rw-r--r--app/assets/stylesheets/framework/layout.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/framework/source_editor.scss2
-rw-r--r--app/assets/stylesheets/framework/super_sidebar.scss177
-rw-r--r--app/assets/stylesheets/framework/top_bar.scss20
-rw-r--r--app/assets/stylesheets/framework/typography.scss16
-rw-r--r--app/assets/stylesheets/highlight/common.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/issuable.scss5
-rw-r--r--app/assets/stylesheets/page_bundles/issuable_list.scss19
-rw-r--r--app/assets/stylesheets/page_bundles/labels.scss49
-rw-r--r--app/assets/stylesheets/page_bundles/login.scss125
-rw-r--r--app/assets/stylesheets/page_bundles/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/pipeline.scss15
-rw-r--r--app/assets/stylesheets/page_bundles/profile.scss6
-rw-r--r--app/assets/stylesheets/page_bundles/project.scss4
-rw-r--r--app/assets/stylesheets/page_bundles/search.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/wiki.scss8
-rw-r--r--app/assets/stylesheets/page_bundles/work_items.scss9
-rw-r--r--app/assets/stylesheets/pages/colors.scss49
-rw-r--r--app/assets/stylesheets/pages/issues.scss2
-rw-r--r--app/assets/stylesheets/pages/notes.scss21
-rw-r--r--app/assets/stylesheets/print.scss162
-rw-r--r--app/assets/stylesheets/snippets.scss3
-rw-r--r--app/assets/stylesheets/themes/dark_mode_overrides.scss1
-rw-r--r--app/assets/stylesheets/themes/theme_blue.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_gray.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_green.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_helper.scss36
-rw-r--r--app/assets/stylesheets/themes/theme_indigo.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_blue.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_gray.scss2
-rw-r--r--app/assets/stylesheets/themes/theme_light_green.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_indigo.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_light_red.scss14
-rw-r--r--app/assets/stylesheets/themes/theme_red.scss14
-rw-r--r--app/assets/stylesheets/utilities.scss10
-rw-r--r--app/assets/stylesheets/vendors/_index.scss1
386 files changed, 5841 insertions, 4149 deletions
diff --git a/app/assets/images/mailers/in_product_marketing/admin_verify-0.png b/app/assets/images/mailers/in_product_marketing/admin_verify-0.png
deleted file mode 100644
index c6d3e55afc1..00000000000
--- a/app/assets/images/mailers/in_product_marketing/admin_verify-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-0.png b/app/assets/images/mailers/in_product_marketing/create-0.png
deleted file mode 100644
index 7fc992f14f2..00000000000
--- a/app/assets/images/mailers/in_product_marketing/create-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-1.png b/app/assets/images/mailers/in_product_marketing/create-1.png
deleted file mode 100644
index 0315ffefb31..00000000000
--- a/app/assets/images/mailers/in_product_marketing/create-1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/create-2.png b/app/assets/images/mailers/in_product_marketing/create-2.png
deleted file mode 100644
index 619f9fcd659..00000000000
--- a/app/assets/images/mailers/in_product_marketing/create-2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/experience-0.png b/app/assets/images/mailers/in_product_marketing/experience-0.png
deleted file mode 100644
index 346204d1db1..00000000000
--- a/app/assets/images/mailers/in_product_marketing/experience-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-0.png b/app/assets/images/mailers/in_product_marketing/team-0.png
deleted file mode 100644
index f10ae998efa..00000000000
--- a/app/assets/images/mailers/in_product_marketing/team-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-1.png b/app/assets/images/mailers/in_product_marketing/team-1.png
deleted file mode 100644
index cd68464e6e8..00000000000
--- a/app/assets/images/mailers/in_product_marketing/team-1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/team-2.png b/app/assets/images/mailers/in_product_marketing/team.png
index b199c659943..b199c659943 100644
--- a/app/assets/images/mailers/in_product_marketing/team-2.png
+++ b/app/assets/images/mailers/in_product_marketing/team.png
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-0.png b/app/assets/images/mailers/in_product_marketing/trial-0.png
deleted file mode 100644
index 3b0d7a8ecd8..00000000000
--- a/app/assets/images/mailers/in_product_marketing/trial-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-1.png b/app/assets/images/mailers/in_product_marketing/trial-1.png
deleted file mode 100644
index 3a30b2acaee..00000000000
--- a/app/assets/images/mailers/in_product_marketing/trial-1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/trial-2.png b/app/assets/images/mailers/in_product_marketing/trial-2.png
deleted file mode 100644
index 95bd965b49f..00000000000
--- a/app/assets/images/mailers/in_product_marketing/trial-2.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-0.png b/app/assets/images/mailers/in_product_marketing/verify-0.png
deleted file mode 100644
index 04b6f172b37..00000000000
--- a/app/assets/images/mailers/in_product_marketing/verify-0.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-1.png b/app/assets/images/mailers/in_product_marketing/verify-1.png
deleted file mode 100644
index 8997e8ba575..00000000000
--- a/app/assets/images/mailers/in_product_marketing/verify-1.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/mailers/in_product_marketing/verify-2.png b/app/assets/images/mailers/in_product_marketing/verify.png
index 93c99dee246..93c99dee246 100644
--- a/app/assets/images/mailers/in_product_marketing/verify-2.png
+++ b/app/assets/images/mailers/in_product_marketing/verify.png
Binary files differ
diff --git a/app/assets/javascripts/admin/abuse_report/components/user_details.vue b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
index 0c32341652b..0e946fed8ac 100644
--- a/app/assets/javascripts/admin/abuse_report/components/user_details.vue
+++ b/app/assets/javascripts/admin/abuse_report/components/user_details.vue
@@ -26,12 +26,18 @@ export default {
.map(([k]) => this.$options.i18n.verificationMethods[k])
.join(', ');
},
- showSimilarRecords() {
+ showCreditCardSimilarRecords() {
return this.user.creditCard.similarRecordsCount > 1;
},
- similarRecordsCount() {
+ creditCardSimilarRecordsCount() {
return formatNumber(this.user.creditCard.similarRecordsCount);
},
+ showPhoneNumberSimilarRecords() {
+ return this.user.phoneNumber.similarRecordsCount > 1;
+ },
+ phoneNumberSimilarRecordsCount() {
+ return formatNumber(this.user.phoneNumber.similarRecordsCount);
+ },
},
i18n: USER_DETAILS_I18N,
};
@@ -60,11 +66,33 @@ export default {
data-testid="credit-card-verification"
:label="$options.i18n.creditCard"
>
- <gl-sprintf v-if="showSimilarRecords" :message="$options.i18n.similarRecords">
+ <gl-sprintf
+ v-if="showCreditCardSimilarRecords"
+ :message="$options.i18n.creditCardSimilarRecords"
+ >
<template #cardMatchesLink="{ content }">
<gl-link :href="user.creditCard.cardMatchesLink">
<gl-sprintf :message="content">
- <template #count>{{ similarRecordsCount }}</template>
+ <template #count>{{ creditCardSimilarRecordsCount }}</template>
+ </gl-sprintf>
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </user-detail>
+
+ <user-detail
+ v-if="user.phoneNumber"
+ data-testid="phone-number-verification"
+ :label="$options.i18n.phoneNumber"
+ >
+ <gl-sprintf
+ v-if="showPhoneNumberSimilarRecords"
+ :message="$options.i18n.phoneNumberSimilarRecords"
+ >
+ <template #phoneMatchesLink="{ content }">
+ <gl-link :href="user.phoneNumber.phoneMatchesLink">
+ <gl-sprintf :message="content">
+ <template #count>{{ phoneNumberSimilarRecordsCount }}</template>
</gl-sprintf>
</gl-link>
</template>
diff --git a/app/assets/javascripts/admin/abuse_report/constants.js b/app/assets/javascripts/admin/abuse_report/constants.js
index c56ea678b1d..69bcdebad61 100644
--- a/app/assets/javascripts/admin/abuse_report/constants.js
+++ b/app/assets/javascripts/admin/abuse_report/constants.js
@@ -61,6 +61,7 @@ export const USER_DETAILS_I18N = {
plan: s__('AbuseReport|Tier'),
verification: s__('AbuseReport|Verification'),
creditCard: s__('AbuseReport|Credit card'),
+ phoneNumber: s__('AbuseReport|Phone number'),
pastReports: s__('AbuseReport|Past abuse reports'),
normalLocation: s__('AbuseReport|Normal location'),
lastSignInIp: s__('AbuseReport|Last login'),
@@ -78,9 +79,12 @@ export const USER_DETAILS_I18N = {
reportedFor: s__(
'AbuseReport|%{reportLinkStart}Reported%{reportLinkEnd} for %{category} %{timeAgo}.',
),
- similarRecords: s__(
+ creditCardSimilarRecords: s__(
'AbuseReport|Card matches %{cardMatchesLinkStart}%{count} accounts%{cardMatchesLinkEnd}',
),
+ phoneNumberSimilarRecords: s__(
+ 'AbuseReport|Phone matches %{phoneMatchesLinkStart}%{count} accounts%{phoneMatchesLinkEnd}',
+ ),
};
export const REPORTED_CONTENT_I18N = {
diff --git a/app/assets/javascripts/api/alert_management_alerts_api.js b/app/assets/javascripts/api/alert_management_alerts_api.js
index fa66ca5b3dd..6f595cb7cb0 100644
--- a/app/assets/javascripts/api/alert_management_alerts_api.js
+++ b/app/assets/javascripts/api/alert_management_alerts_api.js
@@ -1,6 +1,6 @@
import axios from '~/lib/utils/axios_utils';
import { buildApiUrl } from '~/api/api_utils';
-import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
+import { contentTypeMultipartFormData } from '~/lib/utils/headers';
const ALERT_METRIC_IMAGES_PATH =
'/api/:version/projects/:id/alert_management_alerts/:alert_iid/metric_images';
@@ -16,7 +16,7 @@ export function fetchAlertMetricImages({ alertIid, id }) {
}
export function uploadAlertMetricImage({ alertIid, id, file, url = null, urlText = null }) {
- const options = { headers: { ...ContentTypeMultipartFormData } };
+ const options = { headers: { ...contentTypeMultipartFormData } };
const metricImagesUrl = buildApiUrl(ALERT_METRIC_IMAGES_PATH)
.replace(':id', encodeURIComponent(id))
.replace(':alert_iid', encodeURIComponent(alertIid));
diff --git a/app/assets/javascripts/badges/components/badge_list.vue b/app/assets/javascripts/badges/components/badge_list.vue
index a4f88067fa9..3f6435d66ce 100644
--- a/app/assets/javascripts/badges/components/badge_list.vue
+++ b/app/assets/javascripts/badges/components/badge_list.vue
@@ -3,6 +3,7 @@ import {
GlBadge,
GlLoadingIcon,
GlTable,
+ GlTooltipDirective,
GlPagination,
GlButton,
GlModalDirective,
@@ -27,6 +28,7 @@ export default {
},
directives: {
GlModal: GlModalDirective,
+ GlTooltip: GlTooltipDirective,
},
i18n: {
emptyGroupMessage: s__('Badges|This group has no badges. Add an existing badge or create one.'),
@@ -107,19 +109,26 @@ export default {
:current-page="currentPage"
stacked="md"
show-empty
+ class="b-table-fixed"
data-testid="badge-list"
>
<template #cell(name)="{ item }">
- <label class="label-bold str-truncated mb-0">{{ item.name }}</label>
+ <label v-gl-tooltip class="label-bold str-truncated mb-0" :title="item.name">{{
+ item.name
+ }}</label>
<gl-badge size="sm">{{ badgeKindText(item) }}</gl-badge>
</template>
<template #cell(badge)="{ item }">
- <badge :image-url="item.renderedImageUrl" :link-url="item.renderedLinkUrl" />
+ <div class="overflow-hidden">
+ <badge :image-url="item.renderedImageUrl" :link-url="item.renderedLinkUrl" />
+ </div>
</template>
<template #cell(url)="{ item }">
- {{ item.linkUrl }}
+ <span v-gl-tooltip :title="item.linkUrl" class="str-truncated">
+ {{ item.linkUrl }}
+ </span>
</template>
<template #cell(actions)="{ item }">
diff --git a/app/assets/javascripts/behaviors/secret_values.js b/app/assets/javascripts/behaviors/secret_values.js
deleted file mode 100644
index b6ed14611cd..00000000000
--- a/app/assets/javascripts/behaviors/secret_values.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { parseBoolean } from '~/lib/utils/common_utils';
-import { n__ } from '~/locale';
-
-export default class SecretValues {
- constructor({
- container,
- valueSelector = '.js-secret-value',
- placeholderSelector = '.js-secret-value-placeholder',
- }) {
- this.container = container;
- this.valueSelector = valueSelector;
- this.placeholderSelector = placeholderSelector;
- }
-
- init() {
- this.revealButton = this.container.querySelector('.js-secret-value-reveal-button');
-
- if (this.revealButton) {
- const isRevealed = parseBoolean(this.revealButton.dataset.secretRevealStatus);
- this.updateDom(isRevealed);
-
- this.revealButton.addEventListener('click', this.onRevealButtonClicked.bind(this));
- }
- }
-
- onRevealButtonClicked() {
- const previousIsRevealed = parseBoolean(this.revealButton.dataset.secretRevealStatus);
- this.updateDom(!previousIsRevealed);
- }
-
- updateDom(isRevealed) {
- const values = this.container.querySelectorAll(this.valueSelector);
- values.forEach((value) => {
- value.classList.toggle('hide', !isRevealed);
- });
-
- const placeholders = this.container.querySelectorAll(this.placeholderSelector);
- placeholders.forEach((placeholder) => {
- placeholder.classList.toggle('hide', isRevealed);
- });
-
- this.revealButton.textContent = isRevealed
- ? n__('Hide value', 'Hide values', values.length)
- : n__('Reveal value', 'Reveal values', values.length);
- this.revealButton.dataset.secretRevealStatus = isRevealed;
- }
-}
diff --git a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
index e81ceae57c0..f8b2331befa 100644
--- a/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
+++ b/app/assets/javascripts/behaviors/shortcuts/shortcuts_help.vue
@@ -79,9 +79,7 @@ export default {
"
>
<template #link="{ content }">
- <gl-link :href="absoluteUserPreferencesPath">
- {{ content }}
- </gl-link>
+ <gl-link :href="absoluteUserPreferencesPath">{{ content }}</gl-link>
</template>
</gl-sprintf>
</span>
diff --git a/app/assets/javascripts/blob/openapi/index.js b/app/assets/javascripts/blob/openapi/index.js
index 9c22d960bf5..e2715d89b4e 100644
--- a/app/assets/javascripts/blob/openapi/index.js
+++ b/app/assets/javascripts/blob/openapi/index.js
@@ -1,5 +1,5 @@
+import SwaggerClient from 'swagger-client';
import { setAttributes } from '~/lib/utils/dom_utils';
-import axios from '~/lib/utils/axios_utils';
import {
getBaseURL,
relativePathToAbsolute,
@@ -42,11 +42,11 @@ export default async (el = document.getElementById('js-openapi-viewer')) => {
const wrapperEl = el;
const sandboxEl = createSandbox();
- const { data } = await axios.get(wrapperEl.dataset.endpoint);
+ const { spec } = await SwaggerClient.resolve({ url: wrapperEl.dataset.endpoint });
wrapperEl.appendChild(sandboxEl);
sandboxEl.addEventListener('load', () => {
- sandboxEl.contentWindow.postMessage(data, '*');
+ if (spec) sandboxEl.contentWindow.postMessage(JSON.stringify(spec), '*');
});
};
diff --git a/app/assets/javascripts/boards/components/board_app.vue b/app/assets/javascripts/boards/components/board_app.vue
index 2c8aa1cbe21..b1b3e7f7022 100644
--- a/app/assets/javascripts/boards/components/board_app.vue
+++ b/app/assets/javascripts/boards/components/board_app.vue
@@ -155,7 +155,7 @@ export default {
:is-swimlanes-on="isSwimlanesOn"
:filter-params="formattedFilterParams"
:board-lists="boardLists"
- :apollo-error="error"
+ :error="error"
:list-query-variables="listQueryVariables"
@setActiveList="setActiveId"
@setAddColumnFormVisibility="addColumnFormVisible = $event"
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 9173503c888..398dcc494f7 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,6 +1,7 @@
<script>
-import { GlDisclosureDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlTooltipDirective } from '@gitlab/ui';
import Tracking from '~/tracking';
+import { s__ } from '~/locale';
import {
BOARD_CARD_MOVE_TO_POSITIONS_OPTIONS,
BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION,
@@ -12,6 +13,9 @@ export default {
components: {
GlDisclosureDropdown,
},
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
mixins: [Tracking.mixin()],
props: {
item: {
@@ -87,6 +91,9 @@ export default {
}
},
},
+ i18n: {
+ moveCardText: s__('Boards|Card options'),
+ },
};
</script>
@@ -94,12 +101,16 @@ export default {
<gl-disclosure-dropdown
ref="dropdown"
:key="itemIdentifier"
+ v-gl-tooltip.hover.focus.top="{
+ title: $options.i18n.moveCardText,
+ boundary: 'viewport',
+ }"
class="move-to-position gl-display-block gl-mb-2 gl-ml-auto gl-mt-n3 gl-mr-n3 js-no-trigger"
category="tertiary"
:items="$options.BOARD_CARD_MOVE_TO_POSITIONS_OPTIONS"
icon="ellipsis_v"
:tabindex="index"
- :toggle-text="s__('Boards|Move card')"
+ :aria-label="$options.i18n.moveCardText"
:text-sr-only="true"
no-caret
data-testid="board-move-to-position"
diff --git a/app/assets/javascripts/boards/components/board_content.vue b/app/assets/javascripts/boards/components/board_content.vue
index 2b9c5d52d5e..dbdfe314ae0 100644
--- a/app/assets/javascripts/boards/components/board_content.vue
+++ b/app/assets/javascripts/boards/components/board_content.vue
@@ -46,7 +46,7 @@ export default {
required: false,
default: () => {},
},
- apolloError: {
+ error: {
type: String,
required: false,
default: null,
@@ -97,9 +97,6 @@ export default {
return this.canDragColumns ? options : {};
},
- errorToDisplay() {
- return this.apolloError || null;
- },
},
methods: {
afterFormEnters() {
@@ -195,8 +192,8 @@ export default {
data-testid="boards-list"
class="gl-flex-grow-1 gl-display-flex gl-flex-direction-column gl-min-h-0"
>
- <gl-alert v-if="errorToDisplay" variant="danger" :dismissible="true" @dismiss="dismissError">
- {{ errorToDisplay }}
+ <gl-alert v-if="error" variant="danger" :dismissible="true" @dismiss="dismissError">
+ {{ error }}
</gl-alert>
<component
:is="boardColumnWrapper"
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 8a5c6882e56..58c20c0da91 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -3,6 +3,7 @@ import { GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import Draggable from 'vuedraggable';
import { STATUS_CLOSED } from '~/issues/constants';
import { sprintf, __, s__ } from '~/locale';
+import { ESC_KEY_CODE } from '~/lib/utils/keycodes';
import { defaultSortableOptions, DRAG_DELAY } from '~/sortable/constants';
import { sortableStart, sortableEnd } from '~/sortable/utils';
import Tracking from '~/tracking';
@@ -82,6 +83,7 @@ export default {
toList: {},
addItemToListInProgress: false,
updateIssueOrderInProgress: false,
+ dragCancelled: false,
};
},
apollo: {
@@ -307,6 +309,11 @@ export default {
return;
}
+ // Reset dragCancelled flag
+ this.dragCancelled = false;
+ // Attach listener to detect `ESC` key press to cancel drag.
+ document.addEventListener('keyup', this.handleKeyUp.bind(this));
+
sortableStart();
this.track('drag_card', { label: 'board' });
},
@@ -323,6 +330,11 @@ export default {
return;
}
+ // Detach listener as soon as drag ends.
+ document.removeEventListener('keyup', this.handleKeyUp.bind(this));
+ // Drag was cancelled, prevent reordering.
+ if (this.dragCancelled) return;
+
sortableEnd();
let newIndex = originalNewIndex;
let { children } = to;
@@ -375,6 +387,20 @@ export default {
this.updateIssueOrderInProgress = false;
});
},
+ /**
+ * This implementation is needed to support `Esc` key press to cancel drag.
+ * It matches with what we already shipped in https://gitlab.com/gitlab-org/gitlab/-/merge_requests/119311
+ */
+ handleKeyUp(e) {
+ if (e.keyCode === ESC_KEY_CODE) {
+ this.dragCancelled = true;
+ // Sortable.js internally listens for `mouseup` event on document
+ // to register drop event, see https://github.com/SortableJS/Sortable/blob/master/src/Sortable.js#L625
+ // We need to manually trigger it to simulate cancel behaviour as VueDraggable doesn't
+ // natively support it, see https://github.com/SortableJS/Vue.Draggable/issues/968.
+ document.dispatchEvent(new Event('mouseup'));
+ }
+ },
isItemInTheList(itemIid) {
const items = this.toList?.[`${this.issuableType}s`]?.nodes || [];
return items.some((item) => item.iid === itemIid);
diff --git a/app/assets/javascripts/boards/components/config_toggle.vue b/app/assets/javascripts/boards/components/config_toggle.vue
index 69e6cc870d2..f4d4222e41a 100644
--- a/app/assets/javascripts/boards/components/config_toggle.vue
+++ b/app/assets/javascripts/boards/components/config_toggle.vue
@@ -1,7 +1,5 @@
<script>
import { GlButton, GlModalDirective, GlTooltipDirective } from '@gitlab/ui';
-// eslint-disable-next-line no-restricted-imports
-import { mapGetters } from 'vuex';
import { formType } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import { s__, __ } from '~/locale';
@@ -25,12 +23,11 @@ export default {
},
},
computed: {
- ...mapGetters(['hasScope']),
buttonText() {
return this.canAdminList ? s__('Boards|Edit board') : s__('Boards|View scope');
},
tooltipTitle() {
- return this.hasScope || this.boardHasScope ? __("This board's scope is reduced") : '';
+ return this.boardHasScope ? __("This board's scope is reduced") : '';
},
},
methods: {
@@ -48,7 +45,7 @@ export default {
v-gl-modal-directive="'board-config-modal'"
v-gl-tooltip
:title="tooltipTitle"
- :class="{ 'dot-highlight': hasScope || boardHasScope }"
+ :class="{ 'dot-highlight': boardHasScope }"
data-testid="boards-config-button"
@click.prevent="showPage"
>
diff --git a/app/assets/javascripts/boards/components/project_select.vue b/app/assets/javascripts/boards/components/project_select.vue
index 7bbc444701a..9c5dd633092 100644
--- a/app/assets/javascripts/boards/components/project_select.vue
+++ b/app/assets/javascripts/boards/components/project_select.vue
@@ -77,10 +77,12 @@ export default {
},
activeGroupProjects() {
return (
- this.projects?.nodes?.map((project) => ({
- value: project.id,
- text: project.nameWithNamespace,
- })) || []
+ this.projects?.nodes
+ ?.filter((p) => !p.archived)
+ .map((project) => ({
+ value: project.id,
+ text: project.nameWithNamespace,
+ })) || []
);
},
selectedProjectName() {
diff --git a/app/assets/javascripts/boards/constants.js b/app/assets/javascripts/boards/constants.js
index acf01a8c528..a3983f11c86 100644
--- a/app/assets/javascripts/boards/constants.js
+++ b/app/assets/javascripts/boards/constants.js
@@ -51,12 +51,6 @@ export const toggleFormEventPrefix = {
issue: 'toggle-issue-form-',
};
-export const active = 'active';
-
-export const inactiveId = 0;
-
-export const ISSUABLE = 'issuable';
-export const LIST = 'list';
export const INCIDENT = 'INCIDENT';
export const flashAnimationDuration = 2000;
diff --git a/app/assets/javascripts/boards/index.js b/app/assets/javascripts/boards/index.js
index 72b8aef31a4..0d8882cf57e 100644
--- a/app/assets/javascripts/boards/index.js
+++ b/app/assets/javascripts/boards/index.js
@@ -3,10 +3,9 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import BoardApp from '~/boards/components/board_app.vue';
import '~/boards/filters/due_date_filters';
-import store from '~/boards/stores';
import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import {
- NavigationType,
+ navigationType,
isLoggedIn,
parseBoolean,
convertObjectPropsToCamelCase,
@@ -24,7 +23,6 @@ const apolloProvider = new VueApollo({
function mountBoardApp(el) {
const { boardId, groupId, fullPath, rootPath } = el.dataset;
- const isApolloBoard = true;
const rawFilterParams = queryToObject(window.location.search, { gatherArrays: true });
@@ -34,31 +32,12 @@ function mountBoardApp(el) {
const boardType = el.dataset.parent;
- if (!isApolloBoard) {
- store.dispatch('fetchBoard', {
- fullPath,
- fullBoardId: fullBoardId(boardId),
- boardType,
- });
-
- store.dispatch('setInitialBoardData', {
- boardId,
- fullBoardId: fullBoardId(boardId),
- fullPath,
- boardType,
- disabled: parseBoolean(el.dataset.disabled) || true,
- issuableType: TYPE_ISSUE,
- });
- }
-
// eslint-disable-next-line no-new
new Vue({
el,
name: 'BoardAppRoot',
- store,
apolloProvider,
provide: {
- isApolloBoard,
initialBoardId: fullBoardId(boardId),
disabled: parseBoolean(el.dataset.disabled),
groupId: Number(groupId),
@@ -114,7 +93,7 @@ export default () => {
// check for browser back and trigger a hard reload to circumvent browser caching.
window.addEventListener('pageshow', (event) => {
const isNavTypeBackForward =
- window.performance && window.performance.navigation.type === NavigationType.TYPE_BACK_FORWARD;
+ window.performance && window.performance.navigation.type === navigationType.TYPE_BACK_FORWARD;
if (event.persisted || isNavTypeBackForward) {
window.location.reload();
diff --git a/app/assets/javascripts/boards/stores/actions.js b/app/assets/javascripts/boards/stores/actions.js
deleted file mode 100644
index 97e40c8cc39..00000000000
--- a/app/assets/javascripts/boards/stores/actions.js
+++ /dev/null
@@ -1,925 +0,0 @@
-import { sortBy } from 'lodash';
-import * as Sentry from '~/sentry/sentry_browser_wrapper';
-import {
- ListType,
- inactiveId,
- flashAnimationDuration,
- ISSUABLE,
- titleQueries,
- subscriptionQueries,
- deleteListQueries,
- listsQuery,
- updateListQueries,
- FilterFields,
- ListTypeTitles,
- DraggableItemTypes,
- DEFAULT_BOARD_LIST_ITEMS_SIZE,
-} from 'ee_else_ce/boards/constants';
-import {
- formatIssueInput,
- formatBoardLists,
- formatListIssues,
- formatListsPageInfo,
- formatIssue,
- updateListPosition,
- moveItemListHelper,
- getMoveData,
- FiltersInfo,
- filterVariables,
-} from 'ee_else_ce/boards/boards_util';
-import createBoardListMutation from 'ee_else_ce/boards/graphql/board_list_create.mutation.graphql';
-import issueMoveListMutation from 'ee_else_ce/boards/graphql/issue_move_list.mutation.graphql';
-import totalCountAndWeightQuery from 'ee_else_ce/boards/graphql/board_lists_deferred.query.graphql';
-import { fetchPolicies } from '~/lib/graphql';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { defaultClient as gqlClient } from '~/graphql_shared/issuable_client';
-import { TYPE_ISSUE, WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { queryToObject } from '~/lib/utils/url_utility';
-import { s__ } from '~/locale';
-import eventHub from '../eventhub';
-import projectBoardQuery from '../graphql/project_board.query.graphql';
-import groupBoardQuery from '../graphql/group_board.query.graphql';
-import boardLabelsQuery from '../graphql/board_labels.query.graphql';
-import groupBoardMilestonesQuery from '../graphql/group_board_milestones.query.graphql';
-import groupProjectsQuery from '../graphql/group_projects.query.graphql';
-import issueCreateMutation from '../graphql/issue_create.mutation.graphql';
-import listsIssuesQuery from '../graphql/lists_issues.query.graphql';
-import projectBoardMilestonesQuery from '../graphql/project_board_milestones.query.graphql';
-
-import * as types from './mutation_types';
-
-export default {
- fetchBoard: ({ commit, dispatch }, { fullPath, fullBoardId, boardType }) => {
- commit(types.REQUEST_CURRENT_BOARD);
-
- const variables = {
- fullPath,
- boardId: fullBoardId,
- };
-
- return gqlClient
- .query({
- query: boardType === WORKSPACE_GROUP ? groupBoardQuery : projectBoardQuery,
- variables,
- })
- .then(({ data }) => {
- if (data.workspace?.errors) {
- commit(types.RECEIVE_BOARD_FAILURE);
- } else {
- const board = data.workspace?.board;
- dispatch('setBoard', board);
- }
- })
- .catch(() => commit(types.RECEIVE_BOARD_FAILURE));
- },
-
- setInitialBoardData: ({ commit }, data) => {
- commit(types.SET_INITIAL_BOARD_DATA, data);
- },
-
- setBoardConfig: ({ commit }, board) => {
- const config = {
- milestoneId: board.milestone?.id || null,
- milestoneTitle: board.milestone?.title || null,
- iterationId: board.iteration?.id || null,
- iterationTitle: board.iteration?.title || null,
- iterationCadenceId: board.iterationCadence?.id || null,
- assigneeId: board.assignee?.id || null,
- assigneeUsername: board.assignee?.username || null,
- labels: board.labels?.nodes || [],
- labelIds: board.labels?.nodes?.map((label) => label.id) || [],
- weight: board.weight,
- };
- commit(types.SET_BOARD_CONFIG, config);
- },
-
- setBoard: async ({ commit, dispatch }, board) => {
- commit(types.RECEIVE_BOARD_SUCCESS, board);
- await dispatch('setBoardConfig', board);
- dispatch('performSearch', { resetLists: true });
- eventHub.$emit('updateTokens');
- },
-
- setActiveId({ commit }, { id, sidebarType }) {
- commit(types.SET_ACTIVE_ID, { id, sidebarType });
- },
-
- unsetActiveId({ dispatch }) {
- dispatch('setActiveId', { id: inactiveId, sidebarType: '' });
- },
-
- setFilters: ({ commit, state: { issuableType } }, filters) => {
- commit(
- types.SET_FILTERS,
- filterVariables({
- filters,
- issuableType,
- filterInfo: FiltersInfo,
- filterFields: FilterFields,
- }),
- );
- },
-
- performSearch({ dispatch }, { resetLists = false } = {}) {
- dispatch(
- 'setFilters',
- convertObjectPropsToCamelCase(queryToObject(window.location.search, { gatherArrays: true })),
- );
- dispatch('fetchLists', { resetLists });
- dispatch('resetIssues');
- },
-
- fetchLists: ({ commit, state, dispatch }, { resetLists = false } = {}) => {
- const { boardType, filterParams, fullPath, fullBoardId, issuableType } = state;
-
- const variables = {
- fullPath,
- boardId: fullBoardId,
- filters: filterParams,
- ...(issuableType === TYPE_ISSUE && {
- isGroup: boardType === WORKSPACE_GROUP,
- isProject: boardType === WORKSPACE_PROJECT,
- }),
- };
-
- return gqlClient
- .query({
- query: listsQuery[issuableType].query,
- variables,
- ...(resetLists ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
- })
- .then(({ data }) => {
- const { lists, hideBacklogList } = data[boardType].board;
- commit(types.RECEIVE_BOARD_LISTS_SUCCESS, formatBoardLists(lists));
- // Backlog list needs to be created if it doesn't exist and it's not hidden
- if (!lists.nodes.find((l) => l.listType === ListType.backlog) && !hideBacklogList) {
- dispatch('createList', { backlog: true });
- }
- })
- .catch(() => commit(types.RECEIVE_BOARD_LISTS_FAILURE));
- },
-
- highlightList: ({ commit, state }, listId) => {
- if ([ListType.backlog, ListType.closed].includes(state.boardLists[listId].listType)) {
- return;
- }
-
- commit(types.ADD_LIST_TO_HIGHLIGHTED_LISTS, listId);
-
- setTimeout(() => {
- commit(types.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS, listId);
- }, flashAnimationDuration);
- },
-
- createList: ({ dispatch }, { backlog, labelId, milestoneId, assigneeId }) => {
- dispatch('createIssueList', { backlog, labelId, milestoneId, assigneeId });
- },
-
- createIssueList: (
- { state, commit, dispatch, getters },
- { backlog, labelId, milestoneId, assigneeId, iterationId },
- ) => {
- const { fullBoardId } = state;
-
- const existingList = getters.getListByLabelId(labelId);
-
- if (existingList) {
- dispatch('highlightList', existingList.id);
- return;
- }
-
- gqlClient
- .mutate({
- mutation: createBoardListMutation,
- variables: {
- boardId: fullBoardId,
- backlog,
- labelId,
- milestoneId,
- assigneeId,
- iterationId,
- },
- })
- .then(({ data }) => {
- if (data.boardListCreate?.errors.length) {
- commit(types.CREATE_LIST_FAILURE, data.boardListCreate.errors[0]);
- } else {
- const list = data.boardListCreate?.list;
- dispatch('addList', list);
- dispatch('highlightList', list.id);
- }
- })
- .catch((e) => {
- commit(types.CREATE_LIST_FAILURE);
- throw e;
- });
- },
-
- addList: ({ commit, dispatch, getters }, list) => {
- commit(types.RECEIVE_ADD_LIST_SUCCESS, updateListPosition(list));
-
- dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
- });
- },
-
- fetchLabels: ({ state, commit }, searchTerm) => {
- const { fullPath, boardType } = state;
-
- const variables = {
- fullPath,
- searchTerm,
- isGroup: boardType === WORKSPACE_GROUP,
- isProject: boardType === WORKSPACE_PROJECT,
- };
-
- commit(types.RECEIVE_LABELS_REQUEST);
-
- return gqlClient
- .query({
- query: boardLabelsQuery,
- variables,
- })
- .then(({ data }) => {
- const labels = data[boardType]?.labels.nodes;
-
- commit(types.RECEIVE_LABELS_SUCCESS, labels);
- return labels;
- })
- .catch((e) => {
- commit(types.RECEIVE_LABELS_FAILURE);
- throw e;
- });
- },
-
- fetchMilestones({ state, commit }, searchTerm) {
- commit(types.RECEIVE_MILESTONES_REQUEST);
-
- const { fullPath, boardType } = state;
-
- const variables = {
- fullPath,
- searchTerm,
- };
-
- let query;
- if (boardType === WORKSPACE_PROJECT) {
- query = projectBoardMilestonesQuery;
- }
- if (boardType === WORKSPACE_GROUP) {
- query = groupBoardMilestonesQuery;
- }
-
- if (!query) {
- // eslint-disable-next-line @gitlab/require-i18n-strings
- throw new Error('Unknown board type');
- }
-
- return gqlClient
- .query({
- query,
- variables,
- })
- .then(({ data }) => {
- const errors = data.workspace?.errors;
- const milestones = data.workspace?.milestones.nodes;
-
- if (errors?.[0]) {
- throw new Error(errors[0]);
- }
-
- commit(types.RECEIVE_MILESTONES_SUCCESS, milestones);
-
- return milestones;
- })
- .catch((e) => {
- commit(types.RECEIVE_MILESTONES_FAILURE);
- throw e;
- });
- },
-
- moveList: (
- { state: { boardLists }, commit, dispatch },
- {
- item: {
- dataset: { listId: movedListId, draggableItemType },
- },
- newIndex,
- to: { children },
- },
- ) => {
- if (draggableItemType !== DraggableItemTypes.list) {
- return;
- }
-
- const displacedListId = children[newIndex].dataset.listId;
- if (movedListId === displacedListId) {
- return;
- }
-
- const listIds = sortBy(
- Object.keys(boardLists).filter(
- (listId) =>
- listId !== movedListId &&
- boardLists[listId].listType !== ListType.backlog &&
- boardLists[listId].listType !== ListType.closed,
- ),
- (i) => boardLists[i].position,
- );
-
- const targetPosition = boardLists[displacedListId].position;
- // When the dragged list moves left, displaced list should shift right.
- const shiftOffset = Number(boardLists[movedListId].position < targetPosition);
- const displacedListIndex = listIds.findIndex((listId) => listId === displacedListId);
-
- commit(
- types.MOVE_LISTS,
- listIds
- .slice(0, displacedListIndex + shiftOffset)
- .concat([movedListId], listIds.slice(displacedListIndex + shiftOffset))
- .map((listId, index) => ({ listId, position: index })),
- );
- dispatch('updateList', { listId: movedListId, position: targetPosition });
- },
-
- updateList: (
- { state: { issuableType, boardItemsByListId = {} }, dispatch },
- { listId, position, collapsed },
- ) => {
- gqlClient
- .mutate({
- mutation: updateListQueries[issuableType].mutation,
- variables: {
- listId,
- position,
- collapsed,
- },
- })
- .then(({ data }) => {
- if (data?.updateBoardList?.errors.length) {
- throw new Error();
- }
-
- // Only fetch when board items havent been fetched on a collapsed list
- if (!boardItemsByListId[listId]) {
- dispatch('fetchItemsForList', { listId });
- }
- })
- .catch(() => {
- dispatch('handleUpdateListFailure');
- });
- },
-
- handleUpdateListFailure: ({ dispatch, commit }) => {
- dispatch('fetchLists');
-
- commit(
- types.SET_ERROR,
- s__('Boards|An error occurred while updating the board list. Please try again.'),
- );
- },
-
- toggleListCollapsed: ({ commit }, { listId, collapsed }) => {
- commit(types.TOGGLE_LIST_COLLAPSED, { listId, collapsed });
- },
-
- removeList: ({ state: { issuableType, boardLists }, commit, dispatch, getters }, listId) => {
- const listsBackup = { ...boardLists };
-
- commit(types.REMOVE_LIST, listId);
-
- return gqlClient
- .mutate({
- mutation: deleteListQueries[issuableType].mutation,
- variables: {
- listId,
- },
- })
- .then(
- ({
- data: {
- destroyBoardList: { errors },
- },
- }) => {
- if (errors.length > 0) {
- commit(types.REMOVE_LIST_FAILURE, listsBackup);
- } else {
- dispatch('fetchItemsForList', {
- listId: getters.getListByTitle(ListTypeTitles.backlog)?.id,
- });
- }
- },
- )
- .catch(() => {
- commit(types.REMOVE_LIST_FAILURE, listsBackup);
- });
- },
-
- fetchItemsForList: ({ state, commit }, { listId, fetchNext = false }) => {
- if (!listId) return null;
-
- commit(types.REQUEST_ITEMS_FOR_LIST, { listId, fetchNext });
-
- const { fullPath, fullBoardId, boardType, filterParams } = state;
- const variables = {
- fullPath,
- boardId: fullBoardId,
- id: listId,
- filters: filterParams,
- isGroup: boardType === WORKSPACE_GROUP,
- isProject: boardType === WORKSPACE_PROJECT,
- first: DEFAULT_BOARD_LIST_ITEMS_SIZE,
- after: fetchNext ? state.pageInfoByListId[listId].endCursor : undefined,
- };
-
- return gqlClient
- .query({
- query: listsIssuesQuery,
- variables,
- ...(!fetchNext ? { fetchPolicy: fetchPolicies.NO_CACHE } : {}),
- })
- .then(({ data }) => {
- const { lists } = data[boardType].board;
- const listItems = formatListIssues(lists);
- const listPageInfo = formatListsPageInfo(lists);
- commit(types.RECEIVE_ITEMS_FOR_LIST_SUCCESS, { listItems, listPageInfo, listId });
- })
- .catch(() => commit(types.RECEIVE_ITEMS_FOR_LIST_FAILURE, listId));
- },
-
- resetIssues: ({ commit }) => {
- commit(types.RESET_ISSUES);
- },
-
- moveItem: ({ dispatch }, payload) => {
- dispatch('moveIssue', payload);
- },
-
- moveIssue: ({ dispatch, state }, params) => {
- const moveData = getMoveData(state, params);
-
- dispatch('moveIssueCard', moveData);
- dispatch('updateMovedIssue', moveData);
- dispatch('updateIssueOrder', { moveData });
- },
-
- moveIssueCard: ({ commit }, moveData) => {
- const {
- reordering,
- shouldClone,
- itemNotInToList,
- originalIndex,
- itemId,
- fromListId,
- toListId,
- moveBeforeId,
- moveAfterId,
- positionInList,
- allItemsLoadedInList,
- } = moveData;
-
- commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
-
- if (reordering && !allItemsLoadedInList && positionInList === -1) {
- return;
- }
-
- if (reordering) {
- commit(types.ADD_BOARD_ITEM_TO_LIST, {
- itemId,
- listId: toListId,
- moveBeforeId,
- moveAfterId,
- positionInList,
- atIndex: originalIndex,
- allItemsLoadedInList,
- });
-
- return;
- }
-
- if (itemNotInToList) {
- commit(types.ADD_BOARD_ITEM_TO_LIST, {
- itemId,
- listId: toListId,
- moveBeforeId,
- moveAfterId,
- positionInList,
- });
- }
-
- if (shouldClone) {
- commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
- }
- },
-
- updateMovedIssue: (
- { commit, state: { boardItems, boardLists } },
- { itemId, fromListId, toListId },
- ) => {
- const updatedIssue = moveItemListHelper(
- boardItems[itemId],
- boardLists[fromListId],
- boardLists[toListId],
- );
-
- commit(types.UPDATE_BOARD_ITEM, updatedIssue);
- },
-
- undoMoveIssueCard: ({ commit }, moveData) => {
- const {
- reordering,
- shouldClone,
- itemNotInToList,
- itemId,
- fromListId,
- toListId,
- originalIssue,
- originalIndex,
- } = moveData;
-
- commit(types.UPDATE_BOARD_ITEM, originalIssue);
-
- if (reordering) {
- commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
- commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
- return;
- }
-
- if (shouldClone) {
- commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: fromListId });
- }
- if (itemNotInToList) {
- commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { itemId, listId: toListId });
- }
-
- commit(types.ADD_BOARD_ITEM_TO_LIST, { itemId, listId: fromListId, atIndex: originalIndex });
- },
-
- updateIssueOrder: async ({ commit, dispatch, state }, { moveData, mutationVariables = {} }) => {
- try {
- const {
- itemId,
- fromListId,
- toListId,
- moveBeforeId,
- moveAfterId,
- itemNotInToList,
- positionInList,
- } = moveData;
- const {
- fullBoardId,
- filterParams,
- boardItems: {
- [itemId]: { iid, referencePath },
- },
- } = state;
-
- commit(types.MUTATE_ISSUE_IN_PROGRESS, true);
-
- const { data } = await gqlClient.mutate({
- mutation: issueMoveListMutation,
- variables: {
- iid,
- projectPath: referencePath.split(/[#]/)[0],
- boardId: fullBoardId,
- fromListId: getIdFromGraphQLId(fromListId),
- toListId: getIdFromGraphQLId(toListId),
- moveBeforeId: moveBeforeId ? getIdFromGraphQLId(moveBeforeId) : undefined,
- moveAfterId: moveAfterId ? getIdFromGraphQLId(moveAfterId) : undefined,
- positionInList,
- // 'mutationVariables' allows EE code to pass in extra parameters.
- ...mutationVariables,
- },
- update(
- cache,
- {
- data: {
- issuableMoveList: {
- issuable: { weight },
- },
- },
- },
- ) {
- if (fromListId === toListId) return;
-
- const updateFromList = () => {
- const fromList = cache.readQuery({
- query: totalCountAndWeightQuery,
- variables: { id: fromListId, filters: filterParams },
- });
-
- const updatedFromList = {
- boardList: {
- __typename: 'BoardList',
- id: fromList.boardList.id,
- issuesCount: fromList.boardList.issuesCount - 1,
- totalIssueWeight: fromList.boardList.totalIssueWeight - Number(weight),
- },
- };
-
- cache.writeQuery({
- query: totalCountAndWeightQuery,
- variables: { id: fromListId, filters: filterParams },
- data: updatedFromList,
- });
- };
-
- const updateToList = () => {
- if (!itemNotInToList) return;
-
- const toList = cache.readQuery({
- query: totalCountAndWeightQuery,
- variables: { id: toListId, filters: filterParams },
- });
-
- const updatedToList = {
- boardList: {
- __typename: 'BoardList',
- id: toList.boardList.id,
- issuesCount: toList.boardList.issuesCount + 1,
- totalIssueWeight: toList.boardList.totalIssueWeight + Number(weight),
- },
- };
-
- cache.writeQuery({
- query: totalCountAndWeightQuery,
- variables: { id: toListId, filters: filterParams },
- data: updatedToList,
- });
- };
-
- updateFromList();
- updateToList();
- },
- });
-
- if (data?.issuableMoveList?.errors.length || !data.issuableMoveList) {
- throw new Error('issueMoveList empty');
- }
-
- commit(types.MUTATE_ISSUE_SUCCESS, { issue: data.issuableMoveList.issuable });
- commit(types.MUTATE_ISSUE_IN_PROGRESS, false);
- } catch {
- commit(types.MUTATE_ISSUE_IN_PROGRESS, false);
- commit(
- types.SET_ERROR,
- s__('Boards|An error occurred while moving the issue. Please try again.'),
- );
- dispatch('undoMoveIssueCard', moveData);
- }
- },
-
- setAssignees: ({ commit }, { id, assignees }) => {
- commit('UPDATE_BOARD_ITEM_BY_ID', {
- itemId: id,
- prop: 'assignees',
- value: assignees,
- });
- },
-
- addListItem: ({ commit, dispatch }, { list, item, position, inProgress = false }) => {
- commit(types.ADD_BOARD_ITEM_TO_LIST, {
- listId: list.id,
- itemId: item.id,
- atIndex: position,
- inProgress,
- });
- commit(types.UPDATE_BOARD_ITEM, item);
- if (!inProgress) {
- dispatch('setActiveId', { id: item.id, sidebarType: ISSUABLE });
- }
- },
-
- removeListItem: ({ commit }, { listId, itemId }) => {
- commit(types.REMOVE_BOARD_ITEM_FROM_LIST, { listId, itemId });
- commit(types.REMOVE_BOARD_ITEM, itemId);
- },
-
- addListNewIssue: (
- { state: { boardConfig, boardType, fullPath, filterParams }, dispatch, commit },
- { issueInput, list, placeholderId = `tmp-${new Date().getTime()}` },
- ) => {
- const input = formatIssueInput(issueInput, boardConfig);
-
- if (boardType === WORKSPACE_PROJECT) {
- input.projectPath = fullPath;
- }
-
- const placeholderIssue = formatIssue({ ...issueInput, id: placeholderId, isLoading: true });
- dispatch('addListItem', { list, item: placeholderIssue, position: 0, inProgress: true });
-
- gqlClient
- .mutate({
- mutation: issueCreateMutation,
- variables: { input },
- update(cache) {
- const fromList = cache.readQuery({
- query: totalCountAndWeightQuery,
- variables: { id: list.id, filters: filterParams },
- });
-
- const updatedList = {
- boardList: {
- __typename: 'BoardList',
- id: fromList.boardList.id,
- issuesCount: fromList.boardList.issuesCount + 1,
- totalIssueWeight: fromList.boardList.totalIssueWeight,
- },
- };
-
- cache.writeQuery({
- query: totalCountAndWeightQuery,
- variables: { id: list.id, filters: filterParams },
- data: updatedList,
- });
- },
- })
- .then(({ data }) => {
- if (data.createIssuable.errors.length) {
- throw new Error();
- }
-
- const rawIssue = data.createIssuable?.issuable;
- const formattedIssue = formatIssue(rawIssue);
- dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
- dispatch('addListItem', { list, item: formattedIssue, position: 0 });
- })
- .catch(() => {
- dispatch('removeListItem', { listId: list.id, itemId: placeholderId });
- commit(
- types.SET_ERROR,
- s__('Boards|An error occurred while creating the issue. Please try again.'),
- );
- });
- },
-
- setActiveBoardItemLabels: ({ dispatch }, params) => {
- dispatch('setActiveIssueLabels', params);
- },
-
- setActiveIssueLabels: async ({ commit, getters }, input) => {
- const { activeBoardItem } = getters;
-
- let labels = input?.labels || [];
- if (input.removeLabelIds) {
- labels = activeBoardItem.labels.filter(
- (label) => input.removeLabelIds[0] !== getIdFromGraphQLId(label.id),
- );
- }
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: input.id || activeBoardItem.id,
- prop: 'labels',
- value: labels,
- });
- },
-
- setActiveItemSubscribed: async ({ commit, getters, state }, input) => {
- const { activeBoardItem, isEpicBoard } = getters;
- const { fullPath, issuableType } = state;
- const workspacePath = isEpicBoard
- ? { groupPath: fullPath }
- : { projectPath: input.projectPath };
- const { data } = await gqlClient.mutate({
- mutation: subscriptionQueries[issuableType].mutation,
- variables: {
- input: {
- ...workspacePath,
- iid: String(activeBoardItem.iid),
- subscribedState: input.subscribed,
- },
- },
- });
-
- if (data.updateIssuableSubscription?.errors?.length > 0) {
- throw new Error(data.updateIssuableSubscription[issuableType].errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'subscribed',
- value: data.updateIssuableSubscription[issuableType].subscribed,
- });
- },
-
- setActiveItemTitle: async ({ commit, getters, state }, input) => {
- const { activeBoardItem, isEpicBoard } = getters;
- const { fullPath, issuableType } = state;
- const workspacePath = isEpicBoard
- ? { groupPath: fullPath }
- : { projectPath: input.projectPath };
- const { data } = await gqlClient.mutate({
- mutation: titleQueries[issuableType].mutation,
- variables: {
- input: {
- ...workspacePath,
- iid: String(activeBoardItem.iid),
- title: input.title,
- },
- },
- });
-
- if (data.updateIssuableTitle?.errors?.length > 0) {
- throw new Error(data.updateIssuableTitle.errors);
- }
-
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'title',
- value: data.updateIssuableTitle[issuableType].title,
- });
- },
-
- setActiveItemConfidential: ({ commit, getters }, confidential) => {
- const { activeBoardItem } = getters;
- commit(types.UPDATE_BOARD_ITEM_BY_ID, {
- itemId: activeBoardItem.id,
- prop: 'confidential',
- value: confidential,
- });
- },
-
- fetchGroupProjects: ({ commit, state }, { search = '', fetchNext = false }) => {
- commit(types.REQUEST_GROUP_PROJECTS, fetchNext);
-
- const { fullPath } = state;
-
- const variables = {
- fullPath,
- search: search !== '' ? search : undefined,
- after: fetchNext ? state.groupProjectsFlags.pageInfo.endCursor : undefined,
- };
-
- return gqlClient
- .query({
- query: groupProjectsQuery,
- variables,
- })
- .then(({ data }) => {
- const { projects } = data.group;
- commit(types.RECEIVE_GROUP_PROJECTS_SUCCESS, {
- projects: projects.nodes,
- pageInfo: projects.pageInfo,
- fetchNext,
- });
- })
- .catch(() => commit(types.RECEIVE_GROUP_PROJECTS_FAILURE));
- },
-
- setSelectedProject: ({ commit }, project) => {
- commit(types.SET_SELECTED_PROJECT, project);
- },
-
- toggleBoardItemMultiSelection: ({ commit, state, dispatch, getters }, boardItem) => {
- const { selectedBoardItems } = state;
- const index = selectedBoardItems.indexOf(boardItem);
-
- // If user already selected an item (activeBoardItem) without using mult-select,
- // include that item in the selection and unset state.ActiveId to hide the sidebar.
- if (getters.activeBoardItem) {
- commit(types.ADD_BOARD_ITEM_TO_SELECTION, getters.activeBoardItem);
- dispatch('unsetActiveId');
- }
-
- if (index === -1) {
- commit(types.ADD_BOARD_ITEM_TO_SELECTION, boardItem);
- } else {
- commit(types.REMOVE_BOARD_ITEM_FROM_SELECTION, boardItem);
- }
- },
-
- setAddColumnFormVisibility: ({ commit }, visible) => {
- commit(types.SET_ADD_COLUMN_FORM_VISIBLE, visible);
- },
-
- resetBoardItemMultiSelection: ({ commit }) => {
- commit(types.RESET_BOARD_ITEM_SELECTION);
- },
-
- toggleBoardItem: ({ state, dispatch }, { boardItem, sidebarType = ISSUABLE }) => {
- dispatch('resetBoardItemMultiSelection');
-
- if (boardItem.id === state.activeId) {
- dispatch('unsetActiveId');
- } else {
- dispatch('setActiveId', { id: boardItem.id, sidebarType });
- }
- },
-
- setError: ({ commit }, { message, error, captureError = true }) => {
- commit(types.SET_ERROR, message);
-
- if (captureError) {
- Sentry.captureException(error);
- }
- },
-
- unsetError: ({ commit }) => {
- commit(types.SET_ERROR, undefined);
- },
-
- // EE action needs CE empty equivalent
- setActiveItemWeight: () => {},
- setActiveItemHealthStatus: () => {},
-};
diff --git a/app/assets/javascripts/boards/stores/getters.js b/app/assets/javascripts/boards/stores/getters.js
deleted file mode 100644
index 0ad71165996..00000000000
--- a/app/assets/javascripts/boards/stores/getters.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { find } from 'lodash';
-import { TYPE_ISSUE } from '~/issues/constants';
-import { inactiveId } from '../constants';
-
-export default {
- isSidebarOpen: (state) => state.activeId !== inactiveId,
- isSwimlanesOn: () => false,
- getBoardItemById: (state) => (id) => {
- return state.boardItems[id] || {};
- },
-
- getBoardItemsByList: (state, getters) => (listId) => {
- const listItemsIds = state.boardItemsByListId[listId] || [];
- return listItemsIds.map((id) => getters.getBoardItemById(id));
- },
-
- activeBoardItem: (state) => {
- return state.boardItems[state.activeId] || { iid: '', id: '' };
- },
-
- groupPathForActiveIssue: (_, getters) => {
- const { referencePath = '' } = getters.activeBoardItem;
- return referencePath.slice(0, referencePath.lastIndexOf('/'));
- },
-
- projectPathForActiveIssue: (_, getters) => {
- const { referencePath = '' } = getters.activeBoardItem;
- return referencePath.slice(0, referencePath.indexOf('#'));
- },
-
- activeGroupProjects: (state) => {
- return state.groupProjects.filter((p) => !p.archived);
- },
-
- getListByLabelId: (state) => (labelId) => {
- if (!labelId) {
- return null;
- }
- return find(state.boardLists, (l) => l.label?.id === labelId);
- },
-
- getListByTitle: (state) => (title) => {
- return find(state.boardLists, (l) => l.title === title);
- },
-
- isIssueBoard: (state) => {
- return state.issuableType === TYPE_ISSUE;
- },
-
- isEpicBoard: () => {
- return false;
- },
-
- hasScope: (state) => {
- const { boardConfig } = state;
- if (boardConfig.labels?.length > 0) {
- return true;
- }
- let hasScope = false;
- ['assigneeId', 'iterationCadenceId', 'iterationId', 'milestoneId', 'weight'].forEach((attr) => {
- if (boardConfig[attr] !== null && boardConfig[attr] !== undefined) {
- hasScope = true;
- }
- });
- return hasScope;
- },
-};
diff --git a/app/assets/javascripts/boards/stores/index.js b/app/assets/javascripts/boards/stores/index.js
deleted file mode 100644
index fd562df1df7..00000000000
--- a/app/assets/javascripts/boards/stores/index.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import Vue from 'vue';
-// eslint-disable-next-line no-restricted-imports
-import Vuex from 'vuex';
-import actions from 'ee_else_ce/boards/stores/actions';
-import getters from 'ee_else_ce/boards/stores/getters';
-import mutations from 'ee_else_ce/boards/stores/mutations';
-import state from 'ee_else_ce/boards/stores/state';
-
-Vue.use(Vuex);
-
-export const storeOptions = {
- state,
- getters,
- actions,
- mutations,
-};
-
-export const createStore = (options = storeOptions) => new Vuex.Store(options);
-
-export default createStore();
diff --git a/app/assets/javascripts/boards/stores/mutation_types.js b/app/assets/javascripts/boards/stores/mutation_types.js
deleted file mode 100644
index 0e496677b7b..00000000000
--- a/app/assets/javascripts/boards/stores/mutation_types.js
+++ /dev/null
@@ -1,47 +0,0 @@
-export const REQUEST_CURRENT_BOARD = 'REQUEST_CURRENT_BOARD';
-export const RECEIVE_BOARD_SUCCESS = 'RECEIVE_BOARD_SUCCESS';
-export const RECEIVE_BOARD_FAILURE = 'RECEIVE_BOARD_FAILURE';
-export const SET_INITIAL_BOARD_DATA = 'SET_INITIAL_BOARD_DATA';
-export const SET_BOARD_CONFIG = 'SET_BOARD_CONFIG';
-export const SET_FILTERS = 'SET_FILTERS';
-export const CREATE_LIST_SUCCESS = 'CREATE_LIST_SUCCESS';
-export const CREATE_LIST_FAILURE = 'CREATE_LIST_FAILURE';
-export const RECEIVE_LABELS_REQUEST = 'RECEIVE_LABELS_REQUEST';
-export const RECEIVE_LABELS_SUCCESS = 'RECEIVE_LABELS_SUCCESS';
-export const RECEIVE_LABELS_FAILURE = 'RECEIVE_LABELS_FAILURE';
-export const GENERATE_DEFAULT_LISTS_FAILURE = 'GENERATE_DEFAULT_LISTS_FAILURE';
-export const RECEIVE_BOARD_LISTS_SUCCESS = 'RECEIVE_BOARD_LISTS_SUCCESS';
-export const RECEIVE_BOARD_LISTS_FAILURE = 'RECEIVE_BOARD_LISTS_FAILURE';
-export const SHOW_PROMOTION_LIST = 'SHOW_PROMOTION_LIST';
-export const RECEIVE_ADD_LIST_SUCCESS = 'RECEIVE_ADD_LIST_SUCCESS';
-export const MOVE_LISTS = 'MOVE_LISTS';
-export const TOGGLE_LIST_COLLAPSED = 'TOGGLE_LIST_COLLAPSED';
-export const REMOVE_LIST = 'REMOVE_LIST';
-export const REMOVE_LIST_FAILURE = 'REMOVE_LIST_FAILURE';
-export const REQUEST_ITEMS_FOR_LIST = 'REQUEST_ITEMS_FOR_LIST';
-export const RECEIVE_ITEMS_FOR_LIST_FAILURE = 'RECEIVE_ITEMS_FOR_LIST_FAILURE';
-export const RECEIVE_ITEMS_FOR_LIST_SUCCESS = 'RECEIVE_ITEMS_FOR_LIST_SUCCESS';
-export const RECEIVE_MILESTONES_REQUEST = 'RECEIVE_MILESTONES_REQUEST';
-export const RECEIVE_MILESTONES_SUCCESS = 'RECEIVE_MILESTONES_SUCCESS';
-export const RECEIVE_MILESTONES_FAILURE = 'RECEIVE_MILESTONES_FAILURE';
-export const UPDATE_BOARD_ITEM = 'UPDATE_BOARD_ITEM';
-export const REMOVE_BOARD_ITEM = 'REMOVE_BOARD_ITEM';
-export const MUTATE_ISSUE_SUCCESS = 'MUTATE_ISSUE_SUCCESS';
-export const ADD_BOARD_ITEM_TO_LIST = 'ADD_BOARD_ITEM_TO_LIST';
-export const REMOVE_BOARD_ITEM_FROM_LIST = 'REMOVE_BOARD_ITEM_FROM_LIST';
-export const SET_ACTIVE_ID = 'SET_ACTIVE_ID';
-export const UPDATE_BOARD_ITEM_BY_ID = 'UPDATE_BOARD_ITEM_BY_ID';
-export const SET_ASSIGNEE_LOADING = 'SET_ASSIGNEE_LOADING';
-export const RESET_ISSUES = 'RESET_ISSUES';
-export const REQUEST_GROUP_PROJECTS = 'REQUEST_GROUP_PROJECTS';
-export const RECEIVE_GROUP_PROJECTS_SUCCESS = 'RECEIVE_GROUP_PROJECTS_SUCCESS';
-export const RECEIVE_GROUP_PROJECTS_FAILURE = 'RECEIVE_GROUP_PROJECTS_FAILURE';
-export const SET_SELECTED_PROJECT = 'SET_SELECTED_PROJECT';
-export const ADD_BOARD_ITEM_TO_SELECTION = 'ADD_BOARD_ITEM_TO_SELECTION';
-export const REMOVE_BOARD_ITEM_FROM_SELECTION = 'REMOVE_BOARD_ITEM_FROM_SELECTION';
-export const SET_ADD_COLUMN_FORM_VISIBLE = 'SET_ADD_COLUMN_FORM_VISIBLE';
-export const ADD_LIST_TO_HIGHLIGHTED_LISTS = 'ADD_LIST_TO_HIGHLIGHTED_LISTS';
-export const REMOVE_LIST_FROM_HIGHLIGHTED_LISTS = 'REMOVE_LIST_FROM_HIGHLIGHTED_LISTS';
-export const RESET_BOARD_ITEM_SELECTION = 'RESET_BOARD_ITEM_SELECTION';
-export const SET_ERROR = 'SET_ERROR';
-export const MUTATE_ISSUE_IN_PROGRESS = 'MUTATE_ISSUE_IN_PROGRESS';
diff --git a/app/assets/javascripts/boards/stores/mutations.js b/app/assets/javascripts/boards/stores/mutations.js
deleted file mode 100644
index 505c011b034..00000000000
--- a/app/assets/javascripts/boards/stores/mutations.js
+++ /dev/null
@@ -1,319 +0,0 @@
-import { cloneDeep, pull, union } from 'lodash';
-import Vue from 'vue';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { TYPE_EPIC } from '~/issues/constants';
-import { s__, __ } from '~/locale';
-import { formatIssue } from '../boards_util';
-import * as mutationTypes from './mutation_types';
-
-const updateListItemsCount = ({ state, listId, value }) => {
- const list = state.boardLists[listId];
- if (state.issuableType === TYPE_EPIC) {
- const listItem = cloneDeep(state.boardLists[listId]);
- listItem.metadataepicsCount += value;
- Vue.set(state.boardLists[listId], listId, listItem);
- }
- Vue.set(state.boardLists, listId, { ...list });
-};
-
-export const removeItemFromList = ({ state, listId, itemId, reordering = false }) => {
- Vue.set(state.boardItemsByListId, listId, pull(state.boardItemsByListId[listId], itemId));
- if (!reordering) {
- updateListItemsCount({ state, listId, value: -1 });
- }
-};
-
-export const addItemToList = ({
- state,
- listId,
- itemId,
- moveBeforeId,
- moveAfterId,
- atIndex,
- positionInList,
- reordering = false,
-}) => {
- const listIssues = state.boardItemsByListId[listId];
- let newIndex = atIndex || 0;
- const moveToStartOrLast = positionInList !== undefined;
- if (moveBeforeId) {
- newIndex = listIssues.indexOf(moveBeforeId) + 1;
- } else if (moveAfterId) {
- newIndex = listIssues.indexOf(moveAfterId);
- } else if (moveToStartOrLast) {
- newIndex = positionInList === -1 ? listIssues.length : 0;
- }
- listIssues.splice(newIndex, 0, itemId);
- Vue.set(state.boardItemsByListId, listId, listIssues);
- if (!reordering) {
- updateListItemsCount({ state, listId, value: moveToStartOrLast ? 0 : 1 });
- }
-};
-
-export default {
- [mutationTypes.REQUEST_CURRENT_BOARD]: (state) => {
- state.isBoardLoading = true;
- },
-
- [mutationTypes.RECEIVE_BOARD_SUCCESS]: (state, board) => {
- state.board = {
- ...board,
- labels: board?.labels?.nodes || [],
- };
- state.fullBoardId = board.id;
- state.boardId = getIdFromGraphQLId(board.id);
- state.isBoardLoading = false;
- },
-
- [mutationTypes.RECEIVE_BOARD_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching the board. Please reload the page.');
- state.isBoardLoading = false;
- },
-
- [mutationTypes.SET_INITIAL_BOARD_DATA](state, data) {
- const {
- allowSubEpics,
- boardId,
- boardType,
- disabled,
- fullBoardId,
- fullPath,
- issuableType,
- } = data;
- state.allowSubEpics = allowSubEpics;
- state.boardId = boardId;
- state.boardType = boardType;
- state.disabled = disabled;
- state.fullBoardId = fullBoardId;
- state.fullPath = fullPath;
- state.issuableType = issuableType;
- },
-
- [mutationTypes.SET_BOARD_CONFIG](state, boardConfig) {
- state.boardConfig = boardConfig;
- },
-
- [mutationTypes.RECEIVE_BOARD_LISTS_SUCCESS]: (state, lists) => {
- state.boardLists = lists;
- },
-
- [mutationTypes.RECEIVE_BOARD_LISTS_FAILURE]: (state) => {
- state.error = s__(
- 'Boards|An error occurred while fetching the board lists. Please reload the page.',
- );
- },
-
- [mutationTypes.SET_ACTIVE_ID](state, { id, sidebarType }) {
- state.activeId = id;
- state.sidebarType = sidebarType;
- },
-
- [mutationTypes.SET_FILTERS](state, filterParams) {
- state.filterParams = filterParams;
- },
-
- [mutationTypes.CREATE_LIST_FAILURE]: (
- state,
- error = s__('Boards|An error occurred while creating the list. Please try again.'),
- ) => {
- state.error = error;
- },
-
- [mutationTypes.RECEIVE_LABELS_REQUEST]: (state) => {
- state.labelsLoading = true;
- },
-
- [mutationTypes.RECEIVE_LABELS_SUCCESS]: (state, labels) => {
- state.labels = labels;
- state.labelsLoading = false;
- },
-
- [mutationTypes.RECEIVE_LABELS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching labels. Please reload the page.');
- state.labelsLoading = false;
- },
-
- [mutationTypes.GENERATE_DEFAULT_LISTS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while generating lists. Please reload the page.');
- },
-
- [mutationTypes.RECEIVE_ADD_LIST_SUCCESS]: (state, list) => {
- Vue.set(state.boardLists, list.id, list);
- },
-
- [mutationTypes.MOVE_LISTS]: (state, movedLists) => {
- const updatedBoardList = movedLists.reduce((acc, { listId, position }) => {
- acc[listId].position = position;
- return acc;
- }, cloneDeep(state.boardLists));
- Vue.set(state, 'boardLists', updatedBoardList);
- },
-
- [mutationTypes.TOGGLE_LIST_COLLAPSED]: (state, { listId, collapsed }) => {
- Vue.set(state.boardLists[listId], 'collapsed', collapsed);
- },
-
- [mutationTypes.REMOVE_LIST]: (state, listId) => {
- Vue.delete(state.boardLists, listId);
- },
-
- [mutationTypes.REMOVE_LIST_FAILURE](state, listsBackup) {
- state.error = s__('Boards|An error occurred while removing the list. Please try again.');
- state.boardLists = listsBackup;
- },
-
- [mutationTypes.REQUEST_ITEMS_FOR_LIST]: (state, { listId, fetchNext }) => {
- Vue.set(state.listsFlags, listId, { [fetchNext ? 'isLoadingMore' : 'isLoading']: true });
- },
-
- [mutationTypes.RECEIVE_MILESTONES_SUCCESS](state, milestones) {
- state.milestones = milestones;
- state.milestonesLoading = false;
- },
-
- [mutationTypes.RECEIVE_MILESTONES_REQUEST](state) {
- state.milestonesLoading = true;
- },
-
- [mutationTypes.RECEIVE_MILESTONES_FAILURE](state) {
- state.milestonesLoading = false;
- state.error = __('Failed to load milestones.');
- },
-
- [mutationTypes.RECEIVE_ITEMS_FOR_LIST_SUCCESS]: (state, { listItems, listPageInfo, listId }) => {
- const { listData, boardItems } = listItems;
- Vue.set(state, 'boardItems', { ...state.boardItems, ...boardItems });
- Vue.set(
- state.boardItemsByListId,
- listId,
- union(state.boardItemsByListId[listId] || [], listData[listId]),
- );
- Vue.set(state.pageInfoByListId, listId, listPageInfo[listId]);
- Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
- },
-
- [mutationTypes.RECEIVE_ITEMS_FOR_LIST_FAILURE]: (state, listId) => {
- state.error = s__(
- 'Boards|An error occurred while fetching the board issues. Please reload the page.',
- );
- Vue.set(state.listsFlags, listId, { isLoading: false, isLoadingMore: false });
- },
-
- [mutationTypes.RESET_ISSUES]: (state) => {
- Object.keys(state.boardItemsByListId).forEach((listId) => {
- Vue.set(state.boardItemsByListId, listId, []);
- });
- },
-
- [mutationTypes.UPDATE_BOARD_ITEM_BY_ID]: (state, { itemId, prop, value }) => {
- if (!state.boardItems[itemId]) {
- /* eslint-disable-next-line @gitlab/require-i18n-strings */
- throw new Error('No issue found.');
- }
-
- Vue.set(state.boardItems[itemId], prop, value);
- },
-
- [mutationTypes.SET_ASSIGNEE_LOADING](state, isLoading) {
- state.isSettingAssignees = isLoading;
- },
-
- [mutationTypes.MUTATE_ISSUE_SUCCESS]: (state, { issue }) => {
- Vue.set(state.boardItems, issue.id, formatIssue(issue));
- },
-
- [mutationTypes.MUTATE_ISSUE_IN_PROGRESS](state, isLoading) {
- state.isUpdateIssueOrderInProgress = isLoading;
- },
-
- [mutationTypes.ADD_BOARD_ITEM_TO_LIST]: (
- state,
- {
- itemId,
- listId,
- moveBeforeId,
- moveAfterId,
- atIndex,
- positionInList,
- allItemsLoadedInList,
- inProgress = false,
- },
- ) => {
- Vue.set(state.listsFlags, listId, { ...state.listsFlags, addItemToListInProgress: inProgress });
- addItemToList({
- state,
- listId,
- itemId,
- moveBeforeId,
- moveAfterId,
- atIndex,
- positionInList,
- allItemsLoadedInList,
- });
- },
-
- [mutationTypes.REMOVE_BOARD_ITEM_FROM_LIST]: (state, { itemId, listId }) => {
- removeItemFromList({ state, listId, itemId });
- },
-
- [mutationTypes.UPDATE_BOARD_ITEM]: (state, item) => {
- Vue.set(state.boardItems, item.id, item);
- },
-
- [mutationTypes.REMOVE_BOARD_ITEM]: (state, itemId) => {
- Vue.delete(state.boardItems, itemId);
- },
-
- [mutationTypes.REQUEST_GROUP_PROJECTS]: (state, fetchNext) => {
- Vue.set(state, 'groupProjectsFlags', {
- [fetchNext ? 'isLoadingMore' : 'isLoading']: true,
- pageInfo: state.groupProjectsFlags.pageInfo,
- });
- },
-
- [mutationTypes.RECEIVE_GROUP_PROJECTS_SUCCESS]: (state, { projects, pageInfo, fetchNext }) => {
- Vue.set(state, 'groupProjects', fetchNext ? [...state.groupProjects, ...projects] : projects);
- Vue.set(state, 'groupProjectsFlags', { isLoading: false, isLoadingMore: false, pageInfo });
- },
-
- [mutationTypes.RECEIVE_GROUP_PROJECTS_FAILURE]: (state) => {
- state.error = s__('Boards|An error occurred while fetching group projects. Please try again.');
- Vue.set(state, 'groupProjectsFlags', { isLoading: false, isLoadingMore: false });
- },
-
- [mutationTypes.SET_SELECTED_PROJECT]: (state, project) => {
- state.selectedProject = project;
- },
-
- [mutationTypes.ADD_BOARD_ITEM_TO_SELECTION]: (state, boardItem) => {
- state.selectedBoardItems = [...state.selectedBoardItems, boardItem];
- },
-
- [mutationTypes.REMOVE_BOARD_ITEM_FROM_SELECTION]: (state, boardItem) => {
- Vue.set(
- state,
- 'selectedBoardItems',
- state.selectedBoardItems.filter((obj) => obj !== boardItem),
- );
- },
-
- [mutationTypes.SET_ADD_COLUMN_FORM_VISIBLE]: (state, visible) => {
- Vue.set(state.addColumnForm, 'visible', visible);
- },
-
- [mutationTypes.ADD_LIST_TO_HIGHLIGHTED_LISTS]: (state, listId) => {
- state.highlightedLists.push(listId);
- },
-
- [mutationTypes.REMOVE_LIST_FROM_HIGHLIGHTED_LISTS]: (state, listId) => {
- state.highlightedLists = state.highlightedLists.filter((id) => id !== listId);
- },
-
- [mutationTypes.RESET_BOARD_ITEM_SELECTION]: (state) => {
- state.selectedBoardItems = [];
- },
-
- [mutationTypes.SET_ERROR]: (state, error) => {
- state.error = error;
- },
-};
diff --git a/app/assets/javascripts/boards/stores/state.js b/app/assets/javascripts/boards/stores/state.js
deleted file mode 100644
index bf3f777ea7d..00000000000
--- a/app/assets/javascripts/boards/stores/state.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { inactiveId, ListType } from '~/boards/constants';
-
-export default () => ({
- board: {},
- isBoardLoading: false,
- boardType: null,
- issuableType: null,
- fullPath: null,
- disabled: false,
- isShowingLabels: true,
- activeId: inactiveId,
- sidebarType: '',
- boardLists: {},
- listsFlags: {},
- boardItemsByListId: {},
- isSettingAssignees: false,
- pageInfoByListId: {},
- boardItems: {},
- filterParams: {},
- boardConfig: {},
- labelsLoading: false,
- labels: [],
- milestones: [],
- milestonesLoading: false,
- highlightedLists: [],
- selectedBoardItems: [],
- groupProjects: [],
- groupProjectsFlags: {
- isLoading: false,
- isLoadingMore: false,
- pageInfo: {},
- },
- selectedProject: {},
- error: undefined,
- iterations: [],
- iterationsLoading: false,
- addColumnForm: {
- visible: false,
- columnType: ListType.label,
- },
- // TODO: remove after ce/ee split of board_content.vue
- isShowingEpicsSwimlanes: false,
- isUpdateIssueOrderInProgress: false,
-});
diff --git a/app/assets/javascripts/branches/components/graph_bar.vue b/app/assets/javascripts/branches/components/graph_bar.vue
index 21cbcac820a..885db7651a1 100644
--- a/app/assets/javascripts/branches/components/graph_bar.vue
+++ b/app/assets/javascripts/branches/components/graph_bar.vue
@@ -62,7 +62,7 @@ export default {
:class="[roundedClass, positionSideClass]"
class="position-absolute bar js-graph-bar"
></div>
- <span :class="textAlignmentClass" class="d-block pt-1 pr-1 count js-graph-count">
+ <span :class="textAlignmentClass" class="gl-display-block gl-pt-1 gl-px-1 count js-graph-count">
{{ label }}
</span>
</div>
diff --git a/app/assets/javascripts/breadcrumb.js b/app/assets/javascripts/breadcrumb.js
index 0dacd5af5cc..b06a6f8b141 100644
--- a/app/assets/javascripts/breadcrumb.js
+++ b/app/assets/javascripts/breadcrumb.js
@@ -34,7 +34,7 @@ export default () => {
$('li.expander').remove();
// set focus on first breadcrumb item
- $('.breadcrumb-item-text').first().focus();
+ $('.js-breadcrumb-item-text').first().focus();
});
}
};
diff --git a/app/assets/javascripts/captcha/captcha_modal.vue b/app/assets/javascripts/captcha/captcha_modal.vue
index 36aa098d5ff..fd37e498699 100644
--- a/app/assets/javascripts/captcha/captcha_modal.vue
+++ b/app/assets/javascripts/captcha/captcha_modal.vue
@@ -26,10 +26,21 @@ export default {
type: String,
required: true,
},
+ showModal: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ resetSession: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
modalId: uniqueId('captcha-modal-'),
+ captcha: null,
};
},
watch: {
@@ -37,29 +48,44 @@ export default {
// If this is true, we need to present the captcha modal to the user.
// When the modal is shown we will also initialize and render the form.
if (newNeedsCaptchaResponse) {
- this.$refs.modal.show();
+ this.renderCaptcha();
}
},
+ resetSession: {
+ immediate: true,
+ handler(reset) {
+ if (reset && this.captcha) {
+ this.resetCaptcha();
+ }
+ },
+ },
},
mounted() {
// If this is true, we need to present the captcha modal to the user.
// When the modal is shown we will also initialize and render the form.
if (this.needsCaptchaResponse) {
- this.$refs.modal.show();
+ this.renderCaptcha();
}
},
methods: {
emitReceivedCaptchaResponse(captchaResponse) {
- this.$refs.modal.hide();
+ if (this.showModal) this.$refs.modal.hide();
this.$emit('receivedCaptchaResponse', captchaResponse);
},
emitNullReceivedCaptchaResponse() {
this.emitReceivedCaptchaResponse(null);
},
+ renderCaptcha() {
+ if (this.showModal) {
+ this.$refs.modal.show();
+ } else {
+ this.initCaptcha();
+ }
+ },
/**
* handler for when modal is shown
*/
- shown() {
+ initCaptcha() {
const containerRef = this.$refs.captcha;
// NOTE: This is the only bit that is specific to Google's reCAPTCHA captcha implementation.
@@ -72,12 +98,13 @@ export default {
// TODO: Also need to handle expired-callback and error-callback
// See https://gitlab.com/gitlab-org/gitlab/-/issues/217722#future-follow-on-issuesmrs
});
+
+ this.captcha = grecaptcha;
})
.catch((e) => {
// TODO: flash the error or notify the user some other way
// See https://gitlab.com/gitlab-org/gitlab/-/issues/217722#future-follow-on-issuesmrs
this.emitNullReceivedCaptchaResponse();
- this.$refs.modal.hide();
// eslint-disable-next-line no-console
console.error(
@@ -96,6 +123,9 @@ export default {
this.emitNullReceivedCaptchaResponse();
}
},
+ resetCaptcha() {
+ this.captcha.reset();
+ },
},
};
</script>
@@ -104,17 +134,19 @@ export default {
<!-- there must be at least one button or focusable element, or the gl-modal fails to render. -->
<!-- We could modify gl-model to remove this requirement. -->
<gl-modal
+ v-if="showModal"
ref="modal"
:modal-id="modalId"
:title="__('Please solve the captcha')"
:action-cancel="/* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ {
text: __('Cancel'),
} /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */"
- @shown="shown"
+ @shown="initCaptcha"
@hide="hide"
@hidden="$emit('hidden')"
>
<div ref="captcha"></div>
<p>{{ __('We want to be sure it is you, please confirm you are not a robot.') }}</p>
</gl-modal>
+ <div v-else ref="captcha" class="gl-display-inline-block"></div>
</template>
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 6d062d8b7f1..7085397c649 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
@@ -106,7 +106,7 @@ export default {
<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"
- ><span>{{ generateSnippet(component.path) }}</span>
+ ><span>{{ generateSnippet(component.includePath) }}</span>
</pre>
<div class="gl--flex-center gl-bg-gray-10 gl-border gl-border-l-none">
<gl-button
@@ -115,7 +115,7 @@ export default {
icon="copy-to-clipboard"
size="small"
:title="$options.i18n.copyText"
- :data-clipboard-text="generateSnippet(component.path)"
+ :data-clipboard-text="generateSnippet(component.includePath)"
data-testid="copy-to-clipboard"
:aria-label="$options.i18n.copyAriaText"
/>
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 b9d6173a777..929175b964f 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
@@ -49,7 +49,7 @@ export default {
return getIdFromGraphQLId(this.resource.id);
},
hasLatestVersion() {
- return this.latestVersion?.tagName;
+ return this.latestVersion?.name;
},
hasPipelineStatus() {
return this.pipelineStatus?.text;
@@ -58,7 +58,7 @@ export default {
return this.resource.latestVersion;
},
versionBadgeText() {
- return this.latestVersion.tagName;
+ return this.latestVersion.name;
},
webPath() {
return cleanLeadingSeparator(this.resource?.webPath);
@@ -92,7 +92,7 @@ export default {
v-if="hasLatestVersion"
size="sm"
class="gl-ml-3 gl-my-1"
- :href="latestVersion.tagPath"
+ :href="latestVersion.path"
>
{{ versionBadgeText }}
</gl-badge>
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 343b555c4d8..ccef50e469d 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
@@ -50,6 +50,6 @@ export default {
<template>
<div>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="lg" />
- <div v-else v-safe-html="readmeHtml"></div>
+ <div v-else v-safe-html="readmeHtml" class="md"></div>
</div>
</template>
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 3a9ec341789..001e3ec3720 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_header.vue
@@ -1,5 +1,6 @@
<script>
import { GlBanner, GlLink } from '@gitlab/ui';
+import ChatBubbleSvg from '@gitlab/svgs/dist/illustrations/chat-sm.svg?url';
import { __, s__ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
@@ -44,6 +45,7 @@ export default {
learnMore: __('Learn more'),
},
learnMorePath: helpPagePath('ci/components/index'),
+ ChatBubbleSvg,
};
</script>
<template>
@@ -54,6 +56,7 @@ export default {
:title="$options.i18n.banner.title"
:button-text="$options.i18n.banner.btnText"
button-link="https://gitlab.com/gitlab-org/gitlab/-/issues/407556"
+ :svg-path="$options.ChatBubbleSvg"
@close="handleDismissBanner"
>
<p>
diff --git a/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
index e074cfda6f7..1319f204573 100644
--- a/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_search.vue
@@ -1,5 +1,5 @@
<script>
-import { GlSearchBoxByClick, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { GlSearchBoxByClick, GlSorting } from '@gitlab/ui';
import { __ } from '~/locale';
import { SORT_ASC, SORT_DESC, SORT_OPTION_CREATED } from '../../constants';
@@ -7,7 +7,6 @@ export default {
components: {
GlSearchBoxByClick,
GlSorting,
- GlSortingItem,
},
data() {
return {
@@ -25,7 +24,7 @@ export default {
},
currentSortText() {
const currentSort = this.$options.sortOptions.find(
- (sort) => sort.key === this.currentSortOption,
+ (sort) => sort.value === this.currentSortOption,
);
return currentSort.text;
},
@@ -36,9 +35,6 @@ export default {
},
},
methods: {
- isActiveSort(sortItem) {
- return sortItem === this.currentSortOption;
- },
onClear() {
this.$emit('update-search-term', '');
},
@@ -49,10 +45,10 @@ export default {
this.$emit('update-search-term', this.searchTerm);
},
setSelectedSortOption(sortingItem) {
- this.currentSortOption = sortingItem.key;
+ this.currentSortOption = sortingItem;
},
},
- sortOptions: [{ key: SORT_OPTION_CREATED, text: __('Created at') }],
+ sortOptions: [{ value: SORT_OPTION_CREATED, text: __('Created at') }],
};
</script>
<template>
@@ -66,16 +62,10 @@ export default {
<gl-sorting
:is-ascending="isAscending"
:text="currentSortText"
+ :sort-options="$options.sortOptions"
+ :sort-by="currentSortOption"
+ @sortByChange="setSelectedSortOption"
@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/catalog_tabs.vue b/app/assets/javascripts/ci/catalog/components/list/catalog_tabs.vue
new file mode 100644
index 00000000000..f43255ab76b
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/components/list/catalog_tabs.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlBadge, GlTab, GlTabs, GlLoadingIcon } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { SCOPE } from '../../constants';
+
+export default {
+ components: {
+ GlBadge,
+ GlTab,
+ GlTabs,
+ GlLoadingIcon,
+ },
+ props: {
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ resourceCounts: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tabs() {
+ return [
+ {
+ text: s__('CiCatalog|All'),
+ scope: SCOPE.all,
+ testId: 'resources-all-tab',
+ count: this.resourceCounts.all,
+ },
+ {
+ text: s__('CiCatalog|Your resources'),
+ scope: SCOPE.namespaces,
+ testId: 'resources-your-tab',
+ count: this.resourceCounts.namespaces,
+ },
+ ];
+ },
+ showLoadingIcon() {
+ return this.isLoading;
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex align-items-lg-center">
+ <gl-tabs content-class="gl-py-0" class="gl-w-full">
+ <gl-tab
+ v-for="tab in tabs"
+ :key="tab.text"
+ :data-testid="tab.testId"
+ @click="$emit('setScope', tab.scope)"
+ >
+ <template #title>
+ <span>{{ tab.text }}</span>
+ <gl-loading-icon v-if="showLoadingIcon" class="gl-ml-3" />
+
+ <gl-badge v-else size="sm" class="gl-tab-counter-badge">
+ {{ tab.count }}
+ </gl-badge>
+ </template>
+ </gl-tab>
+ </gl-tabs>
+ </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 57d19af614f..42f8cea8727 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
@@ -67,8 +67,8 @@ export default {
releasedAt() {
return getTimeago().format(this.latestVersion?.releasedAt);
},
- tagName() {
- return this.latestVersion?.tagName || this.$options.i18n.unreleased;
+ name() {
+ return this.latestVersion?.name || this.$options.i18n.unreleased;
},
webPath() {
return cleanLeadingSeparator(this.resource?.webPath);
@@ -117,7 +117,7 @@ export default {
<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" class="gl-h-5 gl-align-self-center">{{ tagName }}</gl-badge>
+ <gl-badge size="sm" class="gl-h-5 gl-align-self-center">{{ name }}</gl-badge>
<span class="gl-display-flex gl-align-items-center gl-ml-5">
<span
v-gl-tooltip.top
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 e1c86f38d7e..08500d3093c 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
@@ -3,6 +3,7 @@ import { createAlert } from '~/alert';
import { s__ } from '~/locale';
import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
import CatalogSearch from '../list/catalog_search.vue';
+import CatalogTabs from '../list/catalog_tabs.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';
@@ -10,29 +11,60 @@ 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';
+import getCatalogResourcesCount from '../../graphql/queries/get_ci_catalog_resources_count.query.graphql';
+import { DEFAULT_SORT_VALUE, SCOPE } from '../../constants';
export default {
+ i18n: {
+ fetchError: s__('CiCatalog|There was an error fetching CI/CD Catalog resources.'),
+ countFetchError: s__('CiCatalog|There was an error fetching the CI/CD Catalog resource count.'),
+ },
components: {
CatalogHeader,
CatalogListSkeletonLoader,
CatalogSearch,
+ CatalogTabs,
CiResourcesList,
EmptyState,
},
data() {
return {
catalogResources: [],
+ catalogResourcesCount: { all: 0, namespaces: 0 },
currentPage: 1,
pageInfo: {},
- searchTerm: '',
- totalCount: 0,
+ scope: SCOPE.all,
+ searchTerm: null,
+ sortValue: DEFAULT_SORT_VALUE,
};
},
apollo: {
+ catalogResourcesCount: {
+ query: getCatalogResourcesCount,
+ variables() {
+ return {
+ searchTerm: this.searchTerm,
+ };
+ },
+ update({ namespaces, all }) {
+ return {
+ namespaces: namespaces.count,
+ all: all.count,
+ };
+ },
+ error(e) {
+ createAlert({
+ message: e.message || this.$options.i18n.countFetchError,
+ });
+ },
+ },
catalogResources: {
query: getCatalogResources,
variables() {
return {
+ scope: this.scope,
+ searchTerm: this.searchTerm,
+ sortValue: this.sortValue,
first: ciCatalogResourcesItemsCount,
};
},
@@ -42,10 +74,9 @@ export default {
result({ data }) {
const { pageInfo } = data?.ciCatalogResources || {};
this.pageInfo = pageInfo;
- this.totalCount = data?.ciCatalogResources?.count || 0;
},
error(e) {
- createAlert({ message: e.message || this.$options.i18n.fetchError, variant: 'danger' });
+ createAlert({ message: e.message || this.$options.i18n.fetchError });
},
},
currentPage: {
@@ -62,11 +93,14 @@ export default {
isLoading() {
return this.$apollo.queries.catalogResources.loading;
},
- isSearching() {
- return this.searchTerm?.length > 0;
+ isLoadingCounts() {
+ return this.$apollo.queries.catalogResourcesCount.loading;
+ },
+ namespacesCount() {
+ return this.catalogResourcesCount.namespaces;
},
- showEmptyState() {
- return !this.hasResources && !this.isSearching;
+ currentTabTotalCount() {
+ return this.catalogResourcesCount[this.scope.toLowerCase()];
},
},
methods: {
@@ -103,6 +137,11 @@ export default {
createAlert({ message: e?.message || this.$options.i18n.fetchError, variant: 'danger' });
}
},
+ handleSetScope(scope) {
+ if (this.scope === scope) return;
+
+ this.scope = scope;
+ },
updatePageCount(pageNumber) {
this.$apollo.mutate({
mutation: updateCurrentPageMutation,
@@ -120,30 +159,28 @@ export default {
onUpdateSearchTerm(searchTerm) {
this.searchTerm = !searchTerm.length ? null : searchTerm;
this.resetPageCount();
- this.$apollo.queries.catalogResources.refetch({
- searchTerm: this.searchTerm,
- });
},
onUpdateSorting(sortValue) {
+ this.sortValue = 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.'),
- },
};
</script>
<template>
<div>
<catalog-header />
+ <catalog-tabs
+ :is-loading="isLoadingCounts"
+ :resource-counts="catalogResourcesCount"
+ class="gl-mb-3"
+ @setScope="handleSetScope"
+ />
<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"
+ class="gl-py-2"
@update-search-term="onUpdateSearchTerm"
@update-sorting="onUpdateSorting"
/>
@@ -156,7 +193,7 @@ export default {
:prev-text="__('Prev')"
:next-text="__('Next')"
:resources="catalogResources"
- :total-count="totalCount"
+ :total-count="currentTabTotalCount"
@onPrevPage="handlePrevPage"
@onNextPage="handleNextPage"
/>
diff --git a/app/assets/javascripts/ci/catalog/constants.js b/app/assets/javascripts/ci/catalog/constants.js
index 34c0ac797c1..a180aa84344 100644
--- a/app/assets/javascripts/ci/catalog/constants.js
+++ b/app/assets/javascripts/ci/catalog/constants.js
@@ -2,8 +2,14 @@ import { helpPagePath } from '~/helpers/help_page_helper';
export const CATALOG_FEEDBACK_DISMISSED_KEY = 'catalog_feedback_dismissed';
+export const SCOPE = {
+ all: 'ALL',
+ namespaces: 'NAMESPACES',
+};
+
export const SORT_OPTION_CREATED = 'CREATED';
export const SORT_ASC = 'ASC';
export const SORT_DESC = 'DESC';
+export const DEFAULT_SORT_VALUE = `${SORT_OPTION_CREATED}_${SORT_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 b3a750e9604..316308e96d7 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
@@ -7,8 +7,8 @@ fragment CatalogResourceFields on CiCatalogResource {
starCount
latestVersion {
id
- tagName
- tagPath
+ name
+ path
releasedAt
author {
id
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_create.mutation.graphql b/app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_create.mutation.graphql
index c3b73ebf248..c3b73ebf248 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_create.mutation.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_create.mutation.graphql
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql b/app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_destroy.mutation.graphql
index fa42b081a5f..fa42b081a5f 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/graphql/mutations/catalog_resources_destroy.mutation.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/mutations/catalog_resources_destroy.mutation.graphql
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 41ac72aa9de..bf1edf1af6e 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
@@ -8,7 +8,7 @@ query getCiCatalogResourceComponents($fullPath: ID!) {
nodes {
id
name
- path
+ includePath
inputs {
name
required
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 a77e8f12d03..efc8aa777d4 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
@@ -22,7 +22,7 @@ query getCiCatalogResourceDetails($fullPath: ID!) {
}
}
}
- tagName
+ name
releasedAt
}
}
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 1cf213dec63..24789e9c4ed 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,6 +1,7 @@
#import "~/ci/catalog/graphql/fragments/catalog_resource.fragment.graphql"
query getCatalogResources(
+ $scope: CiCatalogResourceScope
$searchTerm: String
$sortValue: CiCatalogResourceSort
$after: String
@@ -9,6 +10,7 @@ query getCatalogResources(
$last: Int
) {
ciCatalogResources(
+ scope: $scope
search: $searchTerm
sort: $sortValue
after: $after
@@ -22,7 +24,6 @@ query getCatalogResources(
hasNextPage
hasPreviousPage
}
- count
nodes {
...CatalogResourceFields
}
diff --git a/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources_count.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources_count.query.graphql
new file mode 100644
index 00000000000..d4a298e7e09
--- /dev/null
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_resources_count.query.graphql
@@ -0,0 +1,8 @@
+query getCatalogResourcesCount($searchTerm: String) {
+ all: ciCatalogResources(scope: ALL, search: $searchTerm) {
+ count
+ }
+ namespaces: ciCatalogResources(scope: NAMESPACES, search: $searchTerm) {
+ count
+ }
+}
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/graphql/queries/get_ci_catalog_settings.query.graphql b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_settings.query.graphql
index 0de06028386..0de06028386 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/graphql/queries/get_ci_catalog_settings.query.graphql
+++ b/app/assets/javascripts/ci/catalog/graphql/queries/get_ci_catalog_settings.query.graphql
diff --git a/app/assets/javascripts/ci/catalog/graphql/settings.js b/app/assets/javascripts/ci/catalog/graphql/settings.js
index 4038188a7ce..abc95592b14 100644
--- a/app/assets/javascripts/ci/catalog/graphql/settings.js
+++ b/app/assets/javascripts/ci/catalog/graphql/settings.js
@@ -15,7 +15,7 @@ export const cacheConfig = {
});
},
ciCatalogResources: {
- keyArgs: false,
+ keyArgs: ['scope', 'search', 'sort'],
},
},
},
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue b/app/assets/javascripts/ci/ci_environments_dropdown/ci_environments_dropdown.vue
index 77af643cbb3..2d2e3e280c0 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_environments_dropdown.vue
+++ b/app/assets/javascripts/ci/ci_environments_dropdown/ci_environments_dropdown.vue
@@ -3,8 +3,12 @@ import { debounce, uniq } from 'lodash';
import { GlDropdownDivider, GlDropdownItem, GlCollapsibleListbox, GlSprintf } from '@gitlab/ui';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { __, s__, sprintf } from '~/locale';
-import { convertEnvironmentScope } from '../utils';
-import { ENVIRONMENT_QUERY_LIMIT } from '../constants';
+import { convertEnvironmentScope } from './utils';
+import {
+ ALL_ENVIRONMENTS_OPTION,
+ ENVIRONMENT_QUERY_LIMIT,
+ NO_ENVIRONMENT_OPTION,
+} from './constants';
export default {
name: 'CiEnvironmentsDropdown',
@@ -16,10 +20,20 @@ export default {
},
mixins: [glFeatureFlagsMixin()],
props: {
+ isEnvironmentRequired: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
areEnvironmentsLoading: {
type: Boolean,
required: true,
},
+ canCreateWildcard: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
environments: {
type: Array,
required: true,
@@ -51,22 +65,31 @@ export default {
searchedEnvironments() {
let filtered = this.environments;
- // If there is no search term, make sure to include *
- if (!this.searchTerm) {
- filtered = uniq([...filtered, '*']);
- }
-
// add custom env scope if it matches the search term
if (this.customEnvScope && this.customEnvScope.startsWith(this.searchTerm)) {
filtered = uniq([...filtered, this.customEnvScope]);
}
+ // If there is no search term, make sure to include *
+ if (!this.searchTerm) {
+ filtered = uniq([...filtered, ALL_ENVIRONMENTS_OPTION.type]);
+
+ // lastly, add Not Applicable (None) as the first option if isEnvironmentRequired is true
+ if (!this.isEnvironmentRequired) {
+ filtered = [NO_ENVIRONMENT_OPTION.type, ...filtered];
+ }
+ }
+
return filtered.sort().map((environment) => ({
value: environment,
text: environment,
}));
},
shouldRenderCreateButton() {
+ if (!this.canCreateWildcard) {
+ return false;
+ }
+
return (
this.searchTerm && ![...this.environments, this.customEnvScope].includes(this.searchTerm)
);
diff --git a/app/assets/javascripts/ci/ci_environments_dropdown/constants.js b/app/assets/javascripts/ci/ci_environments_dropdown/constants.js
new file mode 100644
index 00000000000..98e543a75d0
--- /dev/null
+++ b/app/assets/javascripts/ci/ci_environments_dropdown/constants.js
@@ -0,0 +1,14 @@
+import { __ } from '~/locale';
+
+export const ENVIRONMENT_QUERY_LIMIT = 30;
+
+export const ALL_ENVIRONMENTS_OPTION = {
+ type: '*',
+ text: __('All (default)'),
+};
+
+export const NO_ENVIRONMENT_OPTION = {
+ // TODO: This is a placeholder value. It will be replaced with the actual value used once it's implemented on the backend
+ type: 'Not applicable',
+ text: __('Not applicable'),
+};
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql b/app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/group_environments.query.graphql
index 5768d370474..5768d370474 100644
--- a/app/assets/javascripts/ci/ci_variable_list/graphql/queries/group_environments.query.graphql
+++ b/app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/group_environments.query.graphql
diff --git a/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql b/app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/project_environments.query.graphql
index 26d1b6a3aaa..26d1b6a3aaa 100644
--- a/app/assets/javascripts/ci/ci_variable_list/graphql/queries/project_environments.query.graphql
+++ b/app/assets/javascripts/ci/ci_environments_dropdown/graphql/queries/project_environments.query.graphql
diff --git a/app/assets/javascripts/ci/ci_variable_list/utils.js b/app/assets/javascripts/ci/ci_environments_dropdown/utils.js
index a7e020206ea..093b04dab0c 100644
--- a/app/assets/javascripts/ci/ci_variable_list/utils.js
+++ b/app/assets/javascripts/ci/ci_environments_dropdown/utils.js
@@ -1,4 +1,4 @@
-import { allEnvironments } from './constants';
+import { ALL_ENVIRONMENTS_OPTION, NO_ENVIRONMENT_OPTION } from './constants';
/**
* This function job is to convert the * wildcard to text when applicable
@@ -10,11 +10,14 @@ import { allEnvironments } from './constants';
*/
export const convertEnvironmentScope = (environmentScope = '') => {
- if (environmentScope === allEnvironments.type || !environmentScope) {
- return allEnvironments.text;
+ switch (environmentScope) {
+ case ALL_ENVIRONMENTS_OPTION.type || '':
+ return ALL_ENVIRONMENTS_OPTION.text;
+ case NO_ENVIRONMENT_OPTION.type:
+ return NO_ENVIRONMENT_OPTION.text;
+ default:
+ return environmentScope;
}
-
- return environmentScope;
};
/**
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
index 842d88e1267..b118d54d2b0 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_group_variables.vue
@@ -2,8 +2,8 @@
import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getGroupEnvironments } from '~/ci/common/private/ci_environments_dropdown';
import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
-import getGroupEnvironments from '../graphql/queries/group_environments.query.graphql';
import getGroupVariables from '../graphql/queries/group_variables.query.graphql';
import addGroupVariable from '../graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '../graphql/mutations/group_delete_variable.mutation.graphql';
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue
index 43938e9b88f..822a2b01f24 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_project_variables.vue
@@ -2,8 +2,8 @@
import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { getProjectEnvironments } from '~/ci/common/private/ci_environments_dropdown';
import { ADD_MUTATION_ACTION, DELETE_MUTATION_ACTION, UPDATE_MUTATION_ACTION } from '../constants';
-import getProjectEnvironments from '../graphql/queries/project_environments.query.graphql';
import getProjectVariables from '../graphql/queries/project_variables.query.graphql';
import addProjectVariable from '../graphql/mutations/project_add_variable.mutation.graphql';
import deleteProjectVariable from '../graphql/mutations/project_delete_variable.mutation.graphql';
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 2ad6c7c6578..ad4b7b790d0 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
@@ -20,8 +20,8 @@ import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import { helpPagePath } from '~/helpers/help_page_helper';
import Tracking from '~/tracking';
+import CiEnvironmentsDropdown from '~/ci/common/private/ci_environments_dropdown';
import {
- allEnvironments,
defaultVariableState,
DRAWER_EVENT_LABEL,
EDIT_VARIABLE_ACTION,
@@ -34,7 +34,6 @@ import {
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 });
@@ -43,9 +42,10 @@ const KEY_REGEX = /^\w+$/;
export const i18n = {
addVariable: s__('CiVariables|Add variable'),
cancel: __('Cancel'),
- defaultScope: allEnvironments.text,
+ defaultScope: __('All (default)'),
deleteVariable: s__('CiVariables|Delete variable'),
editVariable: s__('CiVariables|Edit variable'),
+ saveVariable: __('Save changes'),
environments: __('Environments'),
environmentScopeLinkTitle: ENVIRONMENT_SCOPE_LINK_TITLE,
expandedField: s__('CiVariables|Expand variable reference'),
@@ -259,9 +259,12 @@ export default {
return validationIssuesText.trim();
},
- modalActionText() {
+ modalTitle() {
return this.isEditing ? this.$options.i18n.editVariable : this.$options.i18n.addVariable;
},
+ modalActionText() {
+ return this.isEditing ? this.$options.i18n.saveVariable : this.$options.i18n.addVariable;
+ },
removeVariableMessage() {
return sprintf(this.$options.i18n.modalDeleteMessage, { key: this.variable.key });
},
@@ -359,7 +362,7 @@ export default {
@close="close"
>
<template #title>
- <h2 class="gl-m-0">{{ modalActionText }}</h2>
+ <h2 class="gl-m-0">{{ modalTitle }}</h2>
</template>
<gl-form-group
:label="$options.i18n.type"
@@ -493,8 +496,8 @@ export default {
v-model="variable.value"
:spellcheck="false"
class="gl-border-none gl-font-monospace!"
- rows="3"
- max-rows="10"
+ rows="5"
+ :no-resize="false"
data-testid="ci-variable-value"
/>
<p
@@ -515,9 +518,15 @@ export default {
>
{{ $options.i18n.variableReferenceDescription }}
</gl-alert>
- <div class="gl-display-flex gl-justify-content-end">
- <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
- >{{ $options.i18n.cancel }}
+ <div class="gl-display-flex">
+ <gl-button
+ category="primary"
+ class="gl-mr-3"
+ variant="confirm"
+ :disabled="!canSubmit"
+ data-testid="ci-variable-confirm-button"
+ @click="submit"
+ >{{ modalActionText }}
</gl-button>
<gl-button
v-if="isEditing"
@@ -528,13 +537,8 @@ export default {
data-testid="ci-variable-delete-button"
>{{ $options.i18n.deleteVariable }}</gl-button
>
- <gl-button
- category="primary"
- variant="confirm"
- :disabled="!canSubmit"
- data-testid="ci-variable-confirm-button"
- @click="submit"
- >{{ modalActionText }}
+ <gl-button category="secondary" class="gl-mr-3" data-testid="cancel-button" @click="close"
+ >{{ $options.i18n.cancel }}
</gl-button>
</div>
</gl-drawer>
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
index 011a424b6c2..609b8523612 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_shared.vue
@@ -3,11 +3,13 @@ import { createAlert } from '~/alert';
import { __ } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { reportToSentry } from '~/ci/utils';
-import { mapEnvironmentNames } from '../utils';
+import {
+ ENVIRONMENT_QUERY_LIMIT,
+ mapEnvironmentNames,
+} from '~/ci/common/private/ci_environments_dropdown';
import {
ADD_MUTATION_ACTION,
DELETE_MUTATION_ACTION,
- ENVIRONMENT_QUERY_LIMIT,
SORT_DIRECTIONS,
UPDATE_MUTATION_ACTION,
mapMutationActionToToast,
diff --git a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
index 86287d586ec..901bd39930a 100644
--- a/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
+++ b/app/assets/javascripts/ci/ci_variable_list/components/ci_variable_table.vue
@@ -15,13 +15,13 @@ import {
} from '@gitlab/ui';
import { __, s__, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { convertEnvironmentScope } from '~/ci/common/private/ci_environments_dropdown';
import {
DEFAULT_EXCEEDS_VARIABLE_LIMIT_TEXT,
EXCEEDS_VARIABLE_LIMIT_TEXT,
MAXIMUM_VARIABLE_LIMIT_REACHED,
variableTypes,
} from '../constants';
-import { convertEnvironmentScope } from '../utils';
export default {
defaultFields: [
diff --git a/app/assets/javascripts/ci/ci_variable_list/constants.js b/app/assets/javascripts/ci/ci_variable_list/constants.js
index 4ec7333f465..c4f92fed829 100644
--- a/app/assets/javascripts/ci/ci_variable_list/constants.js
+++ b/app/assets/javascripts/ci/ci_variable_list/constants.js
@@ -1,7 +1,5 @@
import { __, s__, sprintf } from '~/locale';
-export const ENVIRONMENT_QUERY_LIMIT = 30;
-
export const MASKED_VALUE_MIN_LENGTH = 8;
export const WHITESPACE_REG_EX = /\s/;
@@ -15,18 +13,13 @@ export const variableTypes = {
fileType: 'FILE',
};
-export const allEnvironments = {
- type: '*',
- text: __('All (default)'),
-};
-
export const variableOptions = [
{ value: variableTypes.envType, text: __('Variable (default)') },
{ value: variableTypes.fileType, text: __('File') },
];
export const defaultVariableState = {
- environmentScope: allEnvironments.type,
+ environmentScope: '*',
key: '',
masked: false,
protected: false,
diff --git a/app/assets/javascripts/ci/common/private/ci_environments_dropdown.js b/app/assets/javascripts/ci/common/private/ci_environments_dropdown.js
new file mode 100644
index 00000000000..f8958f9600c
--- /dev/null
+++ b/app/assets/javascripts/ci/common/private/ci_environments_dropdown.js
@@ -0,0 +1,9 @@
+import CiEnvironmentsDropdown from '~/ci/ci_environments_dropdown/ci_environments_dropdown.vue';
+
+export default CiEnvironmentsDropdown;
+
+export { getGroupEnvironments } from '~/ci/ci_environments_dropdown/graphql/queries/group_environments.query.graphql';
+export { getProjectEnvironments } from '~/ci/ci_environments_dropdown/graphql/queries/project_environments.query.graphql';
+
+export { ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_environments_dropdown/constants';
+export * from '~/ci/ci_environments_dropdown/utils';
diff --git a/app/assets/javascripts/ci/pipeline_details/constants.js b/app/assets/javascripts/ci/pipeline_details/constants.js
index 51d0e980e78..90e723ea442 100644
--- a/app/assets/javascripts/ci/pipeline_details/constants.js
+++ b/app/assets/javascripts/ci/pipeline_details/constants.js
@@ -5,7 +5,7 @@ export const SUPPORTED_FILTER_PARAMETERS = ['username', 'ref', 'status', 'source
export const NEEDS_PROPERTY = 'needs';
export const EXPLICIT_NEEDS_PROPERTY = 'previousStageJobsOrNeeds';
-export const TestStatus = {
+export const testStatus = {
FAILED: 'failed',
SKIPPED: 'skipped',
SUCCESS: 'success',
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 e3984685094..87081e61e48 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,6 +1,6 @@
import { __, sprintf } from '~/locale';
import { parseSeconds, stringifyTime } from '~/lib/utils/datetime_utility';
-import { TestStatus } from '../../constants';
+import { testStatus } from '../../constants';
/**
* Removes `./` from the beginning of a file path so it can be appended onto a blob path
@@ -13,15 +13,15 @@ export function formatFilePath(file) {
export function iconForTestStatus(status) {
switch (status) {
- case TestStatus.SUCCESS:
+ case testStatus.SUCCESS:
return 'status_success';
- case TestStatus.FAILED:
+ case testStatus.FAILED:
return 'status_failed';
- case TestStatus.ERROR:
+ case testStatus.ERROR:
return 'status_warning';
- case TestStatus.SKIPPED:
+ case testStatus.SKIPPED:
return 'status_skipped';
- case TestStatus.UNKNOWN:
+ case testStatus.UNKNOWN:
default:
return 'status_notfound';
}
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 6e9a705c046..5fd9f7cfd4f 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,7 +2,12 @@
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 {
+ getParameterValues,
+ updateHistory,
+ setUrlParams,
+ removeParams,
+} from '~/lib/utils/url_utility';
import EmptyState from './empty_state.vue';
import TestSuiteTable from './test_suite_table.vue';
import TestSummary from './test_summary.vue';
@@ -49,12 +54,28 @@ export default {
]),
summaryBackClick() {
this.removeSelectedSuiteIndex();
+
+ updateHistory({
+ url: removeParams(['job_name']),
+ title: document.title,
+ replace: true,
+ });
},
summaryTableRowClick(index) {
this.setSelectedSuiteIndex(index);
// Fetch the test suite when the user clicks to see more details
this.fetchTestSuite(index);
+
+ const urlParams = {
+ job_name: this.getSelectedSuite.name,
+ };
+
+ updateHistory({
+ url: setUrlParams(urlParams),
+ title: document.title,
+ replace: true,
+ });
},
beforeEnterTransition() {
document.documentElement.style.overflowX = 'hidden';
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 1d152a63407..a6e679e6d4e 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
@@ -13,6 +13,7 @@ import {
} from '@gitlab/ui';
import { s__, __ } from '~/locale';
import Tracking from '~/tracking';
+import { helpPagePath } from '~/helpers/help_page_helper';
import { pipelineEditorTrackingOptions } from '../../constants';
import ValidatePipelinePopover from '../popovers/validate_pipeline_popover.vue';
import CiLintResults from '../lint/ci_lint_results.vue';
@@ -189,6 +190,7 @@ export default {
},
i18n,
BASE_CLASSES,
+ lintHref: helpPagePath('ci/lint.md'),
};
</script>
@@ -290,7 +292,7 @@ export default {
<code>{{ content }}</code>
</template>
<template #link="{ content }">
- <gl-link target="_blank" href="#">{{ content }}</gl-link>
+ <gl-link target="_blank" :href="$options.lintHref">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
diff --git a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
index e287e4e17d1..63957d9b7fc 100644
--- a/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
+++ b/app/assets/javascripts/ci/runner/components/cells/runner_status_cell.vue
@@ -33,16 +33,13 @@ export default {
</script>
<template>
- <div>
+ <div class="gl-display-flex gl-flex-wrap gl-gap-2">
<runner-status-badge
:contacted-at="contactedAt"
:status="status"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
- />
- <runner-paused-badge
- v-if="paused"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate"
+ class="gl-max-w-full gl-text-truncate"
/>
+ <runner-paused-badge v-if="paused" class="gl-max-w-full gl-text-truncate" />
<slot :runner="runner" name="runner-job-status-badge"></slot>
</div>
</template>
diff --git a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
index bed592e3f30..0dc23882cdc 100644
--- a/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
+++ b/app/assets/javascripts/ci/runner/components/runner_job_status_badge.vue
@@ -26,12 +26,12 @@ export default {
switch (this.jobStatus) {
case JOB_STATUS_RUNNING:
return {
- classes: 'gl-text-blue-600! gl-border gl-border-blue-600!',
+ classes: 'gl-text-blue-600! gl-inset-border-1-gray-400 gl-border-blue-600!',
label: I18N_JOB_STATUS_RUNNING,
};
case JOB_STATUS_IDLE:
return {
- classes: 'gl-text-gray-700! gl-border gl-border-gray-500!',
+ classes: 'gl-text-gray-700! gl-inset-border-1-gray-400 gl-border-gray-500!',
label: I18N_JOB_STATUS_IDLE,
};
default:
@@ -45,7 +45,7 @@ export default {
<gl-badge
v-if="badge"
v-bind="$attrs"
- class="gl-display-inline-block gl-max-w-full gl-text-truncate gl-bg-transparent!"
+ class="gl-max-w-full gl-text-truncate gl-bg-transparent!"
variant="muted"
:class="badge.classes"
>
diff --git a/app/assets/javascripts/clusters/agents/components/show.vue b/app/assets/javascripts/clusters/agents/components/show.vue
index 9d7d68ee31c..c1a6f7e0800 100644
--- a/app/assets/javascripts/clusters/agents/components/show.vue
+++ b/app/assets/javascripts/clusters/agents/components/show.vue
@@ -161,6 +161,8 @@ export default {
</div>
</div>
</gl-tab>
+
+ <slot name="ee-workspaces-tab" :agent-name="agentName" :project-path="projectPath"></slot>
</gl-tabs>
</template>
diff --git a/app/assets/javascripts/comment_templates/components/form.vue b/app/assets/javascripts/comment_templates/components/form.vue
index 5a5d221591a..0e92f20f0c1 100644
--- a/app/assets/javascripts/comment_templates/components/form.vue
+++ b/app/assets/javascripts/comment_templates/components/form.vue
@@ -1,11 +1,12 @@
<!-- eslint-disable vue/multi-word-component-names -->
<script>
import { GlButton, GlForm, GlFormGroup, GlFormInput, GlAlert } from '@gitlab/ui';
-import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import { helpPagePath } from '~/helpers/help_page_helper';
import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import { InternalEvents } from '~/tracking';
+import Api from '~/api';
import createSavedReplyMutation from '../queries/create_saved_reply.mutation.graphql';
import updateSavedReplyMutation from '../queries/update_saved_reply.mutation.graphql';
@@ -16,7 +17,7 @@ export default {
GlFormGroup,
GlFormInput,
GlAlert,
- MarkdownField,
+ MarkdownEditor,
},
mixins: [InternalEvents.mixin()],
props: {
@@ -45,6 +46,14 @@ export default {
name: this.name,
content: this.content,
},
+ formFieldProps: {
+ id: 'comment-template-content',
+ name: 'comment-template-content',
+ 'aria-label': __('Content'),
+ placeholder: __('Write comment template content here…'),
+ 'data-testid': 'comment-template-content-input',
+ class: 'note-textarea js-gfm-input js-autosize markdown-area',
+ },
};
},
computed: {
@@ -61,6 +70,9 @@ export default {
isValid() {
return this.isNameValid && this.isContentValid;
},
+ markdownPath() {
+ return Api.buildUrl(Api.markdownPath);
+ },
},
methods: {
onCancel() {
@@ -116,7 +128,6 @@ export default {
});
},
},
- restrictedToolbarItems: ['full-screen'],
markdownDocsPath: helpPagePath('user/markdown'),
};
</script>
@@ -156,30 +167,19 @@ export default {
data-testid="comment-template-content-form-group"
class="gl-lg-max-w-80p"
>
- <markdown-field
- :enable-preview="false"
+ <markdown-editor
+ v-model="updateCommentTemplate.content"
+ class="js-no-autosize"
:is-submitting="saving"
- :add-spacing-classes="false"
- :textarea-value="updateCommentTemplate.content"
+ :disable-attachments="true"
+ :render-markdown-path="markdownPath"
:markdown-docs-path="$options.markdownDocsPath"
+ :form-field-props="formFieldProps"
:restricted-tool-bar-items="$options.restrictedToolbarItems"
:force-autosize="false"
- class="js-no-autosize gl-border-gray-400!"
- >
- <template #textarea>
- <textarea
- v-model="updateCommentTemplate.content"
- dir="auto"
- class="note-textarea js-gfm-input js-autosize markdown-area"
- data-supports-quick-actions="false"
- :aria-label="__('Content')"
- :placeholder="__('Write comment template content here…')"
- data-testid="comment-template-content-input"
- @keydown.meta.enter="onSubmit"
- @keydown.ctrl.enter="onSubmit"
- ></textarea>
- </template>
- </markdown-field>
+ @keydown.meta.enter="onSubmit"
+ @keydown.ctrl.enter="onSubmit"
+ />
</gl-form-group>
<gl-button
variant="confirm"
diff --git a/app/assets/javascripts/commit/components/signature_badge.vue b/app/assets/javascripts/commit/components/signature_badge.vue
index edc7c9d2f96..dc6d2df22b7 100644
--- a/app/assets/javascripts/commit/components/signature_badge.vue
+++ b/app/assets/javascripts/commit/components/signature_badge.vue
@@ -1,7 +1,7 @@
<script>
import { GlBadge, GlLink, GlPopover } from '@gitlab/ui';
import { helpPagePath } from '~/helpers/help_page_helper';
-import { typeConfig, statusConfig } from '../constants';
+import { typeConfig, statusConfig } from 'ee_else_ce/commit/constants';
import X509CertificateDetails from './x509_certificate_details.vue';
export default {
diff --git a/app/assets/javascripts/commit/constants.js b/app/assets/javascripts/commit/constants.js
index e28009ab996..9eba316b371 100644
--- a/app/assets/javascripts/commit/constants.js
+++ b/app/assets/javascripts/commit/constants.js
@@ -11,6 +11,7 @@ export const verificationStatuses = {
SAME_USER_DIFFERENT_EMAIL: 'SAME_USER_DIFFERENT_EMAIL',
MULTIPLE_SIGNATURES: 'MULTIPLE_SIGNATURES',
REVOKED_KEY: 'REVOKED_KEY',
+ VERIFIED_SYSTEM: 'VERIFIED_SYSTEM',
};
export const signatureTypes = {
@@ -28,15 +29,25 @@ const UNVERIFIED_CONFIG = {
description: __('This commit was signed with an unverified signature.'),
};
+export const VERIFIED_CONFIG = {
+ variant: 'success',
+ label: __('Verified'),
+ title: __('Verified commit'),
+};
+
export const statusConfig = {
[verificationStatuses.VERIFIED]: {
- variant: 'success',
- label: __('Verified'),
- title: __('Verified commit'),
+ ...VERIFIED_CONFIG,
description: __(
'This commit was signed with a verified signature and the committer email was verified to belong to the same user.',
),
},
+ [verificationStatuses.VERIFIED_SYSTEM]: {
+ ...VERIFIED_CONFIG,
+ description: __(
+ 'This commit was created in the GitLab UI, and signed with a GitLab-verified signature.',
+ ),
+ },
[verificationStatuses.UNVERIFIED]: {
...UNVERIFIED_CONFIG,
},
diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
index 0b80802d993..b7031a4885c 100644
--- a/app/assets/javascripts/content_editor/components/wrappers/image.vue
+++ b/app/assets/javascripts/content_editor/components/wrappers/image.vue
@@ -1,5 +1,6 @@
<script>
import { NodeViewWrapper } from '@tiptap/vue-2';
+import { uploadingStates } from '../../services/upload_helpers';
export default {
name: 'ImageWrapper',
@@ -30,6 +31,12 @@ export default {
dragData: {},
};
},
+ computed: {
+ isStaleUploadedImage() {
+ const { uploading } = this.node.attrs;
+ return uploading && uploadingStates[uploading];
+ },
+ },
mounted() {
document.addEventListener('mousemove', this.onDrag);
document.addEventListener('mouseup', this.onDragEnd);
@@ -80,7 +87,11 @@ export default {
};
</script>
<template>
- <node-view-wrapper as="span" class="gl-relative gl-display-inline-block">
+ <node-view-wrapper
+ v-if="!isStaleUploadedImage"
+ as="span"
+ class="gl-relative gl-display-inline-block"
+ >
<span
v-for="handle in $options.resizeHandles"
v-show="selected"
diff --git a/app/assets/javascripts/content_editor/components/wrappers/playable.vue b/app/assets/javascripts/content_editor/components/wrappers/playable.vue
new file mode 100644
index 00000000000..8a380f23a3f
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/playable.vue
@@ -0,0 +1,62 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { NodeViewWrapper, NodeViewContent } from '@tiptap/vue-2';
+import { uploadingStates } from '../../services/upload_helpers';
+
+export default {
+ name: 'PlayableWrapper',
+ components: {
+ NodeViewWrapper,
+ NodeViewContent,
+ GlLink,
+ },
+ props: {
+ getPos: {
+ type: Function,
+ required: true,
+ },
+ editor: {
+ type: Object,
+ required: true,
+ },
+ node: {
+ type: Object,
+ required: true,
+ },
+ selected: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ dragData: {},
+ };
+ },
+ computed: {
+ isStaleUploadedMedia() {
+ const { uploading } = this.node.attrs;
+ return uploading && uploadingStates[uploading];
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper
+ v-if="!isStaleUploadedMedia"
+ as="span"
+ :class="`media-container ${node.type.name}-container`"
+ >
+ <node-view-content
+ :as="node.type.name"
+ :src="node.attrs.src"
+ controls="true"
+ data-setup="{}"
+ :data-title="node.attrs.title || node.attrs.alt"
+ />
+ <gl-link :href="node.attrs.src" class="with-attachment-icon" target="_blank">
+ {{ node.attrs.title || node.attrs.alt }}
+ </gl-link>
+ </node-view-wrapper>
+</template>
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 e7a1b058341..11ac024b799 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
@@ -7,7 +7,7 @@ import { __, n__ } from '~/locale';
const TABLE_CELL_HEADER = 'th';
const TABLE_CELL_BODY = 'td';
-function getDropdownItems({ selectedRect, cellType, rowspan = 1, colspan = 1 }) {
+function getDropdownItems({ selectedRect, cellType, rowspan = 1, colspan = 1, align = 'left' }) {
const totalRows = selectedRect?.map.height;
const totalCols = selectedRect?.map.width;
const isTableBodyCell = cellType === TABLE_CELL_BODY;
@@ -20,9 +20,21 @@ function getDropdownItems({ selectedRect, cellType, rowspan = 1, colspan = 1 })
const showDeleteRowOption = totalRows > selectedRows + 1 && isTableBodyCell;
const showDeleteColumnOption = totalCols > selectedCols;
+ const isTableBodyHeader = cellType === TABLE_CELL_HEADER;
+ const showAlignLeftOption = isTableBodyHeader && (align === 'center' || align === 'right');
+ const showAlignCenterOption = isTableBodyHeader && align !== 'center';
+ const showAlignRightOption = isTableBodyHeader && align !== 'right';
+
return [
{
items: [
+ showAlignLeftOption && { text: __('Align column left'), value: 'alignColumnLeft' },
+ showAlignCenterOption && { text: __('Align column center'), value: 'alignColumnCenter' },
+ showAlignRightOption && { text: __('Align column right'), value: 'alignColumnRight' },
+ ].filter(Boolean),
+ },
+ {
+ items: [
{ text: __('Insert column before'), value: 'addColumnBefore' },
{ text: __('Insert column after'), value: 'addColumnAfter' },
isTableBodyCell && { text: __('Insert row before'), value: 'addRowBefore' },
@@ -93,6 +105,7 @@ export default {
cellType: this.cellType,
rowspan: this.node.attrs.rowspan,
colspan: this.node.attrs.colspan,
+ align: this.node.attrs.align,
});
},
},
@@ -129,7 +142,7 @@ export default {
runCommand({ value: command }) {
this.hideDropdown();
- this.editor.chain()[command]().run();
+ this.editor.chain()[command](this.getPos()).run();
},
hideDropdown() {
@@ -143,6 +156,7 @@ export default {
:as="cellType"
:rowspan="node.attrs.rowspan || 1"
:colspan="node.attrs.colspan || 1"
+ :align="node.attrs.align || 'left'"
dir="auto"
class="gl-m-0! gl-p-0! gl-relative"
@click="hideDropdown"
@@ -168,6 +182,10 @@ export default {
@action="runCommand"
/>
</span>
- <node-view-content as="div" class="gl-p-5 gl-min-w-10" />
+ <node-view-content
+ as="div"
+ class="gl-p-5 gl-min-w-10"
+ :style="{ 'text-align': node.attrs.align || 'left' }"
+ />
</node-view-wrapper>
</template>
diff --git a/app/assets/javascripts/content_editor/extensions/copy_paste.js b/app/assets/javascripts/content_editor/extensions/copy_paste.js
index 23f2da7bc28..1164e7ead33 100644
--- a/app/assets/javascripts/content_editor/extensions/copy_paste.js
+++ b/app/assets/javascripts/content_editor/extensions/copy_paste.js
@@ -172,6 +172,8 @@ export default Extension.create({
return true;
}
+ if (!textContent) return false;
+
const hasHTML = clipboardData.types.some((type) => type === HTML_FORMAT);
const hasVsCode = clipboardData.types.some((type) => type === VS_CODE_FORMAT);
const vsCodeMeta = hasVsCode ? JSON.parse(clipboardData.getData(VS_CODE_FORMAT)) : {};
diff --git a/app/assets/javascripts/content_editor/extensions/playable.js b/app/assets/javascripts/content_editor/extensions/playable.js
index 47766c966a1..7726e0b6572 100644
--- a/app/assets/javascripts/content_editor/extensions/playable.js
+++ b/app/assets/javascripts/content_editor/extensions/playable.js
@@ -1,4 +1,6 @@
import { Node } from '@tiptap/core';
+import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import PlayableWrapper from '../components/wrappers/playable.vue';
const queryPlayableElement = (element, mediaType) => element.querySelector(mediaType);
@@ -58,7 +60,6 @@ export default Node.create({
controls: true,
'data-setup': '{}',
'data-title': node.attrs.alt,
- ...this.extraElementAttrs,
},
],
[
@@ -68,4 +69,8 @@ export default Node.create({
],
];
},
+
+ addNodeView() {
+ return VueNodeViewRenderer(PlayableWrapper);
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/table_cell.js b/app/assets/javascripts/content_editor/extensions/table_cell.js
index 9f437ce066c..53dba4fd960 100644
--- a/app/assets/javascripts/content_editor/extensions/table_cell.js
+++ b/app/assets/javascripts/content_editor/extensions/table_cell.js
@@ -5,6 +5,17 @@ import TableCellBodyWrapper from '../components/wrappers/table_cell_body.vue';
export default TableCell.extend({
content: 'block+',
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ align: {
+ default: 'left',
+ parseHTML: (element) => element.getAttribute('align') || element.style.textAlign || 'left',
+ renderHTML: () => '',
+ },
+ };
+ },
+
addNodeView() {
return VueNodeViewRenderer(TableCellBodyWrapper);
},
diff --git a/app/assets/javascripts/content_editor/extensions/table_header.js b/app/assets/javascripts/content_editor/extensions/table_header.js
index 045fd03199b..ca2a0eb5cfd 100644
--- a/app/assets/javascripts/content_editor/extensions/table_header.js
+++ b/app/assets/javascripts/content_editor/extensions/table_header.js
@@ -1,9 +1,45 @@
import { TableHeader } from '@tiptap/extension-table-header';
import { VueNodeViewRenderer } from '@tiptap/vue-2';
+import { CellSelection } from '@tiptap/pm/tables';
import TableCellHeaderWrapper from '../components/wrappers/table_cell_header.vue';
export default TableHeader.extend({
content: 'block+',
+
+ addAttributes() {
+ return {
+ ...this.parent?.(),
+ align: {
+ default: 'left',
+ parseHTML: (element) => element.getAttribute('align') || element.style.textAlign || 'left',
+ renderHTML: () => '',
+ },
+ };
+ },
+
+ addCommands() {
+ return {
+ ...this.parent?.(),
+ alignColumn: (pos, align) => ({ commands }) => {
+ commands.selectColumn(pos);
+ commands.updateAttributes('tableHeader', { align });
+ commands.updateAttributes('tableCell', { align });
+ },
+ alignColumnLeft: (pos) => ({ commands }) => commands.alignColumn(pos, 'left'),
+ alignColumnCenter: (pos) => ({ commands }) => commands.alignColumn(pos, 'center'),
+ alignColumnRight: (pos) => ({ commands }) => commands.alignColumn(pos, 'right'),
+ selectColumn: (pos) => ({ tr, dispatch }) => {
+ if (dispatch) {
+ const position = tr.doc.resolve(pos);
+ const colSelection = CellSelection.colSelection(position);
+ tr.setSelection(colSelection);
+ }
+
+ return true;
+ },
+ };
+ },
+
addNodeView() {
return VueNodeViewRenderer(TableCellHeaderWrapper);
},
diff --git a/app/assets/javascripts/content_editor/extensions/task_item.js b/app/assets/javascripts/content_editor/extensions/task_item.js
index 849fd55034e..1e19878be9b 100644
--- a/app/assets/javascripts/content_editor/extensions/task_item.js
+++ b/app/assets/javascripts/content_editor/extensions/task_item.js
@@ -19,9 +19,17 @@ export default TaskItem.extend({
return checkbox?.checked;
},
- renderHTML: (attributes) => ({
- 'data-checked': attributes.checked,
- }),
+ renderHTML: (attributes) => attributes.checked && { 'data-checked': true },
+ keepOnSplit: false,
+ },
+ inapplicable: {
+ default: false,
+ parseHTML: (element) => {
+ const checkbox = element.querySelector('input[type=checkbox].task-list-item-checkbox');
+
+ return typeof checkbox?.dataset.inapplicable !== 'undefined';
+ },
+ renderHTML: (attributes) => attributes.inapplicable && { 'data-inapplicable': true },
keepOnSplit: false,
},
};
@@ -33,6 +41,24 @@ export default TaskItem.extend({
tag: 'li.task-list-item',
priority: PARSE_HTML_PRIORITY_HIGHEST,
},
+ {
+ tag: 'li.task-list-item.inapplicable s',
+ skip: true,
+ priority: PARSE_HTML_PRIORITY_HIGHEST,
+ },
];
},
+
+ addNodeView() {
+ const nodeView = this.parent?.();
+ return ({ node, ...args }) => {
+ const nodeViewInstance = nodeView({ node, ...args });
+
+ if (node.attrs.inapplicable) {
+ nodeViewInstance.dom.querySelector('input[type=checkbox]').disabled = true;
+ }
+
+ return nodeViewInstance;
+ };
+ },
});
diff --git a/app/assets/javascripts/content_editor/extensions/video.js b/app/assets/javascripts/content_editor/extensions/video.js
index 312e8cd5ff6..fa9fa866137 100644
--- a/app/assets/javascripts/content_editor/extensions/video.js
+++ b/app/assets/javascripts/content_editor/extensions/video.js
@@ -6,7 +6,6 @@ export default Playable.extend({
return {
...this.parent?.(),
mediaType: 'video',
- extraElementAttrs: { width: '400' },
};
},
});
diff --git a/app/assets/javascripts/content_editor/services/markdown_serializer.js b/app/assets/javascripts/content_editor/services/markdown_serializer.js
index 972b4acf523..3b759de57f2 100644
--- a/app/assets/javascripts/content_editor/services/markdown_serializer.js
+++ b/app/assets/javascripts/content_editor/services/markdown_serializer.js
@@ -228,7 +228,11 @@ const defaultSerializerConfig = {
[TableHeader.name]: renderTableCell,
[TableRow.name]: renderTableRow,
[TaskItem.name]: preserveUnchanged((state, node) => {
- state.write(`[${node.attrs.checked ? 'x' : ' '}] `);
+ let symbol = ' ';
+ if (node.attrs.inapplicable) symbol = '~';
+ else if (node.attrs.checked) symbol = 'x';
+
+ state.write(`[${symbol}] `);
if (!node.textContent) state.write('&nbsp;');
state.renderContent(node);
}),
diff --git a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
index a4abb8dcf38..de230c370b1 100644
--- a/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
+++ b/app/assets/javascripts/content_editor/services/markdown_sourcemap.js
@@ -24,19 +24,23 @@ const getRangeFromSourcePos = (sourcePos) => {
export const getMarkdownSource = (element) => {
if (!element.dataset.sourcepos) return undefined;
- const source = getFullSource(element);
- const range = getRangeFromSourcePos(element.dataset.sourcepos);
- let elSource = '';
-
- if (!source.length) return undefined;
-
- 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 {
- elSource += `\n${source[i]}` || '';
+ try {
+ const source = getFullSource(element);
+ const range = getRangeFromSourcePos(element.dataset.sourcepos);
+ let elSource = '';
+
+ if (!source.length) return undefined;
+
+ 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 {
+ elSource += `\n${source[i]}` || '';
+ }
}
- }
- return elSource.trim();
+ return elSource.trim();
+ } catch {
+ return undefined;
+ }
};
diff --git a/app/assets/javascripts/content_editor/services/serialization_helpers.js b/app/assets/javascripts/content_editor/services/serialization_helpers.js
index 87959a44560..7a2fbf8fcab 100644
--- a/app/assets/javascripts/content_editor/services/serialization_helpers.js
+++ b/app/assets/javascripts/content_editor/services/serialization_helpers.js
@@ -2,8 +2,8 @@ import { uniq, isString, omit, isFunction } from 'lodash';
import { removeLastSlashInUrlPath, removeUrlProtocol } from '../../lib/utils/url_utility';
const defaultAttrs = {
- td: { colspan: 1, rowspan: 1, colwidth: null },
- th: { colspan: 1, rowspan: 1, colwidth: null },
+ td: { colspan: 1, rowspan: 1, colwidth: null, align: 'left' },
+ th: { colspan: 1, rowspan: 1, colwidth: null, align: 'left' },
};
const defaultIgnoreAttrs = ['sourceMarkdown', 'sourceMapKey'];
@@ -649,6 +649,9 @@ export const link = {
const { canonicalSrc, href, title, sourceMarkdown } = mark.attrs;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (href.startsWith('data:') || href.startsWith('blob:')) return '';
+
if (linkType(sourceMarkdown) === LINK_MARKDOWN) {
return '[';
}
@@ -668,6 +671,9 @@ export const link = {
const { canonicalSrc, href, title, sourceMarkdown, isReference } = mark.attrs;
+ // eslint-disable-next-line @gitlab/require-i18n-strings
+ if (href.startsWith('data:') || href.startsWith('blob:')) return '';
+
if (isReference) {
return `][${state.esc(canonicalSrc || href || '')}]`;
}
diff --git a/app/assets/javascripts/content_editor/services/upload_helpers.js b/app/assets/javascripts/content_editor/services/upload_helpers.js
index f5785397bf0..960f28747b0 100644
--- a/app/assets/javascripts/content_editor/services/upload_helpers.js
+++ b/app/assets/javascripts/content_editor/services/upload_helpers.js
@@ -133,6 +133,8 @@ export const uploadFile = ({ uploadsPath, renderMarkdown, file }) => {
});
};
+export const uploadingStates = {};
+
const uploadMedia = async ({ type, editor, file, uploadsPath, renderMarkdown, eventHub }) => {
// needed to avoid mismatched transaction error
await Promise.resolve();
@@ -170,6 +172,8 @@ const uploadMedia = async ({ type, editor, file, uploadsPath, renderMarkdown, ev
// the position might have changed while uploading, so we need to find it again
position = findUploadedFilePosition(editor, file.name);
+ uploadingStates[file.name] = true;
+
editor.view.dispatch(
editor.state.tr.setMeta('preventAutolink', true).setNodeMarkup(position, undefined, {
uploading: false,
@@ -200,6 +204,8 @@ const uploadAttachment = async ({ editor, file, uploadsPath, renderMarkdown, eve
const { selection } = editor.view.state;
const currentNode = selection.$to.node();
+ uploadingStates[file.name] = true;
+
let position = selection.to;
let content = {
type: 'text',
diff --git a/app/assets/javascripts/deploy_keys/components/action_btn.vue b/app/assets/javascripts/deploy_keys/components/action_btn.vue
index 7bc1eb5d652..4a2da487e9b 100644
--- a/app/assets/javascripts/deploy_keys/components/action_btn.vue
+++ b/app/assets/javascripts/deploy_keys/components/action_btn.vue
@@ -1,6 +1,5 @@
<script>
import { GlButton } from '@gitlab/ui';
-import eventHub from '../eventhub';
export default {
components: {
@@ -11,10 +10,6 @@ export default {
type: Object,
required: true,
},
- type: {
- type: String,
- required: true,
- },
category: {
type: String,
required: false,
@@ -30,6 +25,10 @@ export default {
required: false,
default: '',
},
+ mutation: {
+ type: Object,
+ required: true,
+ },
},
data() {
return {
@@ -39,10 +38,15 @@ export default {
methods: {
doAction() {
this.isLoading = true;
-
- eventHub.$emit(`${this.type}.key`, this.deployKey, () => {
- this.isLoading = false;
- });
+ this.$apollo
+ .mutate({
+ mutation: this.mutation,
+ variables: { id: this.deployKey.id },
+ })
+ .catch((error) => this.$emit('error', error))
+ .finally(() => {
+ this.isLoading = false;
+ });
},
},
};
@@ -50,6 +54,7 @@ export default {
<template>
<gl-button
+ v-bind="$attrs"
:category="category"
:variant="variant"
:icon="icon"
diff --git a/app/assets/javascripts/deploy_keys/components/app.vue b/app/assets/javascripts/deploy_keys/components/app.vue
index ec17bbea48f..7168a209b52 100644
--- a/app/assets/javascripts/deploy_keys/components/app.vue
+++ b/app/assets/javascripts/deploy_keys/components/app.vue
@@ -1,11 +1,18 @@
<script>
-import { GlButton, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlLoadingIcon, GlPagination } from '@gitlab/ui';
import { createAlert } from '~/alert';
-import { s__ } from '~/locale';
+import { s__, __, sprintf } from '~/locale';
+import { captureException } from '~/sentry/sentry_browser_wrapper';
+import pageInfoQuery from '~/graphql_shared/client/page_info.query.graphql';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import eventHub from '../eventhub';
-import DeployKeysService from '../service';
-import DeployKeysStore from '../store';
+import deployKeysQuery from '../graphql/queries/deploy_keys.query.graphql';
+import currentPageQuery from '../graphql/queries/current_page.query.graphql';
+import currentScopeQuery from '../graphql/queries/current_scope.query.graphql';
+import confirmRemoveKeyQuery from '../graphql/queries/confirm_remove_key.query.graphql';
+import updateCurrentScopeMutation from '../graphql/mutations/update_current_scope.mutation.graphql';
+import updateCurrentPageMutation from '../graphql/mutations/update_current_page.mutation.graphql';
+import confirmDisableMutation from '../graphql/mutations/confirm_action.mutation.graphql';
+import disableKeyMutation from '../graphql/mutations/disable_key.mutation.graphql';
import ConfirmModal from './confirm_modal.vue';
import KeysPanel from './keys_panel.vue';
@@ -17,120 +24,147 @@ export default {
GlButton,
GlIcon,
GlLoadingIcon,
+ GlPagination,
},
props: {
- endpoint: {
+ projectId: {
type: String,
required: true,
},
- projectId: {
+ projectPath: {
type: String,
required: true,
},
},
+ apollo: {
+ deployKeys: {
+ query: deployKeysQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ scope: this.currentScope,
+ page: this.currentPage,
+ };
+ },
+ update(data) {
+ return data?.project?.deployKeys || [];
+ },
+ error(error) {
+ createAlert({
+ message: s__('DeployKeys|Error getting deploy keys'),
+ captureError: true,
+ error,
+ });
+ },
+ },
+ pageInfo: {
+ query: pageInfoQuery,
+ variables() {
+ return { input: { page: this.currentPage, scope: this.currentScope } };
+ },
+ update({ pageInfo }) {
+ return pageInfo || {};
+ },
+ },
+ currentPage: {
+ query: currentPageQuery,
+ },
+ currentScope: {
+ query: currentScopeQuery,
+ },
+ deployKeyToRemove: {
+ query: confirmRemoveKeyQuery,
+ },
+ },
data() {
return {
- currentTab: 'enabled_keys',
- isLoading: false,
- store: new DeployKeysStore(),
- removeKey: () => {},
- cancel: () => {},
- confirmModalVisible: false,
+ deployKeys: [],
+ pageInfo: {},
+ deployKeyToRemove: null,
};
},
scopes: {
- enabled_keys: s__('DeployKeys|Enabled deploy keys'),
- available_project_keys: s__('DeployKeys|Privately accessible deploy keys'),
- public_keys: s__('DeployKeys|Publicly accessible deploy keys'),
+ enabledKeys: s__('DeployKeys|Enabled deploy keys'),
+ availableProjectKeys: s__('DeployKeys|Privately accessible deploy keys'),
+ availablePublicKeys: s__('DeployKeys|Publicly accessible deploy keys'),
},
i18n: {
loading: s__('DeployKeys|Loading deploy keys'),
addButton: s__('DeployKeys|Add new key'),
+ prevPage: __('Go to previous page'),
+ nextPage: __('Go to next page'),
+ next: __('Next'),
+ prev: __('Prev'),
+ goto: (page) => sprintf(__('Go to page %{page}'), { page }),
},
computed: {
tabs() {
- return Object.keys(this.$options.scopes).map((scope) => {
- const count = Array.isArray(this.keys[scope]) ? this.keys[scope].length : null;
-
+ return Object.entries(this.$options.scopes).map(([scope, name]) => {
return {
- name: this.$options.scopes[scope],
+ name,
scope,
- isActive: scope === this.currentTab,
- count,
+ isActive: scope === this.currentScope,
};
});
},
- hasKeys() {
- return Object.keys(this.keys).length;
- },
- keys() {
- return this.store.keys;
+ confirmModalVisible() {
+ return Boolean(this.deployKeyToRemove);
},
},
- created() {
- this.service = new DeployKeysService(this.endpoint);
-
- eventHub.$on('enable.key', this.enableKey);
- eventHub.$on('remove.key', this.confirmRemoveKey);
- eventHub.$on('disable.key', this.confirmRemoveKey);
- },
- mounted() {
- this.fetchKeys();
- },
- beforeDestroy() {
- eventHub.$off('enable.key', this.enableKey);
- eventHub.$off('remove.key', this.confirmRemoveKey);
- eventHub.$off('disable.key', this.confirmRemoveKey);
- },
methods: {
- onChangeTab(tab) {
- this.currentTab = tab;
+ onChangeTab(scope) {
+ return this.$apollo
+ .mutate({
+ mutation: updateCurrentScopeMutation,
+ variables: { scope },
+ })
+ .then(() => {
+ this.$apollo.queries.deployKeys.refetch();
+ })
+ .catch((error) => {
+ captureException(error, {
+ tags: {
+ deployKeyScope: scope,
+ },
+ });
+ });
},
- fetchKeys() {
- this.isLoading = true;
-
- return this.service
- .getKeys()
- .then((data) => {
- this.isLoading = false;
- this.store.keys = data;
+ moveNext() {
+ return this.movePage('next');
+ },
+ movePrevious() {
+ return this.movePage('previous');
+ },
+ movePage(direction) {
+ return this.moveToPage(this.pageInfo[`${direction}Page`]);
+ },
+ moveToPage(page) {
+ return this.$apollo.mutate({ mutation: updateCurrentPageMutation, variables: { page } });
+ },
+ removeKey() {
+ this.$apollo
+ .mutate({
+ mutation: disableKeyMutation,
+ variables: { id: this.deployKeyToRemove.id },
+ })
+ .then(() => {
+ if (!this.deployKeys.length) {
+ return this.movePage('previous');
+ }
+ return null;
})
+ .then(() => this.$apollo.queries.deployKeys.refetch())
.catch(() => {
- this.isLoading = false;
- this.store.keys = {};
- return createAlert({
- message: s__('DeployKeys|Error getting deploy keys'),
+ createAlert({
+ message: s__('DeployKeys|Error removing deploy key'),
});
});
},
- enableKey(deployKey) {
- this.service
- .enableKey(deployKey.id)
- .then(this.fetchKeys)
- .catch(() =>
- createAlert({
- message: s__('DeployKeys|Error enabling deploy key'),
- }),
- );
- },
- confirmRemoveKey(deployKey, callback) {
- const hideModal = () => {
- this.confirmModalVisible = false;
- callback?.();
- };
- this.removeKey = () => {
- this.service
- .disableKey(deployKey.id)
- .then(this.fetchKeys)
- .then(hideModal)
- .catch(() =>
- createAlert({
- message: s__('DeployKeys|Error removing deploy key'),
- }),
- );
- };
- this.cancel = hideModal;
- this.confirmModalVisible = true;
+ cancel() {
+ this.$apollo.mutate({
+ mutation: confirmDisableMutation,
+ variables: { id: null },
+ });
},
},
};
@@ -139,47 +173,59 @@ export default {
<template>
<div class="deploy-keys">
<confirm-modal :visible="confirmModalVisible" @remove="removeKey" @cancel="cancel" />
+ <div class="gl-new-card-header gl-align-items-center gl-py-0 gl-pl-0">
+ <div class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-b-0">
+ <div class="fade-left">
+ <gl-icon name="chevron-lg-left" :size="12" />
+ </div>
+ <div class="fade-right">
+ <gl-icon name="chevron-lg-right" :size="12" />
+ </div>
+
+ <navigation-tabs
+ :tabs="tabs"
+ scope="deployKeys"
+ class="gl-rounded-lg"
+ @onChangeTab="onChangeTab"
+ />
+ </div>
+
+ <div class="gl-new-card-actions">
+ <gl-button
+ size="small"
+ class="js-toggle-button js-toggle-content"
+ data-testid="add-new-deploy-key-button"
+ >
+ {{ $options.i18n.addButton }}
+ </gl-button>
+ </div>
+ </div>
<gl-loading-icon
- v-if="isLoading && !hasKeys"
+ v-if="$apollo.queries.deployKeys.loading"
:label="$options.i18n.loading"
- size="sm"
+ size="md"
class="gl-m-5"
/>
- <template v-else-if="hasKeys">
- <div class="gl-new-card-header gl-align-items-center gl-pt-0 gl-pb-0 gl-pl-0">
- <div class="top-area scrolling-tabs-container inner-page-scroll-tabs gl-border-b-0">
- <div class="fade-left">
- <gl-icon name="chevron-lg-left" :size="12" />
- </div>
- <div class="fade-right">
- <gl-icon name="chevron-lg-right" :size="12" />
- </div>
-
- <navigation-tabs
- :tabs="tabs"
- scope="deployKeys"
- class="gl-rounded-lg"
- @onChangeTab="onChangeTab"
- />
- </div>
-
- <div class="gl-new-card-actions">
- <gl-button
- size="small"
- class="js-toggle-button js-toggle-content"
- data-testid="add-new-deploy-key-button"
- >
- {{ $options.i18n.addButton }}
- </gl-button>
- </div>
- </div>
+ <template v-else>
<keys-panel
:project-id="projectId"
- :keys="keys[currentTab]"
- :store="store"
- :endpoint="endpoint"
+ :keys="deployKeys"
data-testid="project-deploy-keys-container"
/>
+ <gl-pagination
+ align="center"
+ :total-items="pageInfo.total"
+ :per-page="pageInfo.perPage"
+ :value="currentPage"
+ :next="$options.i18n.next"
+ :prev="$options.i18n.prev"
+ :label-previous-page="$options.i18n.prevPage"
+ :label-next-page="$options.i18n.nextPage"
+ :label-page="$options.i18n.goto"
+ @next="moveNext()"
+ @previous="movePrevious()"
+ @input="moveToPage"
+ />
</template>
</div>
</template>
diff --git a/app/assets/javascripts/deploy_keys/components/key.vue b/app/assets/javascripts/deploy_keys/components/key.vue
index 16c745d8cff..d4b140f1adb 100644
--- a/app/assets/javascripts/deploy_keys/components/key.vue
+++ b/app/assets/javascripts/deploy_keys/components/key.vue
@@ -2,8 +2,12 @@
<script>
import { GlBadge, GlButton, GlIcon, GlTooltipDirective } from '@gitlab/ui';
import { head, tail } from 'lodash';
+import { createAlert } from '~/alert';
import { s__, sprintf } from '~/locale';
import timeagoMixin from '~/vue_shared/mixins/timeago';
+import currentScopeQuery from '../graphql/queries/current_scope.query.graphql';
+import enableKeyMutation from '../graphql/mutations/enable_key.mutation.graphql';
+import confirmDisableMutation from '../graphql/mutations/confirm_action.mutation.graphql';
import ActionBtn from './action_btn.vue';
@@ -23,48 +27,25 @@ export default {
type: Object,
required: true,
},
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
projectId: {
type: String,
required: false,
default: null,
},
},
+ apollo: {
+ currentScope: {
+ query: currentScopeQuery,
+ },
+ },
data() {
return {
projectsExpanded: false,
};
},
computed: {
- editDeployKeyPath() {
- return `${this.endpoint}/${this.deployKey.id}/edit`;
- },
projects() {
- const projects = [...this.deployKey.deploy_keys_projects];
-
- if (this.projectId !== null) {
- const indexOfCurrentProject = projects.findIndex(
- (project) =>
- project &&
- project.project &&
- project.project.id &&
- project.project.id.toString() === this.projectId,
- );
-
- if (indexOfCurrentProject > -1) {
- const currentProject = projects.splice(indexOfCurrentProject, 1);
- currentProject[0].project.full_name = s__('DeployKeys|Current project');
- return currentProject.concat(projects);
- }
- }
- return projects;
+ return this.deployKey.deployKeysProjects;
},
firstProject() {
return head(this.projects);
@@ -81,13 +62,11 @@ export default {
return sprintf(s__('DeployKeys|+%{count} others'), { count: this.restProjects.length });
},
isEnabled() {
- return this.store.isEnabled(this.deployKey.id);
+ return this.currentScope === 'enabledKeys';
},
isRemovable() {
return (
- this.store.isEnabled(this.deployKey.id) &&
- this.deployKey.destroyed_when_orphaned &&
- this.deployKey.almost_orphaned
+ this.isEnabled && this.deployKey.destroyedWhenOrphaned && this.deployKey.almostOrphaned
);
},
isExpandable() {
@@ -99,14 +78,37 @@ export default {
},
methods: {
projectTooltipTitle(project) {
- return project.can_push
+ return project.canPush
? s__('DeployKeys|Grant write permissions to this key')
: s__('DeployKeys|Read access only');
},
toggleExpanded() {
this.projectsExpanded = !this.projectsExpanded;
},
+ isCurrentProject({ project } = {}) {
+ if (this.projectId !== null) {
+ return Boolean(project?.id?.toString() === this.projectId);
+ }
+
+ return false;
+ },
+ projectName(project) {
+ if (this.isCurrentProject(project)) {
+ return s__('DeployKeys|Current project');
+ }
+
+ return project?.project?.fullName;
+ },
+ onEnableError(error) {
+ createAlert({
+ message: s__('DeployKeys|Error enabling deploy key'),
+ captureError: true,
+ error,
+ });
+ },
},
+ enableKeyMutation,
+ confirmDisableMutation,
};
</script>
@@ -128,7 +130,7 @@ export default {
<dl class="gl-font-sm gl-mb-0">
<dt>{{ __('SHA256') }}</dt>
<dd class="fingerprint" data-testid="key-sha256-fingerprint-content">
- {{ deployKey.fingerprint_sha256 }}
+ {{ deployKey.fingerprintSha256 }}
</dd>
<template v-if="deployKey.fingerprint">
<dt>
@@ -150,10 +152,10 @@ export default {
<gl-badge
v-gl-tooltip
:title="projectTooltipTitle(firstProject)"
- :icon="firstProject.can_push ? 'lock-open' : 'lock'"
+ :icon="firstProject.canPush ? 'lock-open' : 'lock'"
class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate"
>
- <span class="gl-text-truncate">{{ firstProject.project.full_name }}</span>
+ <span class="gl-text-truncate">{{ projectName(firstProject) }}</span>
</gl-badge>
<gl-badge
@@ -170,14 +172,14 @@ export default {
<gl-badge
v-for="deployKeysProject in restProjects"
v-else-if="isExpanded"
- :key="deployKeysProject.project.full_path"
+ :key="deployKeysProject.project.fullPath"
v-gl-tooltip
- :href="deployKeysProject.project.full_path"
+ :href="deployKeysProject.project.fullPath"
:title="projectTooltipTitle(deployKeysProject)"
- :icon="deployKeysProject.can_push ? 'lock-open' : 'lock'"
+ :icon="deployKeysProject.canPush ? 'lock-open' : 'lock'"
class="deploy-project-label gl-mr-2 gl-mb-2 gl-truncate"
>
- <span class="gl-text-truncate">{{ deployKeysProject.project.full_name }}</span>
+ <span class="gl-text-truncate">{{ projectName(deployKeysProject) }}</span>
</gl-badge>
</template>
<span v-else class="gl-text-secondary">{{ __('None') }}</span>
@@ -188,8 +190,8 @@ export default {
{{ __('Created') }}
</div>
<div class="table-mobile-content gl-text-gray-700 key-created-at">
- <span v-gl-tooltip :title="tooltipTitle(deployKey.created_at)">
- <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.created_at) }}</span>
+ <span v-gl-tooltip :title="tooltipTitle(deployKey.createdAt)">
+ <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.createdAt) }}</span>
</span>
</div>
</div>
@@ -199,12 +201,12 @@ export default {
</div>
<div class="table-mobile-content gl-text-gray-700 key-expires-at">
<span
- v-if="deployKey.expires_at"
+ v-if="deployKey.expiresAt"
v-gl-tooltip
- :title="tooltipTitle(deployKey.expires_at)"
+ :title="tooltipTitle(deployKey.expiresAt)"
data-testid="expires-at-tooltip"
>
- <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.expires_at) }}</span>
+ <gl-icon name="calendar" /> <span>{{ timeFormatted(deployKey.expiresAt) }}</span>
</span>
<span v-else>
<span data-testid="expires-never">{{ __('Never') }}</span>
@@ -213,13 +215,19 @@ export default {
</div>
<div class="table-section section-10 table-button-footer deploy-key-actions">
<div class="btn-group table-action-buttons">
- <action-btn v-if="!isEnabled" :deploy-key="deployKey" type="enable" category="secondary">
+ <action-btn
+ v-if="!isEnabled"
+ :deploy-key="deployKey"
+ :mutation="$options.enableKeyMutation"
+ category="secondary"
+ @error="onEnableError"
+ >
{{ __('Enable') }}
</action-btn>
<gl-button
- v-if="deployKey.can_edit"
+ v-if="deployKey.editPath"
v-gl-tooltip
- :href="editDeployKeyPath"
+ :href="deployKey.editPath"
:title="__('Edit')"
:aria-label="__('Edit')"
data-container="body"
@@ -232,10 +240,10 @@ export default {
:deploy-key="deployKey"
:title="__('Remove')"
:aria-label="__('Remove')"
+ :mutation="$options.confirmDisableMutation"
category="secondary"
variant="danger"
icon="remove"
- type="remove"
data-container="body"
/>
<action-btn
@@ -244,7 +252,7 @@ export default {
:deploy-key="deployKey"
:title="__('Disable')"
:aria-label="__('Disable')"
- type="disable"
+ :mutation="$options.confirmDisableMutation"
data-container="body"
icon="cancel"
category="secondary"
diff --git a/app/assets/javascripts/deploy_keys/components/keys_panel.vue b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
index dac63188aa5..088b85e6093 100644
--- a/app/assets/javascripts/deploy_keys/components/keys_panel.vue
+++ b/app/assets/javascripts/deploy_keys/components/keys_panel.vue
@@ -10,14 +10,6 @@ export default {
type: Array,
required: true,
},
- store: {
- type: Object,
- required: true,
- },
- endpoint: {
- type: String,
- required: true,
- },
projectId: {
type: String,
required: false,
@@ -48,8 +40,6 @@ export default {
v-for="deployKey in keys"
:key="deployKey.id"
:deploy-key="deployKey"
- :store="store"
- :endpoint="endpoint"
:project-id="projectId"
/>
</template>
diff --git a/app/assets/javascripts/deploy_keys/graphql/resolvers.js b/app/assets/javascripts/deploy_keys/graphql/resolvers.js
index 1993801636e..a8693665b90 100644
--- a/app/assets/javascripts/deploy_keys/graphql/resolvers.js
+++ b/app/assets/javascripts/deploy_keys/graphql/resolvers.js
@@ -15,6 +15,8 @@ export const mapDeployKey = (deployKey) => ({
__typename: 'LocalDeployKey',
});
+const DEFAULT_PAGE_SIZE = 5;
+
export const resolvers = (endpoints) => ({
Project: {
deployKeys(_, { scope, page }, { client }) {
@@ -25,19 +27,21 @@ export const resolvers = (endpoints) => ({
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 axios
+ .get(endpoint, { params: { page, per_page: DEFAULT_PAGE_SIZE } })
+ .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) || [];
});
- return data?.keys?.map(mapDeployKey) || [];
- });
},
},
Mutation: {
@@ -48,6 +52,13 @@ export const resolvers = (endpoints) => ({
});
},
currentScope(_, { scope }, { client }) {
+ const key = `${scope}Endpoint`;
+ const { [key]: endpoint } = endpoints;
+
+ if (!endpoint) {
+ throw new Error(`invalid deploy key scope selected: ${scope}`);
+ }
+
client.writeQuery({
query: currentPageQuery,
data: { currentPage: 1 },
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index 83601d5b2e3..673462073f0 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,24 +1,26 @@
import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import DeployKeysApp from './components/app.vue';
+import { createApolloProvider } from './graphql/client';
-export default () =>
- new Vue({
- el: document.getElementById('js-deploy-keys'),
- components: {
- DeployKeysApp,
- },
- data() {
- return {
- endpoint: this.$options.el.dataset.endpoint,
- projectId: this.$options.el.dataset.projectId,
- };
- },
+Vue.use(VueApollo);
+
+export default () => {
+ const el = document.getElementById('js-deploy-keys');
+ return new Vue({
+ el,
+ apolloProvider: createApolloProvider({
+ enabledKeysEndpoint: el.dataset.enabledEndpoint,
+ availableProjectKeysEndpoint: el.dataset.availableProjectEndpoint,
+ availablePublicKeysEndpoint: el.dataset.availablePublicEndpoint,
+ }),
render(createElement) {
- return createElement('deploy-keys-app', {
+ return createElement(DeployKeysApp, {
props: {
- endpoint: this.endpoint,
- projectId: this.projectId,
+ projectId: el.dataset.projectId,
+ projectPath: el.dataset.projectPath,
},
});
},
});
+};
diff --git a/app/assets/javascripts/deploy_keys/service/index.js b/app/assets/javascripts/deploy_keys/service/index.js
deleted file mode 100644
index 2837fc8ed88..00000000000
--- a/app/assets/javascripts/deploy_keys/service/index.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import axios from '~/lib/utils/axios_utils';
-
-export default class DeployKeysService {
- constructor(endpoint) {
- this.endpoint = endpoint;
- }
-
- getKeys() {
- return axios.get(this.endpoint).then((response) => response.data);
- }
-
- enableKey(id) {
- return axios.put(`${this.endpoint}/${id}/enable`).then((response) => response.data);
- }
-
- disableKey(id) {
- return axios.put(`${this.endpoint}/${id}/disable`).then((response) => response.data);
- }
-}
diff --git a/app/assets/javascripts/deploy_keys/store/index.js b/app/assets/javascripts/deploy_keys/store/index.js
deleted file mode 100644
index dcd77e921cd..00000000000
--- a/app/assets/javascripts/deploy_keys/store/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-export default class DeployKeysStore {
- constructor() {
- this.keys = {};
- }
-
- isEnabled(id) {
- return this.keys.enabled_keys.some((key) => key.id === id);
- }
-}
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 00fd9f43a4f..698fd3909ed 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -144,6 +144,11 @@ export default {
required: false,
default: '',
},
+ pinnedFileUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -153,6 +158,7 @@ export default {
autoScrolled: false,
activeProject: undefined,
hasScannerError: false,
+ pinnedFileStatus: '',
};
},
apollo: {
@@ -215,7 +221,6 @@ export default {
...mapState('findingsDrawer', ['activeDrawer']),
...mapState('diffs', [
'isLoading',
- 'diffFiles',
'diffViewType',
'commit',
'renderOverflowWarning',
@@ -245,6 +250,7 @@ export default {
'isBatchLoading',
'isBatchLoadingError',
'flatBlobsList',
+ 'diffFiles',
]),
...mapGetters(['isNotesFetched', 'getNoteableData']),
...mapGetters('findingsDrawer', ['activeDrawer']),
@@ -355,7 +361,7 @@ export default {
const id = window?.location?.hash;
if (id && id.indexOf('#note') !== 0) {
- this.setHighlightedRow(id.split('diff-content').pop().slice(1));
+ this.setHighlightedRow({ lineCode: id.split('diff-content').pop().slice(1) });
}
const events = [];
@@ -438,6 +444,7 @@ export default {
'setFileByFile',
'disableVirtualScroller',
'setGenerateTestFilePath',
+ 'fetchPinnedFile',
]),
...mapActions('findingsDrawer', ['setDrawer']),
closeDrawer() {
@@ -509,6 +516,20 @@ export default {
return !this.diffFiles.length;
},
fetchData({ toggleTree = true, fetchMeta = true } = {}) {
+ if (this.pinnedFileUrl && this.pinnedFileStatus !== 'loaded') {
+ this.pinnedFileStatus = 'loading';
+ this.fetchPinnedFile(this.pinnedFileUrl)
+ .then(() => {
+ this.pinnedFileStatus = 'loaded';
+ if (toggleTree) this.setTreeDisplay();
+ })
+ .catch(() => {
+ this.pinnedFileStatus = 'error';
+ createAlert({
+ message: __("Couldn't fetch the pinned file."),
+ });
+ });
+ }
if (fetchMeta) {
this.fetchDiffFilesMeta()
.then((data) => {
@@ -539,7 +560,7 @@ export default {
}
if (!this.viewDiffsFileByFile) {
- this.fetchDiffFilesBatch()
+ this.fetchDiffFilesBatch(Boolean(this.pinnedFileUrl))
.then(() => {
if (toggleTree) this.setTreeDisplay();
// Guarantee the discussions are assigned after the batch finishes.
@@ -724,6 +745,9 @@ export default {
<gl-loading-icon size="lg" />
</div>
<template v-else-if="renderDiffFiles">
+ <div v-if="pinnedFileStatus === 'loading'" class="loading">
+ <gl-loading-icon size="lg" />
+ </div>
<dynamic-scroller
v-if="isVirtualScrollingEnabled"
:items="diffs"
diff --git a/app/assets/javascripts/diffs/components/commit_item.vue b/app/assets/javascripts/diffs/components/commit_item.vue
index 7493bd5fdf7..3545eb4ed73 100644
--- a/app/assets/javascripts/diffs/components/commit_item.vue
+++ b/app/assets/javascripts/diffs/components/commit_item.vue
@@ -94,7 +94,7 @@ export default {
class="d-block d-sm-flex flex-row-reverse justify-content-between align-items-start flex-lg-row-reverse"
>
<div
- class="commit-actions flex-row d-none d-sm-flex align-items-start flex-wrap justify-content-end"
+ class="commit-actions flex-row d-none d-sm-flex align-items-center flex-wrap justify-content-end"
>
<div
v-if="commit.signature_html"
@@ -105,7 +105,7 @@ export default {
:endpoint="commit.pipeline_status_path"
class="d-inline-flex mb-2"
/>
- <gl-button-group class="gl-ml-4 gl-mb-4" data-testid="commit-sha-group">
+ <gl-button-group class="gl-ml-4" data-testid="commit-sha-group">
<gl-button label class="gl-font-monospace" data-testid="commit-sha-short-id">{{
commit.short_id
}}</gl-button>
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 82b721da493..39f642b0831 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -17,6 +17,7 @@ import DiffFileDrafts from '~/batch_comments/components/diff_file_drafts.vue';
import NoteForm from '~/notes/components/note_form.vue';
import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
+import { fileContentsId } from '~/diffs/components/diff_row_utils';
import {
DIFF_FILE_AUTOMATIC_COLLAPSE,
DIFF_FILE_MANUAL_COLLAPSE,
@@ -110,7 +111,10 @@ export default {
'canMerge',
]),
...mapGetters(['isNotesFetched', 'getNoteableData', 'noteableType']),
- ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled']),
+ ...mapGetters('diffs', ['getDiffFileDiscussions', 'isVirtualScrollingEnabled', 'pinnedFile']),
+ isPinnedFile() {
+ return this.file === this.pinnedFile;
+ },
viewBlobHref() {
return escape(this.file.view_path);
},
@@ -206,6 +210,9 @@ export default {
diffFileHash() {
return this.file.file_hash;
},
+ fileId() {
+ return fileContentsId(this.file);
+ },
},
watch: {
'file.id': {
@@ -293,7 +300,7 @@ export default {
},
handleToggle({ viaUserInteraction = false } = {}) {
const collapsingNow = !this.isCollapsed;
- const contentElement = this.$el.querySelector(`#diff-content-${this.file.file_hash}`);
+ const contentElement = this.$el.querySelector(`#${fileContentsId(this.file)}`);
this.setFileCollapsedByUser({
filePath: this.file.file_path,
@@ -386,6 +393,7 @@ export default {
'comments-disabled': Boolean(file.brokenSymlink),
'has-body': showBody,
'is-virtual-scrolling': isVirtualScrollingEnabled,
+ 'pinned-file': isPinnedFile,
}"
:data-path="file.new_path"
class="diff-file file-holder gl-border-none gl-mb-0! gl-pb-5"
@@ -400,6 +408,7 @@ export default {
:add-merge-request-buttons="true"
:view-diffs-file-by-file="viewDiffsFileByFile"
:show-local-file-reviews="showLocalFileReviews"
+ :pinned="isPinnedFile"
class="js-file-title file-title gl-border-1 gl-border-solid gl-border-gray-100"
:class="hasBodyClasses.header"
@toggleFile="handleToggle({ viaUserInteraction: true })"
@@ -428,7 +437,7 @@ export default {
</div>
<template v-else>
<div
- :id="`diff-content-${file.file_hash}`"
+ :id="fileId"
:class="hasBodyClasses.contentByHash"
class="diff-content"
data-testid="content-area"
diff --git a/app/assets/javascripts/diffs/components/diff_file_header.vue b/app/assets/javascripts/diffs/components/diff_file_header.vue
index e45fd508a5b..97db0fc1c24 100644
--- a/app/assets/javascripts/diffs/components/diff_file_header.vue
+++ b/app/assets/javascripts/diffs/components/diff_file_header.vue
@@ -22,6 +22,7 @@ import { __, s__, sprintf } from '~/locale';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { fileContentsId, pinnedFileHref } from '~/diffs/components/diff_row_utils';
import { DIFF_FILE_AUTOMATIC_COLLAPSE } from '../constants';
import { DIFF_FILE_HEADER } from '../i18n';
import { collapsedType, isCollapsed } from '../utils/diff_file';
@@ -102,6 +103,11 @@ export default {
required: false,
default: false,
},
+ pinned: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
idState() {
return {
@@ -113,9 +119,8 @@ export default {
...mapGetters('diffs', ['diffHasExpandedDiscussions', 'diffHasDiscussions']),
...mapGetters(['getNoteableData']),
diffContentIDSelector() {
- return `#diff-content-${this.diffFile.file_hash}`;
+ return `${pinnedFileHref(this.diffFile)}#${fileContentsId(this.diffFile)}`;
},
-
titleLink() {
if (this.diffFile.submodule) {
return this.diffFile.submodule_tree_url || this.diffFile.submodule_link;
@@ -222,6 +227,7 @@ export default {
'setFileForcedOpen',
'setGenerateTestFilePath',
'toggleFileCommentForm',
+ 'unpinFile',
]),
handleToggleFile() {
this.setFileForcedOpen({
@@ -295,7 +301,19 @@ export default {
>
<div class="file-header-content">
<gl-button
- v-if="collapsible"
+ v-if="pinned"
+ v-gl-tooltip.hover.focus
+ :title="__('Unpin the file')"
+ :aria-label="__('Unpin the file')"
+ icon="thumbtack"
+ size="small"
+ class="btn-icon gl-mr-2"
+ category="tertiary"
+ data-testid="unpin-button"
+ @click="unpinFile"
+ />
+ <gl-button
+ v-else-if="collapsible"
ref="collapseButton"
class="gl-mr-2"
category="tertiary"
@@ -305,10 +323,10 @@ export default {
@click.stop="handleToggleFile"
/>
<a
- ref="titleWrapper"
:v-once="!viewDiffsFileByFile"
class="gl-mr-2 gl-text-decoration-none! gl-word-break-all"
:href="titleLink"
+ data-testid="file-title"
@click="handleFileNameClick"
>
<span v-if="isFileRenamed">
@@ -354,7 +372,7 @@ export default {
<small
v-if="isModeChanged"
ref="fileMode"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
class="mr-1"
:title="$options.i18n.fileModeTooltip"
>
@@ -377,7 +395,7 @@ export default {
/>
<gl-form-checkbox
v-if="isReviewable && showLocalFileReviews"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
data-testid="fileReviewCheckbox"
class="gl-mr-5 gl-mb-n3 gl-display-flex gl-align-items-center"
:title="$options.i18n.fileReviewTooltip"
@@ -388,7 +406,7 @@ export default {
</gl-form-checkbox>
<gl-button
v-if="showCommentButton"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
:title="__('Comment on this file')"
:aria-label="__('Comment on this file')"
icon="comment"
@@ -402,7 +420,7 @@ export default {
<gl-button
v-if="diffFile.external_url"
ref="externalLink"
- v-gl-tooltip.hover
+ v-gl-tooltip.hover.focus
:href="diffFile.external_url"
:title="externalUrlLabel"
:aria-label="externalUrlLabel"
diff --git a/app/assets/javascripts/diffs/components/diff_row.vue b/app/assets/javascripts/diffs/components/diff_row.vue
index 3dad7a1a8e4..a9da77104d4 100644
--- a/app/assets/javascripts/diffs/components/diff_row.vue
+++ b/app/assets/javascripts/diffs/components/diff_row.vue
@@ -292,7 +292,7 @@ export default {
v-if="props.line.left.old_line && props.line.left.type !== $options.CONFLICT_THEIR"
:data-linenumber="props.line.left.old_line"
:href="props.line.lineHrefOld"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
<component
@@ -318,7 +318,7 @@ export default {
v-if="props.line.left.new_line && props.line.left.type !== $options.CONFLICT_OUR"
:data-linenumber="props.line.left.new_line"
:href="props.line.lineHrefOld"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
</div>
@@ -446,7 +446,7 @@ export default {
v-if="props.line.right.new_line"
:data-linenumber="props.line.right.new_line"
:href="props.line.lineHrefNew"
- @click="listeners.setHighlightedRow(props.line.lineCode)"
+ @click="listeners.setHighlightedRow({ lineCode: props.line.lineCode, event: $event })"
>
</a>
<component
diff --git a/app/assets/javascripts/diffs/components/diff_row_utils.js b/app/assets/javascripts/diffs/components/diff_row_utils.js
index a489c96b0c9..5c62e0179ac 100644
--- a/app/assets/javascripts/diffs/components/diff_row_utils.js
+++ b/app/assets/javascripts/diffs/components/diff_row_utils.js
@@ -33,7 +33,19 @@ export const shouldRenderCommentButton = (isLoggedIn, isCommentButtonRendered) =
export const hasDiscussions = (line) => line?.discussions?.length > 0;
-export const lineHref = (line) => `#${line?.line_code || ''}`;
+export const pinnedFileHref = (diffFile) => {
+ if (!window?.gon?.features?.pinnedFile) return '';
+ return `?pin=${diffFile.file_hash}`;
+};
+
+export const lineHref = (line, content) => {
+ if (!line || !line.line_code) return '';
+ return `${pinnedFileHref(content.diffFile)}#${line.line_code}`;
+};
+
+export const fileContentsId = (diffFile) => {
+ return `diff-content-${diffFile.file_hash}`;
+};
export const lineCode = (line) => {
if (!line) return undefined;
@@ -179,8 +191,8 @@ export const mapParallel = (content) => (line) => {
isContextLineRight: isContextLine(right?.type),
hasDiscussionsLeft: hasDiscussions(left),
hasDiscussionsRight: hasDiscussions(right),
- lineHrefOld: lineHref(left),
- lineHrefNew: lineHref(right),
+ lineHrefOld: lineHref(left, content),
+ lineHrefNew: lineHref(right, content),
lineCode: lineCode(line),
isMetaLineLeft: isMetaLine(left?.type),
isMetaLineRight: isMetaLine(right?.type),
diff --git a/app/assets/javascripts/diffs/components/tree_list.vue b/app/assets/javascripts/diffs/components/tree_list.vue
index 07984beb709..ab21391b364 100644
--- a/app/assets/javascripts/diffs/components/tree_list.vue
+++ b/app/assets/javascripts/diffs/components/tree_list.vue
@@ -34,7 +34,7 @@ export default {
},
computed: {
...mapState('diffs', ['tree', 'renderTreeList', 'currentDiffFileId', 'viewedDiffFileIds']),
- ...mapGetters('diffs', ['allBlobs']),
+ ...mapGetters('diffs', ['allBlobs', 'pinnedFile']),
filteredTreeList() {
let search = this.search.toLowerCase().trim();
@@ -71,21 +71,59 @@ export default {
// out: [{ path: 'a', tree: [{ path: 'b' }] }, { path: 'b' }, { path: 'c' }]
flatFilteredTreeList() {
const result = [];
- const createFlatten = (level) => (item) => {
+ const createFlatten = (level, hidden) => (item) => {
result.push({
...item,
+ hidden,
level: item.isHeader ? 0 : level,
key: item.key || item.path,
});
- if (item.opened || item.isHeader) {
- item.tree.forEach(createFlatten(level + 1));
- }
+ const isHidden = hidden || (item.type === 'tree' && !item.opened);
+ item.tree.forEach(createFlatten(level + 1, isHidden));
};
this.filteredTreeList.forEach(createFlatten(0));
return result;
},
+ flatListWithPinnedFile() {
+ const result = [...this.flatFilteredTreeList];
+ const pinnedIndex = result.findIndex((item) => item.path === this.pinnedFile.file_path);
+ const [pinnedItem] = result.splice(pinnedIndex, 1);
+
+ if (pinnedItem.parentPath === '/')
+ return [{ ...pinnedItem, level: 0, pinned: true, hidden: false }, ...result];
+
+ // remove detached folder from the tree
+ const next = result[pinnedIndex];
+ const prev = result[pinnedIndex - 1];
+ const hasContainingFolder =
+ prev && prev.type === 'tree' && prev.level === pinnedItem.level - 1;
+ const hasSibling = next && next.type !== 'tree' && next.level === pinnedItem.level;
+ if (hasContainingFolder && !hasSibling) {
+ // folder tree is always condensed so we only need to remove the parent folder
+ result.splice(pinnedIndex - 1, 1);
+ }
+
+ return [
+ {
+ level: 0,
+ key: 'pinned-path',
+ isHeader: true,
+ opened: true,
+ path: pinnedItem.parentPath,
+ type: 'tree',
+ hidden: false,
+ },
+ { ...pinnedItem, level: 1, pinned: true, hidden: false },
+ ...result,
+ ];
+ },
+ treeList() {
+ const list = this.pinnedFile ? this.flatListWithPinnedFile : this.flatFilteredTreeList;
+ if (this.search) return list;
+ return list.filter((item) => !item.hidden);
+ },
},
methods: {
...mapActions('diffs', ['toggleTreeOpen', 'goToFile']),
@@ -125,13 +163,13 @@ export default {
</button>
</div>
</div>
- <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="flatFilteredTreeList.length">
+ <tree-list-height class="gl-flex-grow-1 gl-min-h-0" :items-count="treeList.length">
<template #default="{ scrollerHeight, rowHeight }">
<div :class="{ 'tree-list-blobs': !renderTreeList || search }" class="mr-tree-list">
<recycle-scroller
- v-if="flatFilteredTreeList.length"
+ v-if="treeList.length"
:style="{ height: `${scrollerHeight}px` }"
- :items="flatFilteredTreeList"
+ :items="treeList"
:item-size="rowHeight"
:buffer="100"
key-field="key"
diff --git a/app/assets/javascripts/diffs/index.js b/app/assets/javascripts/diffs/index.js
index 15e4225f062..b219b5499c9 100644
--- a/app/assets/javascripts/diffs/index.js
+++ b/app/assets/javascripts/diffs/index.js
@@ -93,6 +93,7 @@ export default function initDiffsApp(store = notesStore) {
helpPagePath: this.helpPagePath,
shouldShow: this.activeTab === 'diffs',
changesEmptyStateIllustration: this.changesEmptyStateIllustration,
+ pinnedFileUrl: dataset.pinnedFileUrl,
},
});
},
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 1c0e20183e2..e15d403bd11 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -10,7 +10,7 @@ import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
-import { mergeUrlParams, getLocationHash } from '~/lib/utils/url_utility';
+import { mergeUrlParams, getLocationHash, getParameterValues } from '~/lib/utils/url_utility';
import notesEventHub from '~/notes/event_hub';
import { generateTreeList } from '~/diffs/utils/tree_worker_utils';
import { sortTree } from '~/ide/stores/utils';
@@ -54,7 +54,6 @@ import {
} from '../constants';
import {
DISCUSSION_SINGLE_DIFF_FAILED,
- LOAD_SINGLE_DIFF_FAILED,
BUILDING_YOUR_MR,
SOMETHING_WENT_WRONG,
ERROR_LOADING_FULL_DIFF,
@@ -119,7 +118,9 @@ export const setBaseConfig = ({ commit }, options) => {
};
export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry) => {
- const versionPath = state.mergeRequestDiff?.version_path;
+ const url = new URL(state.endpointBatch, 'https://gitlab.com');
+ const diffId = getParameterValues('diff_id', url)[0];
+ const startSha = getParameterValues('start_sha', url)[0];
if (
treeEntry &&
@@ -133,12 +134,14 @@ export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry)
w: state.showWhitespace ? '0' : '1',
view: 'inline',
commit_id: getters.commitId,
+ diff_head: true,
};
- if (versionPath) {
- const { diffId, startSha } = getDerivedMergeRequestInformation({ endpoint: versionPath });
-
+ if (diffId) {
urlParams.diff_id = diffId;
+ }
+
+ if (startSha) {
urlParams.start_sha = startSha;
}
@@ -161,7 +164,9 @@ export const prefetchSingleFile = async ({ state, getters, commit }, treeEntry)
export const fetchFileByFile = async ({ state, getters, commit }) => {
const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
const id = parseUrlHashAsFileHash(window?.location?.hash, state.currentDiffFileId);
- const versionPath = state.mergeRequestDiff?.version_path;
+ const url = new URL(state.endpointBatch, 'https://gitlab.com');
+ const diffId = getParameterValues('diff_id', url)[0];
+ const startSha = getParameterValues('start_sha', url)[0];
const treeEntry = id
? getters.flatBlobsList.find(({ fileHash }) => fileHash === id)
: getters.flatBlobsList[0];
@@ -179,12 +184,14 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
w: state.showWhitespace ? '0' : '1',
view: 'inline',
commit_id: getters.commitId,
+ diff_head: true,
};
- if (versionPath) {
- const { diffId, startSha } = getDerivedMergeRequestInformation({ endpoint: versionPath });
-
+ if (diffId) {
urlParams.diff_id = diffId;
+ }
+
+ if (startSha) {
urlParams.start_sha = startSha;
}
@@ -210,7 +217,7 @@ export const fetchFileByFile = async ({ state, getters, commit }) => {
}
};
-export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
+export const fetchDiffFilesBatch = ({ commit, state, dispatch }, pinnedFileLoading = false) => {
let perPage = state.viewDiffsFileByFile ? 1 : state.perPage;
let increaseAmount = 1.4;
const startPage = 0;
@@ -224,8 +231,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
let totalLoaded = 0;
let scrolledVirtualScroller = hash === '';
- commit(types.SET_BATCH_LOADING_STATE, 'loading');
- commit(types.SET_RETRIEVING_BATCHES, true);
+ if (!pinnedFileLoading) {
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+ }
eventHub.$emit(EVT_PERF_MARK_DIFF_FILES_START);
const getBatch = (page = startPage) =>
@@ -237,7 +246,7 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffFiles });
commit(types.SET_BATCH_LOADING_STATE, 'loaded');
- if (!scrolledVirtualScroller) {
+ if (!scrolledVirtualScroller && !pinnedFileLoading) {
const index = state.diffFiles.findIndex(
(f) =>
f.file_hash === hash || f[INLINE_DIFF_LINES_KEY].find((l) => l.line_code === hash),
@@ -301,9 +310,10 @@ export const fetchDiffFilesBatch = ({ commit, state, dispatch }) => {
return null;
})
- .catch(() => {
+ .catch((error) => {
commit(types.SET_RETRIEVING_BATCHES, false);
commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
});
return getBatch();
@@ -384,7 +394,11 @@ export const fetchCoverageFiles = ({ commit, state }) => {
coveragePoll.makeRequest();
};
-export const setHighlightedRow = ({ commit }, lineCode) => {
+export const setHighlightedRow = ({ commit }, { lineCode, event }) => {
+ if (event && event.target.href) {
+ event.preventDefault();
+ window.history.replaceState(null, undefined, event.target.href);
+ }
const fileHash = lineCode.split('_')[0];
commit(types.SET_HIGHLIGHTED_ROW, lineCode);
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -657,6 +671,8 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
} else {
if (!state.treeEntries[path]) return;
+ dispatch('unpinFile');
+
const { fileHash } = state.treeEntries[path];
commit(types.SET_CURRENT_DIFF_FILE, fileHash);
@@ -667,11 +683,7 @@ export const goToFile = ({ state, commit, dispatch, getters }, { path }) => {
scrollToElement('.diff-files-holder', { duration: 0 });
if (!getters.isTreePathLoaded(path)) {
- dispatch('fetchFileByFile').catch(() => {
- createAlert({
- message: LOAD_SINGLE_DIFF_FAILED,
- });
- });
+ dispatch('fetchFileByFile');
}
}
};
@@ -995,6 +1007,8 @@ export const setCurrentDiffFileIdFromNote = ({ commit, getters, rootGetters }, n
};
export const navigateToDiffFileIndex = ({ state, getters, commit, dispatch }, index) => {
+ dispatch('unpinFile');
+
const { fileHash } = getters.flatBlobsList[index];
document.location.hash = fileHash;
@@ -1055,3 +1069,50 @@ export const toggleFileCommentForm = ({ state, commit }, filePath) => {
export const addDraftToFile = ({ commit }, { filePath, draft }) =>
commit(types.ADD_DRAFT_TO_FILE, { filePath, draft });
+
+export const fetchPinnedFile = ({ state, commit }, pinnedFileUrl) => {
+ const isNoteLink = isUrlHashNoteLink(window?.location?.hash);
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loading');
+ commit(types.SET_RETRIEVING_BATCHES, true);
+
+ return axios
+ .get(pinnedFileUrl)
+ .then(({ data: diffData }) => {
+ const [{ file_hash }] = diffData.diff_files;
+
+ // we must store pinned file in the `diffs`, otherwise collapsing and commenting on a file won't work
+ // once the same file arrives in a file batch we must only update its' position
+ // we also must not update file's position since it's loaded out of order
+ commit(types.SET_DIFF_DATA_BATCH, { diff_files: diffData.diff_files, updatePosition: false });
+ commit(types.SET_PINNED_FILE_HASH, file_hash);
+
+ if (!isNoteLink && !state.currentDiffFileId) {
+ commit(types.SET_CURRENT_DIFF_FILE, file_hash);
+ }
+
+ commit(types.SET_BATCH_LOADING_STATE, 'loaded');
+
+ setTimeout(() => {
+ handleLocationHash();
+ });
+
+ eventHub.$emit('diffFilesModified');
+ })
+ .catch((error) => {
+ commit(types.SET_BATCH_LOADING_STATE, 'error');
+ throw error;
+ })
+ .finally(() => {
+ commit(types.SET_RETRIEVING_BATCHES, false);
+ });
+};
+
+export const unpinFile = ({ getters, commit }) => {
+ if (!getters.pinnedFile) return;
+ commit(types.SET_PINNED_FILE_HASH, null);
+ const newUrl = new URL(window.location);
+ newUrl.searchParams.delete('pin');
+ newUrl.hash = '';
+ window.history.replaceState(null, undefined, newUrl);
+};
diff --git a/app/assets/javascripts/diffs/store/getters.js b/app/assets/javascripts/diffs/store/getters.js
index bfafb4d281d..f15c3d1693c 100644
--- a/app/assets/javascripts/diffs/store/getters.js
+++ b/app/assets/javascripts/diffs/store/getters.js
@@ -201,3 +201,18 @@ export const isVirtualScrollingEnabled = (state) => {
export const isBatchLoading = (state) => state.batchLoadingState === 'loading';
export const isBatchLoadingError = (state) => state.batchLoadingState === 'error';
+
+export const diffFiles = (state, getters) => {
+ const { pinnedFile } = getters;
+ if (pinnedFile) {
+ const diffs = state.diffFiles.slice(0);
+ diffs.splice(diffs.indexOf(pinnedFile), 1);
+ return [pinnedFile, ...diffs];
+ }
+ return state.diffFiles;
+};
+
+export const pinnedFile = (state) => {
+ if (!state.pinnedFileHash) return null;
+ return state.diffFiles.find((file) => file.file_hash === state.pinnedFileHash);
+};
diff --git a/app/assets/javascripts/diffs/store/modules/diff_state.js b/app/assets/javascripts/diffs/store/modules/diff_state.js
index d5e1a05f4a5..3b6408e6d78 100644
--- a/app/assets/javascripts/diffs/store/modules/diff_state.js
+++ b/app/assets/javascripts/diffs/store/modules/diff_state.js
@@ -37,4 +37,5 @@ export default () => ({
mrReviews: {},
latestDiff: true,
disableVirtualScroller: false,
+ pinnedFileHash: null,
});
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index b155804c70c..7b19f96b151 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -6,6 +6,7 @@ export const SET_RETRIEVING_BATCHES = 'SET_RETRIEVING_BATCHES';
export const SET_DIFF_METADATA = 'SET_DIFF_METADATA';
export const SET_DIFF_DATA_BATCH = 'SET_DIFF_DATA_BATCH';
export const SET_DIFF_FILES = 'SET_DIFF_FILES';
+export const SET_DIFF_TREE_ENTRY = 'SET_DIFF_TREE_ENTRY';
export const SET_MR_FILE_REVIEWS = 'SET_MR_FILE_REVIEWS';
@@ -23,6 +24,7 @@ export const TREE_ENTRY_DIFF_LOADING = 'TREE_ENTRY_DIFF_LOADING';
export const SET_SHOW_TREE_LIST = 'SET_SHOW_TREE_LIST';
export const SET_CURRENT_DIFF_FILE = 'SET_CURRENT_DIFF_FILE';
export const SET_DIFF_FILE_VIEWED = 'SET_DIFF_FILE_VIEWED';
+export const SET_PINNED_FILE_HASH = 'SET_PINNED_FILE_HASH';
export const OPEN_DIFF_FILE_COMMENT_FORM = 'OPEN_DIFF_FILE_COMMENT_FORM';
export const UPDATE_DIFF_FILE_COMMENT_FORM = 'UPDATE_DIFF_FILE_COMMENT_FORM';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index bc5ed3c40df..3c750830baa 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -81,15 +81,27 @@ export default {
});
},
- [types.SET_DIFF_DATA_BATCH](state, data) {
+ [types.SET_DIFF_DATA_BATCH](state, { diff_files: diffFiles, updatePosition = true }) {
Object.assign(state, {
diffFiles: prepareDiffData({
- diff: data,
+ diff: { diff_files: diffFiles },
priorFiles: state.diffFiles,
+ // when a pinned file is added to diffs its position may be incorrect since it's loaded out of order
+ // we need to ensure when we load it in batched request it updates it position
+ updatePosition,
}),
treeEntries: markTreeEntriesLoaded({
priorEntries: state.treeEntries,
- loadedFiles: data.diff_files,
+ loadedFiles: diffFiles,
+ }),
+ });
+ },
+
+ [types.SET_DIFF_TREE_ENTRY](state, diffFile) {
+ Object.assign(state, {
+ treeEntries: markTreeEntriesLoaded({
+ priorEntries: state.treeEntries,
+ loadedFiles: [diffFile],
}),
});
},
@@ -404,4 +416,7 @@ export default {
file?.drafts.push(draft);
},
+ [types.SET_PINNED_FILE_HASH](state, fileHash) {
+ state.pinnedFileHash = fileHash;
+ },
};
diff --git a/app/assets/javascripts/diffs/store/utils.js b/app/assets/javascripts/diffs/store/utils.js
index fb467a606b9..ad8cacf8504 100644
--- a/app/assets/javascripts/diffs/store/utils.js
+++ b/app/assets/javascripts/diffs/store/utils.js
@@ -395,9 +395,15 @@ function finalizeDiffFile(file) {
return file;
}
-function deduplicateFilesList(files) {
+function deduplicateFilesList(files, updatePosition) {
const dedupedFiles = files.reduce((newList, file) => {
const id = diffFileUniqueId(file);
+ if (updatePosition && id in newList) {
+ // Object.values preserves key order but doesn't update order when writing to the same key
+ // In order to update position of the item we have to delete it first and then add it back
+ // eslint-disable-next-line no-param-reassign
+ delete newList[id];
+ }
return {
...newList,
@@ -408,14 +414,20 @@ function deduplicateFilesList(files) {
return Object.values(dedupedFiles);
}
-export function prepareDiffData({ diff, priorFiles = [], meta = false }) {
- const cleanedFiles = (diff.diff_files || [])
- .map((file, index, allFiles) => prepareRawDiffFile({ file, allFiles, meta, index }))
- .map(ensureBasicDiffFileLines)
- .map(prepareDiffFileLines)
- .map((file) => finalizeDiffFile(file));
+export function prepareDiffData({ diff, priorFiles = [], meta = false, updatePosition = false }) {
+ const transformersChain = [
+ (file, index, allFiles) => prepareRawDiffFile({ file, index, allFiles, meta }),
+ ensureBasicDiffFileLines,
+ prepareDiffFileLines,
+ finalizeDiffFile,
+ ];
+ const cleanedFiles = (diff.diff_files || []).map((file, index, allFiles) => {
+ return transformersChain.reduce((fileResult, transformer) => {
+ return transformer(fileResult, index, allFiles);
+ }, file);
+ });
- return deduplicateFilesList([...priorFiles, ...cleanedFiles]);
+ return deduplicateFilesList([...priorFiles, ...cleanedFiles], updatePosition);
}
export function getDiffPositionByLineCode(diffFiles) {
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 9ee4f7cf4aa..17744e2c6ab 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -46,7 +46,6 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
let hasPlainText;
formTextarea.wrap('<div class="div-dropzone"></div>');
- formTextarea.on('paste', (event) => handlePaste(event));
// Add dropzone area to the form.
const $mdArea = formTextarea.closest('.md-area');
@@ -60,6 +59,8 @@ export default function dropzoneInput(form, config = { parallelUploads: 2 }) {
return null;
}
+ formTextarea.on('paste', (event) => handlePaste(event));
+
const dropzone = $formDropzone.dropzone({
url: uploadsPath,
dictDefaultMessage: '',
diff --git a/app/assets/javascripts/editor/schema/ci.json b/app/assets/javascripts/editor/schema/ci.json
index 1fb68394912..3fbd4728cfc 100644
--- a/app/assets/javascripts/editor/schema/ci.json
+++ b/app/assets/javascripts/editor/schema/ci.json
@@ -154,6 +154,9 @@
"always",
"never"
]
+ },
+ "auto_cancel": {
+ "$ref": "#/definitions/workflowAutoCancel"
}
},
"additionalProperties": false
@@ -528,6 +531,12 @@
"type": "string",
"minLength": 1,
"description": "Image architecture to pull."
+ },
+ "user": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Username or UID to use for the container."
}
}
},
@@ -603,6 +612,12 @@
"type": "string",
"minLength": 1,
"description": "Image architecture to pull."
+ },
+ "user": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 255,
+ "description": "Username or UID to use for the container."
}
}
},
@@ -735,6 +750,30 @@
}
]
},
+ "gcp_secret_manager": {
+ "type": "object",
+ "markdownDescription": "Defines the secret version to be fetched from GCP Secret Manager. Name refers to the secret name in GCP secret manager. Version refers to the desired secret version (defaults to 'latest').",
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "version": {
+ "oneOf": [
+ {
+ "type": "string"
+ },
+ {
+ "type": "integer"
+ }
+ ],
+ "default": "version"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "additionalProperties": false
+ },
"azure_key_vault": {
"type": "object",
"properties": {
@@ -757,7 +796,7 @@
},
"token": {
"type": "string",
- "description": "Specifies the JWT variable that should be used to authenticate with Hashicorp Vault."
+ "description": "Specifies the JWT variable that should be used to authenticate with the secret provider."
}
},
"anyOf": [
@@ -770,8 +809,18 @@
"required": [
"azure_key_vault"
]
+ },
+ {
+ "required": [
+ "gcp_secret_manager"
+ ]
}
],
+ "dependencies": {
+ "gcp_secret_manager": [
+ "token"
+ ]
+ },
"additionalProperties": false
}
}
@@ -944,7 +993,8 @@
},
"workflowAutoCancel": {
"type": "object",
- "markdownDescription": "Define the rules for when pipeline should be automatically cancelled.",
+ "description": "Define the rules for when pipeline should be automatically cancelled.",
+ "additionalProperties": false,
"properties": {
"on_job_failure": {
"markdownDescription": "Define which jobs to stop after a job fails.",
@@ -954,6 +1004,15 @@
"none",
"all"
]
+ },
+ "on_new_commit": {
+ "markdownDescription": "Configure the behavior of the auto-cancel redundant pipelines feature. [Learn More](https://docs.gitlab.com/ee/ci/yaml/#workflowauto_cancelon_new_commit)",
+ "type": "string",
+ "enum": [
+ "conservative",
+ "interruptible",
+ "none"
+ ]
}
}
},
diff --git a/app/assets/javascripts/entrypoints/analytics.js b/app/assets/javascripts/entrypoints/analytics.js
index e18c4bc8742..f5c61d82f64 100644
--- a/app/assets/javascripts/entrypoints/analytics.js
+++ b/app/assets/javascripts/entrypoints/analytics.js
@@ -13,6 +13,10 @@ if (appId && host) {
performanceTiming: false,
errorTracking: false,
},
+ pagePingTracking: {
+ minimumVisitLength: 10,
+ heartbeatDelay: 10,
+ },
});
const userId = window.gl?.snowplowStandardContext?.data?.user_id;
diff --git a/app/assets/javascripts/entrypoints/sandboxed_swagger.js b/app/assets/javascripts/entrypoints/sandboxed_swagger.js
new file mode 100644
index 00000000000..993896e6a2a
--- /dev/null
+++ b/app/assets/javascripts/entrypoints/sandboxed_swagger.js
@@ -0,0 +1 @@
+import '../lib/swagger';
diff --git a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
index 20ed67f6bd9..8b305160b2b 100644
--- a/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_status_bar.vue
@@ -6,6 +6,8 @@ import {
SYNC_STATUS_BADGES,
STATUS_TRUE,
STATUS_FALSE,
+ STATUS_UNKNOWN,
+ REASON_PROGRESSING,
HELM_RELEASES_RESOURCE_TYPE,
KUSTOMIZATIONS_RESOURCE_TYPE,
} from '../constants';
@@ -115,6 +117,15 @@ export default {
return condition.status === STATUS_TRUE && condition.type === 'Stalled';
});
},
+ fluxAnyReconcilingWithBadConfig() {
+ return this.fluxCRD.find((condition) => {
+ return (
+ condition.status === STATUS_UNKNOWN &&
+ condition.type === 'Ready' &&
+ condition.reason === REASON_PROGRESSING
+ );
+ });
+ },
fluxAnyReconciling() {
return this.fluxCRD.find((condition) => {
return condition.status === STATUS_TRUE && condition.type === 'Reconciling';
@@ -143,6 +154,12 @@ export default {
if (this.fluxAnyStalled) {
return { ...SYNC_STATUS_BADGES.stalled, popoverText: this.fluxAnyStalled.message };
}
+ if (this.fluxAnyReconcilingWithBadConfig) {
+ return {
+ ...SYNC_STATUS_BADGES.reconciling,
+ popoverText: this.fluxAnyReconcilingWithBadConfig.message,
+ };
+ }
if (this.fluxAnyReconciling) {
return SYNC_STATUS_BADGES.reconciling;
}
diff --git a/app/assets/javascripts/environments/components/kubernetes_tabs.vue b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
index 0d80b1fd797..da37df3fae7 100644
--- a/app/assets/javascripts/environments/components/kubernetes_tabs.vue
+++ b/app/assets/javascripts/environments/components/kubernetes_tabs.vue
@@ -1,9 +1,12 @@
<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 {
+ getAge,
+ generateServicePortsString,
+} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
+import { SERVICES_TABLE_FIELDS } from '~/kubernetes_dashboard/constants';
import k8sServicesQuery from '../graphql/queries/k8s_services.query.graphql';
-import { generateServicePortsString } from '../helpers/k8s_integration_helper';
import { SERVICES_LIMIT_PER_PAGE } from '../constants';
import KubernetesSummary from './kubernetes_summary.vue';
@@ -82,6 +85,14 @@ export default {
? null
: nextPage;
},
+ servicesFields() {
+ return SERVICES_TABLE_FIELDS.map((field) => {
+ return {
+ ...field,
+ thClass: tableHeadingClasses,
+ };
+ });
+ },
},
i18n: {
servicesTitle: s__('Environment|Services'),
@@ -94,43 +105,6 @@ export default {
ports: s__('Environment|Ports'),
age: s__('Environment|Age'),
},
- servicesFields: [
- {
- key: 'name',
- label: __('Name'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'namespace',
- label: __('Namespace'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'type',
- label: __('Type'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'clusterIP',
- label: s__('Environment|Cluster IP'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'externalIP',
- label: s__('Environment|External IP'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'ports',
- label: s__('Environment|Ports'),
- thClass: tableHeadingClasses,
- },
- {
- key: 'age',
- label: s__('Environment|Age'),
- thClass: tableHeadingClasses,
- },
- ],
SERVICES_LIMIT_PER_PAGE,
};
</script>
@@ -154,7 +128,7 @@ export default {
<gl-table
v-else
- :fields="$options.servicesFields"
+ :fields="servicesFields"
:items="servicesItems"
:per-page="$options.SERVICES_LIMIT_PER_PAGE"
:current-page="currentPage"
diff --git a/app/assets/javascripts/environments/components/new_environment_item.vue b/app/assets/javascripts/environments/components/new_environment_item.vue
index aacb460a817..8bd55e697fa 100644
--- a/app/assets/javascripts/environments/components/new_environment_item.vue
+++ b/app/assets/javascripts/environments/components/new_environment_item.vue
@@ -368,6 +368,7 @@ export default {
</div>
<div v-if="clusterAgent" :class="$options.kubernetesOverviewClasses">
<kubernetes-overview
+ :class="{ 'gl-ml-7': inFolder }"
:cluster-agent="clusterAgent"
:namespace="kubernetesNamespace"
:flux-resource-path="fluxResourcePath"
diff --git a/app/assets/javascripts/environments/components/stop_environment_modal.vue b/app/assets/javascripts/environments/components/stop_environment_modal.vue
index b583694e154..76fa0c9bcb2 100644
--- a/app/assets/javascripts/environments/components/stop_environment_modal.vue
+++ b/app/assets/javascripts/environments/components/stop_environment_modal.vue
@@ -1,13 +1,15 @@
<script>
import { GlSprintf, GlTooltipDirective, GlModal } from '@gitlab/ui';
import { __, s__ } from '~/locale';
-import { DOCS_URL_IN_EE_DIR } from 'jh_else_ce/lib/utils/url_utility';
+import { helpPagePath } from '~/helpers/help_page_helper';
import eventHub from '../event_hub';
import stopEnvironmentMutation from '../graphql/mutations/stop_environment.mutation.graphql';
export default {
- yamlDocsLink: `${DOCS_URL_IN_EE_DIR}/ee/ci/yaml/`,
- stoppingEnvironmentDocsLink: `${DOCS_URL_IN_EE_DIR}/environments/#stopping-an-environment`,
+ yamlDocsLink: helpPagePath('ci/yaml/index'),
+ stoppingEnvironmentDocsLink: helpPagePath('ci/environments/index', {
+ anchor: 'stopping-an-environment',
+ }),
id: 'stop-environment-modal',
name: 'StopEnvironmentModal',
diff --git a/app/assets/javascripts/environments/constants.js b/app/assets/javascripts/environments/constants.js
index 2fe9008c042..c996d70af52 100644
--- a/app/assets/javascripts/environments/constants.js
+++ b/app/assets/javascripts/environments/constants.js
@@ -154,6 +154,9 @@ export const SYNC_STATUS_BADGES = {
export const STATUS_TRUE = 'True';
export const STATUS_FALSE = 'False';
+export const STATUS_UNKNOWN = 'Unknown';
+
+export const REASON_PROGRESSING = 'Progressing';
const ERROR_UNAUTHORIZED = 'unauthorized';
const ERROR_FORBIDDEN = 'forbidden';
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 0201fb53f77..05a2eba6af3 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -3,6 +3,7 @@ import VueApollo from 'vue-apollo';
import VueRouter from 'vue-router';
import createDefaultClient from '~/lib/graphql';
import Translate from '~/vue_shared/translate';
+import { removeLastSlashInUrlPath } from '~/lib/utils/url_utility';
import { apolloProvider } from '../graphql/client';
import EnvironmentsFolderView from './environments_folder_view.vue';
import EnvironmentsFolderApp from './environments_folder_app.vue';
@@ -20,10 +21,9 @@ export default () => {
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 folderPath = environmentsData.endpoint.replace('.json', '');
+ const kasTunnelUrl = removeLastSlashInUrlPath(environmentsData.kasTunnelUrl);
+ const { projectPath, folderName, helpPagePath } = environmentsData;
const router = new VueRouter({
mode: 'history',
@@ -54,6 +54,7 @@ export default () => {
provide: {
projectPath,
helpPagePath,
+ kasTunnelUrl,
},
apolloProvider,
router,
@@ -74,8 +75,8 @@ export default () => {
},
data() {
return {
- endpoint: environmentsData.environmentsDataEndpoint,
- folderName: environmentsData.environmentsDataFolderName,
+ endpoint: environmentsData.endpoint,
+ folderName: environmentsData.folderName,
cssContainerClass: environmentsData.cssClass,
};
},
diff --git a/app/assets/javascripts/environments/graphql/client.js b/app/assets/javascripts/environments/graphql/client.js
index 0eb12427914..de89934cd52 100644
--- a/app/assets/javascripts/environments/graphql/client.js
+++ b/app/assets/javascripts/environments/graphql/client.js
@@ -174,6 +174,8 @@ export const apolloProvider = (endpoint) => {
cache.writeQuery({
query: fluxKustomizationStatusQuery,
data: {
+ message: '',
+ reason: '',
status: '',
type: '',
},
@@ -181,6 +183,8 @@ export const apolloProvider = (endpoint) => {
cache.writeQuery({
query: fluxHelmReleaseStatusQuery,
data: {
+ message: '',
+ reason: '',
status: '',
type: '',
},
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
index 042bdc1992d..35f7fe56b47 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_helm_release_status.query.graphql
@@ -2,6 +2,7 @@ query getFluxHelmReleaseStatusQuery($configuration: LocalConfiguration, $fluxRes
fluxHelmReleaseStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
@client {
message
+ reason
status
type
}
diff --git a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
index 458b8a4d9db..8564b306d5b 100644
--- a/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
+++ b/app/assets/javascripts/environments/graphql/queries/flux_kustomization_status.query.graphql
@@ -5,6 +5,7 @@ query getFluxHelmKustomizationStatusQuery(
fluxKustomizationStatus(configuration: $configuration, fluxResourcePath: $fluxResourcePath)
@client {
message
+ reason
status
type
}
diff --git a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
index bb5cab7c279..99d5ee44b6c 100644
--- a/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/environments/helpers/k8s_integration_helper.js
@@ -2,21 +2,17 @@ import {
calculateDeploymentStatus,
calculateStatefulSetStatus,
calculateDaemonSetStatus,
+ calculateJobStatus,
+ calculateCronJobStatus,
} from '~/kubernetes_dashboard/helpers/k8s_integration_helper';
-import { STATUS_READY, STATUS_FAILED } from '~/kubernetes_dashboard/constants';
+import {
+ STATUS_READY,
+ STATUS_FAILED,
+ STATUS_COMPLETED,
+ STATUS_SUSPENDED,
+} from '~/kubernetes_dashboard/constants';
import { CLUSTER_AGENT_ERROR_MESSAGES } from '../constants';
-export function generateServicePortsString(ports) {
- if (!ports?.length) return '';
-
- return ports
- .map((port) => {
- const nodePort = port.nodePort ? `:${port.nodePort}` : '';
- return `${port.port}${nodePort}/${port.protocol}`;
- })
- .join(', ');
-}
-
export function getDeploymentsStatuses(items) {
const failed = [];
const ready = [];
@@ -45,13 +41,14 @@ export function getDeploymentsStatuses(items) {
};
}
+const isCompleted = (status) => status === STATUS_COMPLETED;
+const isReady = (status) => status === STATUS_READY;
+const isFailed = (status) => status === STATUS_FAILED;
+const isSuspended = (status) => status === STATUS_SUSPENDED;
+
export function getDaemonSetStatuses(items) {
- const failed = items.filter((item) => {
- return calculateDaemonSetStatus(item) === STATUS_FAILED;
- });
- const ready = items.filter((item) => {
- return calculateDaemonSetStatus(item) === STATUS_READY;
- });
+ const failed = items.filter((item) => isFailed(calculateDaemonSetStatus(item)));
+ const ready = items.filter((item) => isReady(calculateDaemonSetStatus(item)));
return {
...(failed.length && { failed }),
@@ -60,12 +57,8 @@ export function getDaemonSetStatuses(items) {
}
export function getStatefulSetStatuses(items) {
- const failed = items.filter((item) => {
- return calculateStatefulSetStatus(item) === STATUS_FAILED;
- });
- const ready = items.filter((item) => {
- return calculateStatefulSetStatus(item) === STATUS_READY;
- });
+ const failed = items.filter((item) => isFailed(calculateStatefulSetStatus(item)));
+ const ready = items.filter((item) => isReady(calculateStatefulSetStatus(item)));
return {
...(failed.length && { failed }),
@@ -74,12 +67,8 @@ export function getStatefulSetStatuses(items) {
}
export function getReplicaSetStatuses(items) {
- const failed = items.filter((item) => {
- return calculateStatefulSetStatus(item) === STATUS_FAILED;
- });
- const ready = items.filter((item) => {
- return calculateStatefulSetStatus(item) === STATUS_READY;
- });
+ const failed = items.filter((item) => isFailed(calculateStatefulSetStatus(item)));
+ const ready = items.filter((item) => isReady(calculateStatefulSetStatus(item)));
return {
...(failed.length && { failed }),
@@ -88,12 +77,8 @@ export function getReplicaSetStatuses(items) {
}
export function getJobsStatuses(items) {
- const failed = items.filter((item) => {
- return item.status.failed > 0 || item.status?.succeeded !== item.spec?.completions;
- });
- const completed = items.filter((item) => {
- return item.status?.succeeded === item.spec?.completions;
- });
+ const failed = items.filter((item) => isFailed(calculateJobStatus(item)));
+ const completed = items.filter((item) => isCompleted(calculateJobStatus(item)));
return {
...(failed.length && { failed }),
@@ -107,11 +92,11 @@ export function getCronJobsStatuses(items) {
const suspended = [];
items.forEach((item) => {
- if (item.status?.active > 0 && !item.status?.lastScheduleTime) {
+ if (isFailed(calculateCronJobStatus(item))) {
failed.push(item);
- } else if (item.spec?.suspend) {
+ } else if (isSuspended(calculateCronJobStatus(item))) {
suspended.push(item);
- } else if (item.status?.lastScheduleTime) {
+ } else if (isReady(calculateCronJobStatus(item))) {
ready.push(item);
}
});
diff --git a/app/assets/javascripts/error_tracking/components/error_details_info.vue b/app/assets/javascripts/error_tracking/components/error_details_info.vue
index 0b4eabe25d1..3db946acdfc 100644
--- a/app/assets/javascripts/error_tracking/components/error_details_info.vue
+++ b/app/assets/javascripts/error_tracking/components/error_details_info.vue
@@ -74,7 +74,9 @@ export default {
</template>
</gl-card>
+ <!-- user count is currently not supported for integrated error tracking https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2345 -->
<gl-card
+ v-if="!error.integrated"
:class="$options.CARD_CLASS"
:body-class="$options.BODY_CLASS"
:header-class="$options.HEADER_CLASS"
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 95ae5e5a92c..95b47a9e491 100644
--- a/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
+++ b/app/assets/javascripts/error_tracking/components/error_tracking_list.vue
@@ -76,7 +76,7 @@ export default {
{
key: 'status',
label: '',
- tdClass: `${tableDataClass}`,
+ tdClass: `${tableDataClass} gl-text-center`,
},
],
statusFilters: {
@@ -182,6 +182,13 @@ export default {
showIntegratedDisabledAlert() {
return !this.isAlertDismissed && this.showIntegratedTrackingDisabledAlert;
},
+ fields() {
+ if (this.integratedErrorTrackingEnabled) {
+ // user count is currently not supported for integrated error tracking https://gitlab.com/gitlab-org/opstrace/opstrace/-/issues/2345
+ return this.$options.fields.filter((field) => field.key !== 'users');
+ }
+ return this.$options.fields;
+ },
},
watch: {
pagination() {
@@ -417,7 +424,7 @@ export default {
<gl-table
class="error-list-table gl-mt-5"
:items="errors"
- :fields="$options.fields"
+ :fields="fields"
:show-empty="true"
fixed
stacked="md"
diff --git a/app/assets/javascripts/gfm_auto_complete.js b/app/assets/javascripts/gfm_auto_complete.js
index b11f7b1ba76..f1e46262b2f 100644
--- a/app/assets/javascripts/gfm_auto_complete.js
+++ b/app/assets/javascripts/gfm_auto_complete.js
@@ -3,6 +3,7 @@ import '~/lib/utils/jquery_at_who';
import { escape as lodashEscape, sortBy, template, escapeRegExp } from 'lodash';
import * as Emoji from '~/emoji';
import axios from '~/lib/utils/axios_utils';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import { loadingIconForLegacyJS } from '~/loading_icon_for_legacy_js';
import { s__, __, sprintf } from '~/locale';
import { isUserBusy } from '~/set_status_modal/utils';
@@ -26,6 +27,8 @@ export const CONTACT_STATE_ACTIVE = 'active';
export const CONTACTS_ADD_COMMAND = '/add_contacts';
export const CONTACTS_REMOVE_COMMAND = '/remove_contacts';
+const useMentionsBackendFiltering = window.gon.features?.mentionAutocompleteBackendFiltering;
+
/**
* Escapes user input before we pass it to at.js, which
* renders it as HTML in the autocomplete dropdown.
@@ -62,6 +65,8 @@ export function showAndHideHelper($input, alias = '') {
});
}
+// This should be kept in sync with the backend filtering in
+// `User#gfm_autocomplete_search` and `Namespace#gfm_autocomplete_search`
function createMemberSearchString(member) {
return `${member.name.replace(/ /g, '')} ${member.username}`;
}
@@ -344,6 +349,7 @@ class GfmAutoComplete {
}
setupMembers($input) {
+ const instance = this;
const fetchData = this.fetchData.bind(this);
const MEMBER_COMMAND = {
ASSIGN: '/assign',
@@ -383,6 +389,7 @@ class GfmAutoComplete {
// eslint-disable-next-line no-template-curly-in-string
insertTpl: '${atwho-at}${username}',
limit: 10,
+ delay: useMentionsBackendFiltering ? DEFAULT_DEBOUNCE_AND_THROTTLE_MS : null,
searchKey: 'search',
alwaysHighlightFirst: true,
skipSpecialCharacterTest: true,
@@ -409,16 +416,19 @@ class GfmAutoComplete {
const match = GfmAutoComplete.defaultMatcher(flag, subtext, this.app.controllers);
return match && match.length ? match[1] : null;
},
- filter(query, data, searchKey) {
- if (GfmAutoComplete.isLoading(data)) {
+ filter(query, data) {
+ if (useMentionsBackendFiltering) {
+ if (GfmAutoComplete.isLoading(data) || instance.previousQuery !== query) {
+ instance.previousQuery = query;
+
+ fetchData(this.$inputor, this.at, query);
+ return data;
+ }
+ } else if (GfmAutoComplete.isLoading(data)) {
fetchData(this.$inputor, this.at);
return data;
}
- if (data === GfmAutoComplete.defaultLoadingData) {
- return $.fn.atwho.default.callbacks.filter(query, data, searchKey);
- }
-
if (command === MEMBER_COMMAND.ASSIGN) {
// Only include members which are not assigned to Issuable currently
return data.filter((member) => !assignees.includes(member.search));
@@ -988,6 +998,11 @@ GfmAutoComplete.atTypeMap = {
};
GfmAutoComplete.typesWithBackendFiltering = ['vulnerabilities'];
+
+if (useMentionsBackendFiltering) {
+ GfmAutoComplete.typesWithBackendFiltering.push('members');
+}
+
GfmAutoComplete.isTypeWithBackendFiltering = (type) =>
GfmAutoComplete.typesWithBackendFiltering.includes(GfmAutoComplete.atTypeMap[type]);
@@ -1040,6 +1055,8 @@ GfmAutoComplete.Members = {
// `member.search` is a name:username string like `MargeSimpson msimpson`
return member.search.toLowerCase().includes(query);
},
+ // This should be kept in sync with the backend sorting in
+ // `User#gfm_autocomplete_search` and `Namespace#gfm_autocomplete_search`
sort(query, members) {
const lowercaseQuery = query.toLowerCase();
const { nameOrUsernameStartsWith, nameOrUsernameIncludes } = GfmAutoComplete.Members;
diff --git a/app/assets/javascripts/graphql_shared/issuable_client.js b/app/assets/javascripts/graphql_shared/issuable_client.js
index d0ba34b6127..8e19de9f7c2 100644
--- a/app/assets/javascripts/graphql_shared/issuable_client.js
+++ b/app/assets/javascripts/graphql_shared/issuable_client.js
@@ -1,6 +1,5 @@
import produce from 'immer';
import VueApollo from 'vue-apollo';
-import { defaultDataIdFromObject } from '@apollo/client/core';
import { concatPagination } from '@apollo/client/utilities';
import errorQuery from '~/boards/graphql/client/error.query.graphql';
import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
@@ -14,13 +13,6 @@ import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_
export const config = {
typeDefs,
cacheConfig: {
- // included temporarily until Vuex is removed from boards app
- dataIdFromObject: (object) => {
- // eslint-disable-next-line no-underscore-dangle
- return object.__typename === 'BoardList' && !window.gon?.features?.apolloBoards
- ? object.iid
- : defaultDataIdFromObject(object);
- },
typePolicies: {
Query: {
fields: {
@@ -34,6 +26,12 @@ export const config = {
return currentState ?? [];
},
},
+ boardList: {
+ keyArgs: ['id'],
+ },
+ epicBoardList: {
+ keyArgs: ['id'],
+ },
},
},
Project: {
@@ -135,104 +133,80 @@ export const config = {
nodes: concatPagination(),
},
},
- ...(window.gon?.features?.apolloBoards
- ? {
- BoardList: {
- fields: {
- issues: {
- keyArgs: ['filters'],
- },
- },
- },
- IssueConnection: {
- merge(existing = { nodes: [] }, incoming, { args }) {
- if (!args?.after) {
- return incoming;
- }
- return {
- ...incoming,
- nodes: [...existing.nodes, ...incoming.nodes],
- };
- },
- },
- EpicList: {
- fields: {
- epics: {
- keyArgs: ['filters'],
- },
- },
- },
- EpicConnection: {
- merge(existing = { nodes: [] }, incoming, { args }) {
- if (!args.after) {
- return incoming;
- }
- return {
- ...incoming,
- nodes: [...existing.nodes, ...incoming.nodes],
- };
- },
- },
- Group: {
- fields: {
- projects: {
- keyArgs: ['includeSubgroups', 'search'],
- },
- descendantGroups: {
- keyArgs: ['includeSubgroups', 'search'],
- },
- },
- },
- ProjectConnection: {
- fields: {
- nodes: concatPagination(),
- },
- },
- GroupConnection: {
- fields: {
- nodes: concatPagination(),
- },
- },
- Board: {
- fields: {
- epics: {
- keyArgs: ['boardId'],
- },
- },
- },
- BoardEpicConnection: {
- merge(existing = { nodes: [] }, incoming, { args }) {
- if (!args.after) {
- return incoming;
- }
- return {
- ...incoming,
- nodes: [...existing.nodes, ...incoming.nodes],
- };
- },
- },
- Query: {
- fields: {
- boardList: {
- keyArgs: ['id'],
- },
- epicBoardList: {
- keyArgs: ['id'],
- },
- isShowingLabels: {
- read(currentState) {
- return currentState ?? true;
- },
- },
- selectedBoardItems: {
- read(currentState) {
- return currentState ?? [];
- },
- },
- },
- },
+ BoardList: {
+ fields: {
+ issues: {
+ keyArgs: ['filters'],
+ },
+ },
+ },
+ IssueConnection: {
+ merge(existing = { nodes: [] }, incoming, { args }) {
+ if (!args?.after) {
+ return incoming;
}
- : {}),
+ return {
+ ...incoming,
+ nodes: [...existing.nodes, ...incoming.nodes],
+ };
+ },
+ },
+ EpicList: {
+ fields: {
+ epics: {
+ keyArgs: ['filters'],
+ },
+ },
+ },
+ EpicConnection: {
+ merge(existing = { nodes: [] }, incoming, { args }) {
+ if (!args?.after) {
+ return incoming;
+ }
+ return {
+ ...incoming,
+ nodes: [...existing.nodes, ...incoming.nodes],
+ };
+ },
+ },
+ Group: {
+ fields: {
+ projects: {
+ keyArgs: ['includeSubgroups', 'search'],
+ },
+ descendantGroups: {
+ keyArgs: ['includeSubgroups', 'search'],
+ },
+ },
+ },
+ ProjectConnection: {
+ fields: {
+ nodes: concatPagination(),
+ },
+ },
+ GroupConnection: {
+ fields: {
+ nodes: concatPagination(),
+ },
+ },
+ Board: {
+ fields: {
+ epics: {
+ keyArgs: ['boardId'],
+ },
+ },
+ },
+ BoardEpicConnection: {
+ merge(existing = { nodes: [] }, incoming, { args }) {
+ if (!args.after) {
+ return incoming;
+ }
+ return {
+ ...incoming,
+ nodes: [...existing.nodes, ...incoming.nodes],
+ };
+ },
+ },
},
},
};
diff --git a/app/assets/javascripts/graphql_shared/possible_types.json b/app/assets/javascripts/graphql_shared/possible_types.json
index 4edad63cc79..fe151c2b358 100644
--- a/app/assets/javascripts/graphql_shared/possible_types.json
+++ b/app/assets/javascripts/graphql_shared/possible_types.json
@@ -187,6 +187,7 @@
"WorkItemWidget": [
"WorkItemWidgetAssignees",
"WorkItemWidgetAwardEmoji",
+ "WorkItemWidgetColor",
"WorkItemWidgetCurrentUserTodos",
"WorkItemWidgetDescription",
"WorkItemWidgetHealthStatus",
@@ -199,6 +200,7 @@
"WorkItemWidgetNotifications",
"WorkItemWidgetProgress",
"WorkItemWidgetRequirementLegacy",
+ "WorkItemWidgetRolledupDates",
"WorkItemWidgetStartAndDueDate",
"WorkItemWidgetStatus",
"WorkItemWidgetTestReports",
diff --git a/app/assets/javascripts/graphql_shared/utils.js b/app/assets/javascripts/graphql_shared/utils.js
index 6a64e8a2fa8..828a18a240e 100644
--- a/app/assets/javascripts/graphql_shared/utils.js
+++ b/app/assets/javascripts/graphql_shared/utils.js
@@ -50,10 +50,10 @@ export const getTypeFromGraphQLId = (gid = '') => {
return type || null;
};
-export const MutationOperationMode = {
- Append: 'APPEND',
- Remove: 'REMOVE',
- Replace: 'REPLACE',
+export const mutationOperationMode = {
+ append: 'APPEND',
+ remove: 'REMOVE',
+ replace: 'REPLACE',
};
/**
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index 3440bd87e6b..4ede8fda01d 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -22,6 +22,7 @@ export default {
GlLoadingIcon,
GlEmptyState,
},
+ inject: ['emptySearchIllustration'],
props: {
action: {
type: String,
@@ -245,6 +246,7 @@ export default {
<groups-component v-if="hasGroups" :groups="groups" :page-info="pageInfo" :action="action" />
<gl-empty-state
v-else-if="fromSearch"
+ :svg-path="emptySearchIllustration"
:title="$options.i18n.searchEmptyState.title"
:description="$options.i18n.searchEmptyState.description"
data-testid="search-empty-state"
diff --git a/app/assets/javascripts/groups/components/group_item.vue b/app/assets/javascripts/groups/components/group_item.vue
index 3a08e3e546f..6e347a3c95b 100644
--- a/app/assets/javascripts/groups/components/group_item.vue
+++ b/app/assets/javascripts/groups/components/group_item.vue
@@ -20,6 +20,7 @@ import {
VISIBILITY_LEVELS_STRING_TO_INTEGER,
VISIBILITY_TYPE_ICON,
GROUP_VISIBILITY_TYPE,
+ PROJECT_VISIBILITY_TYPE,
} from '~/visibility_level/constants';
import { ITEM_TYPE, ACTIVE_TAB_SHARED } from '../constants';
@@ -105,7 +106,8 @@ export default {
return VISIBILITY_TYPE_ICON[this.group.visibility];
},
visibilityTooltip() {
- return GROUP_VISIBILITY_TYPE[this.group.visibility];
+ if (this.isGroup) return GROUP_VISIBILITY_TYPE[this.group.visibility];
+ return PROJECT_VISIBILITY_TYPE[this.group.visibility];
},
microdata() {
return this.group.microdata || {};
diff --git a/app/assets/javascripts/groups/components/group_name_and_path.vue b/app/assets/javascripts/groups/components/group_name_and_path.vue
index 853fdd7c55e..83953feedbf 100644
--- a/app/assets/javascripts/groups/components/group_name_and_path.vue
+++ b/app/assets/javascripts/groups/components/group_name_and_path.vue
@@ -39,6 +39,9 @@ export default {
'Groups|Must start with letter, digit, emoji, or underscore. Can also contain periods, dashes, spaces, and parentheses.',
),
invalidFeedback: s__('Groups|Enter a descriptive name for your group.'),
+ warningForUsingDotInName: s__(
+ 'Groups|Your group name must not contain a period if you intend to use SCIM integration, as it can lead to errors.',
+ ),
},
path: {
placeholder: __('my-awesome-group'),
@@ -299,6 +302,14 @@ export default {
@invalid="handleInvalidName"
/>
</gl-form-group>
+ <gl-alert
+ class="gl-mb-5"
+ :dismissible="false"
+ variant="warning"
+ data-testid="dot-in-path-alert"
+ >
+ {{ $options.i18n.inputs.name.warningForUsingDotInName }}
+ </gl-alert>
<div :class="newSubgroup && 'row gl-mb-3'">
<gl-form-group v-if="newSubgroup" class="col-sm-6 gl-pr-0" :label="inputLabels.subgroupPath">
@@ -386,7 +397,12 @@ export default {
</div>
<template v-if="isEditingGroup">
- <gl-alert class="gl-mb-5" :dismissible="false" variant="warning">
+ <gl-alert
+ class="gl-mb-5"
+ :dismissible="false"
+ variant="warning"
+ data-testid="changing-url-alert"
+ >
{{ $options.i18n.changingUrlWarningMessage }}
<gl-link :href="$options.changingGroupPathHelpPagePath"
>{{ $options.i18n.learnMore }}
diff --git a/app/assets/javascripts/groups/components/overview_tabs.vue b/app/assets/javascripts/groups/components/overview_tabs.vue
index 8781f03a412..82bd4765986 100644
--- a/app/assets/javascripts/groups/components/overview_tabs.vue
+++ b/app/assets/javascripts/groups/components/overview_tabs.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTabs, GlTab, GlSearchBoxByType, GlSorting, GlSortingItem } from '@gitlab/ui';
+import { GlTabs, GlTab, GlSearchBoxByType, GlSorting } from '@gitlab/ui';
import { isString, debounce } from 'lodash';
import { __ } from '~/locale';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -30,7 +30,6 @@ export default {
GroupsApp,
GlSearchBoxByType,
GlSorting,
- GlSortingItem,
SubgroupsAndProjectsEmptyState,
SharedProjectsEmptyState,
ArchivedProjectsEmptyState,
@@ -84,6 +83,9 @@ export default {
sortQueryStringValue() {
return this.isAscending ? this.sort.asc : this.sort.desc;
},
+ activeTabSortOptions() {
+ return this.activeTab.sortingItems.map(({ label }) => ({ value: label, text: label }));
+ },
},
mounted() {
this.search = this.$route.query?.filter || '';
@@ -178,12 +180,14 @@ export default {
this.handleSearchOrSortChange();
},
- handleSortingItemClick(sortingItem) {
- if (sortingItem === this.sort) {
+ handleSortingItemClick(value) {
+ const selectedSortingItem = this.activeTab.sortingItems.find((item) => item.label === value);
+
+ if (selectedSortingItem === this.sort) {
return;
}
- this.sort = sortingItem;
+ this.sort = selectedSortingItem;
this.handleSearchOrSortChange();
},
@@ -239,16 +243,11 @@ export default {
data-testid="group_sort_by_dropdown"
:text="sort.label"
:is-ascending="isAscending"
+ :sort-options="activeTabSortOptions"
+ :sort-by="sort.label"
+ @sortByChange="handleSortingItemClick"
@sortDirectionChange="handleSortDirectionChange"
- >
- <gl-sorting-item
- v-for="sortingItem in activeTab.sortingItems"
- :key="sortingItem.label"
- :active="sortingItem === sort"
- @click="handleSortingItemClick(sortingItem)"
- >{{ sortingItem.label }}</gl-sorting-item
- >
- </gl-sorting>
+ />
</div>
</div>
</li>
diff --git a/app/assets/javascripts/groups/init_overview_tabs.js b/app/assets/javascripts/groups/init_overview_tabs.js
index 80dd1d36734..2f03705b453 100644
--- a/app/assets/javascripts/groups/init_overview_tabs.js
+++ b/app/assets/javascripts/groups/init_overview_tabs.js
@@ -47,6 +47,7 @@ export const initGroupOverviewTabs = () => {
newProjectIllustration,
emptyProjectsIllustration,
emptySubgroupIllustration,
+ emptySearchIllustration,
canCreateSubgroups,
canCreateProjects,
currentGroupVisibility,
@@ -67,6 +68,7 @@ export const initGroupOverviewTabs = () => {
newProjectIllustration,
emptyProjectsIllustration,
emptySubgroupIllustration,
+ emptySearchIllustration,
canCreateSubgroups: parseBoolean(canCreateSubgroups),
canCreateProjects: parseBoolean(canCreateProjects),
currentGroupVisibility,
diff --git a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
index 5560d10d179..37f95a7ab30 100644
--- a/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/groups/settings/api/access_dropdown_api.js
@@ -7,10 +7,15 @@ const buildUrl = (urlRoot, url) => {
return joinPaths(urlRoot, url);
};
-export const getSubGroups = () => {
+const defaultOptions = { includeParentDescendants: false };
+
+export const getSubGroups = (options = defaultOptions) => {
+ const { includeParentDescendants } = options;
+
return axios.get(buildUrl(gon.relative_url_root || '', GROUP_SUBGROUPS_PATH), {
params: {
group_id: gon.current_group_id,
+ include_parent_descendants: includeParentDescendants,
},
});
};
diff --git a/app/assets/javascripts/groups/settings/components/access_dropdown.vue b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
index 457a2db174c..fee1383c8e2 100644
--- a/app/assets/javascripts/groups/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/groups/settings/components/access_dropdown.vue
@@ -93,7 +93,11 @@ export default {
this.loading = true;
if (this.hasLicense) {
- Promise.all([this.groups.length ? Promise.resolve({ data: this.groups }) : getSubGroups()])
+ Promise.all([
+ this.groups.length
+ ? Promise.resolve({ data: this.groups })
+ : getSubGroups({ includeParentDescendants: true }),
+ ])
.then(([groupsResponse]) => {
this.consolidateData(groupsResponse.data);
this.setSelected({ initial });
diff --git a/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue b/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue
index 76b0b819698..238e382fabe 100644
--- a/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue
+++ b/app/assets/javascripts/groups_projects/components/more_actions_dropdown.vue
@@ -2,6 +2,7 @@
import {
GlButton,
GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
GlDisclosureDropdown,
GlIcon,
GlTooltipDirective,
@@ -13,6 +14,7 @@ export default {
components: {
GlButton,
GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
GlDisclosureDropdown,
GlIcon,
},
@@ -32,6 +34,9 @@ export default {
namespaceType() {
return this.isGroup ? WORKSPACE_GROUP : WORKSPACE_PROJECT;
},
+ hasPath() {
+ return this.leavePath || this.withdrawPath || this.requestAccessPath;
+ },
leaveTitle() {
return this.isGroup
? this.$options.i18n.groupLeaveTitle
@@ -143,12 +148,12 @@ export default {
</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-group v-if="hasPath" bordered>
+ <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-group>
</gl-disclosure-dropdown>
</template>
diff --git a/app/assets/javascripts/ide/components/file_alert.vue b/app/assets/javascripts/ide/components/file_alert.vue
deleted file mode 100644
index 2a894596bf4..00000000000
--- a/app/assets/javascripts/ide/components/file_alert.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-import { GlAlert } from '@gitlab/ui';
-import { getAlert } from '../lib/alerts';
-
-export default {
- components: {
- GlAlert,
- },
- props: {
- alertKey: {
- type: Symbol,
- required: true,
- },
- },
- computed: {
- alert() {
- return getAlert(this.alertKey);
- },
- },
-};
-</script>
-<template>
- <gl-alert v-bind="alert.props" @dismiss="alert.dismiss($store)">
- <component :is="alert.message" />
- </gl-alert>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index 8f4f777d396..d2375078820 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,5 @@
<script>
import { GlTabs, GlTab } from '@gitlab/ui';
-import { debounce } from 'lodash';
// eslint-disable-next-line no-restricted-imports
import { mapState, mapGetters, mapActions } from 'vuex';
import {
@@ -29,7 +28,7 @@ import { viewerInformationForPath } from '~/vue_shared/components/content_viewer
import DiffViewer from '~/vue_shared/components/diff_viewer/diff_viewer.vue';
import { markRaw } from '~/lib/utils/vue3compat/mark_raw';
import { readFileAsDataURL } from '~/lib/utils/file_utility';
-import { isDefaultCiConfig, hasCiConfigExtension } from '~/lib/utils/common_utils';
+import { hasCiConfigExtension } from '~/lib/utils/common_utils';
import {
leftSidebarViews,
@@ -43,7 +42,6 @@ import mapRulesToMonaco from '../lib/editorconfig/rules_mapper';
import { getFileEditorOrDefault } from '../stores/modules/editor/utils';
import { extractMarkdownImagesFromEntries } from '../stores/utils';
import { getPathParent, registerSchema, isTextFile } from '../utils';
-import FileAlert from './file_alert.vue';
import FileTemplatesBar from './file_templates/bar.vue';
const MARKDOWN_FILE_TYPE = 'markdown';
@@ -53,7 +51,6 @@ export default {
components: {
GlTabs,
GlTab,
- FileAlert,
ContentViewer,
DiffViewer,
FileTemplatesBar,
@@ -93,7 +90,6 @@ export default {
'previewMarkdownPath',
]),
...mapGetters([
- 'getAlert',
'currentMergeRequest',
'getStagedFile',
'isEditModeActive',
@@ -102,9 +98,6 @@ export default {
'getJsonSchemaForPath',
]),
...mapGetters('fileTemplates', ['showFileTemplatesBar']),
- alertKey() {
- return this.getAlert(this.file);
- },
fileEditor() {
return getFileEditorOrDefault(this.fileEditors, this.file.path);
},
@@ -159,16 +152,6 @@ export default {
},
},
watch: {
- 'file.name': {
- handler() {
- this.stopWatchingCiYaml();
-
- if (isDefaultCiConfig(this.file.name)) {
- this.startWatchingCiYaml();
- }
- },
- immediate: true,
- },
file(newVal, oldVal) {
if (oldVal.pending) {
this.removePendingTab(oldVal);
@@ -235,7 +218,6 @@ export default {
'removePendingTab',
'triggerFilesChange',
'addTempImage',
- 'detectGitlabCiFileAlerts',
]),
...mapActions('editor', ['updateFileEditor']),
initEditor() {
@@ -498,18 +480,6 @@ export default {
this.updateFileEditor({ path: this.file.path, data });
},
- startWatchingCiYaml() {
- this.unwatchCiYaml = this.$watch(
- 'file.content',
- debounce(this.detectGitlabCiFileAlerts, 500),
- );
- },
- stopWatchingCiYaml() {
- if (this.unwatchCiYaml) {
- this.unwatchCiYaml();
- this.unwatchCiYaml = null;
- }
- },
},
viewerTypes,
FILE_VIEW_MODE_EDITOR,
@@ -531,7 +501,6 @@ export default {
@click="updateEditor({ viewMode: $options.FILE_VIEW_MODE_PREVIEW })"
/>
</gl-tabs>
- <file-alert v-if="alertKey" :alert-key="alertKey" />
<file-templates-bar v-else-if="showFileTemplatesBar(file.name)" />
<div
v-show="showEditor"
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
index b09cd7f6643..51c7e69449b 100644
--- a/app/assets/javascripts/ide/index.js
+++ b/app/assets/javascripts/ide/index.js
@@ -70,7 +70,6 @@ export const initLegacyWebIDE = (el, options = {}) => {
this.init({
renderWhitespaceInCode: parseBoolean(el.dataset.renderWhitespaceInCode),
editorTheme: window.gon?.user_color_scheme || DEFAULT_THEME,
- environmentsGuidanceAlertDismissed: !parseBoolean(el.dataset.enableEnvironmentsGuidance),
previewMarkdownPath: el.dataset.previewMarkdownPath,
userPreferencesPath: el.dataset.userPreferencesPath,
});
diff --git a/app/assets/javascripts/ide/lib/alerts/environments.vue b/app/assets/javascripts/ide/lib/alerts/environments.vue
deleted file mode 100644
index bfe101bc7e7..00000000000
--- a/app/assets/javascripts/ide/lib/alerts/environments.vue
+++ /dev/null
@@ -1,33 +0,0 @@
-<!-- eslint-disable vue/multi-word-component-names -->
-<script>
-import { GlSprintf, GlLink } from '@gitlab/ui';
-import { helpPagePath } from '~/helpers/help_page_helper';
-import { __ } from '~/locale';
-
-export default {
- components: { GlSprintf, GlLink },
- message: __(
- "No deployments detected. Use environments to control your software's continuous deployment. %{linkStart}Learn more about deployment jobs.%{linkEnd}",
- ),
- computed: {
- helpLink() {
- return helpPagePath('ci/environments/index.md');
- },
- },
-};
-</script>
-<template>
- <span>
- <gl-sprintf :message="$options.message">
- <template #link="{ content }">
- <gl-link
- :href="helpLink"
- target="_blank"
- data-track-action="click_link"
- data-track-experiment="in_product_guidance_environments_webide"
- >{{ content }}</gl-link
- >
- </template>
- </gl-sprintf>
- </span>
-</template>
diff --git a/app/assets/javascripts/ide/lib/alerts/index.js b/app/assets/javascripts/ide/lib/alerts/index.js
deleted file mode 100644
index ac4eeb0386f..00000000000
--- a/app/assets/javascripts/ide/lib/alerts/index.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { isDefaultCiConfig } from '~/lib/utils/common_utils';
-import { leftSidebarViews } from '../../constants';
-import EnvironmentsMessage from './environments.vue';
-
-const alerts = [
- {
- key: Symbol('ALERT_ENVIRONMENT'),
- show: (state, file) =>
- state.currentActivityView === leftSidebarViews.commit.name &&
- isDefaultCiConfig(file.path) &&
- state.environmentsGuidanceAlertDetected &&
- !state.environmentsGuidanceAlertDismissed,
- props: { variant: 'tip' },
- dismiss: ({ dispatch }) => dispatch('dismissEnvironmentsGuidance'),
- message: EnvironmentsMessage,
- },
-];
-
-export const findAlertKeyToShow = (...args) => alerts.find((x) => x.show(...args))?.key;
-
-export const getAlert = (key) => alerts.find((x) => x.key === key);
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
index 1f9bc834140..01e1fd50bcc 100644
--- a/app/assets/javascripts/ide/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -1,11 +1,9 @@
import Api from '~/api';
import getIdeProject from 'ee_else_ce/ide/queries/get_ide_project.query.graphql';
-import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import axios from '~/lib/utils/axios_utils';
import { joinPaths, escapeFileUrl } from '~/lib/utils/url_utility';
-import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
-import { query, mutate } from './gql';
+import { query } from './gql';
export default {
getFileData(endpoint) {
@@ -84,18 +82,6 @@ export default {
const url = `${gon.relative_url_root}/${projectPath}/service_ping/web_ide_pipelines_count`;
return axios.post(url);
},
- getCiConfig(projectPath, content) {
- return query({
- query: ciConfig,
- variables: { projectPath, content },
- }).then(({ data }) => data.ciConfig);
- },
- dismissUserCallout(name) {
- return mutate({
- mutation: dismissUserCallout,
- variables: { input: { featureName: name } },
- }).then(({ data }) => data);
- },
getProjectPermissionsData(projectPath) {
return query({
query: getIdeProject,
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
index 0106eeae162..bb4b181c56d 100644
--- a/app/assets/javascripts/ide/stores/actions.js
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -311,4 +311,3 @@ export * from './actions/tree';
export * from './actions/file';
export * from './actions/project';
export * from './actions/merge_request';
-export * from './actions/alert';
diff --git a/app/assets/javascripts/ide/stores/actions/alert.js b/app/assets/javascripts/ide/stores/actions/alert.js
deleted file mode 100644
index 4c33dc19520..00000000000
--- a/app/assets/javascripts/ide/stores/actions/alert.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import service from '../../services';
-import {
- DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
- DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
-} from '../mutation_types';
-
-export const detectGitlabCiFileAlerts = ({ dispatch }, content) =>
- dispatch('detectEnvironmentsGuidance', content);
-
-export const detectEnvironmentsGuidance = ({ commit, state }, content) =>
- service.getCiConfig(state.currentProjectId, content).then((data) => {
- commit(DETECT_ENVIRONMENTS_GUIDANCE_ALERT, data?.stages);
- });
-
-export const dismissEnvironmentsGuidance = ({ commit }) =>
- service.dismissUserCallout('web_ide_ci_environments_guidance').then(() => {
- commit(DISMISS_ENVIRONMENTS_GUIDANCE_ALERT);
- });
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
index 74fe61b6e2f..0f4dbb56e04 100644
--- a/app/assets/javascripts/ide/stores/getters.js
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -260,5 +260,3 @@ export const getJsonSchemaForPath = (state, getters) => (path) => {
fileMatch: [`*${path}`],
};
};
-
-export * from './getters/alert';
diff --git a/app/assets/javascripts/ide/stores/getters/alert.js b/app/assets/javascripts/ide/stores/getters/alert.js
deleted file mode 100644
index 714e2d89b4f..00000000000
--- a/app/assets/javascripts/ide/stores/getters/alert.js
+++ /dev/null
@@ -1,3 +0,0 @@
-import { findAlertKeyToShow } from '../../lib/alerts';
-
-export const getAlert = (state) => (file) => findAlertKeyToShow(state, file);
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index 13f338c4a48..ae6588f948f 100644
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -71,8 +71,3 @@ export const RENAME_ENTRY = 'RENAME_ENTRY';
export const REVERT_RENAME_ENTRY = 'REVERT_RENAME_ENTRY';
export const RESTORE_TREE = 'RESTORE_TREE';
-
-// Alert mutation types
-
-export const DETECT_ENVIRONMENTS_GUIDANCE_ALERT = 'DETECT_ENVIRONMENTS_GUIDANCE_ALERT';
-export const DISMISS_ENVIRONMENTS_GUIDANCE_ALERT = 'DISMISS_ENVIRONMENTS_GUIDANCE_ALERT';
diff --git a/app/assets/javascripts/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index d11fc388d5e..300d352f81c 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,6 +1,5 @@
import Vue from 'vue';
import * as types from './mutation_types';
-import alertMutations from './mutations/alert';
import branchMutations from './mutations/branch';
import fileMutations from './mutations/file';
import mergeRequestMutation from './mutations/merge_request';
@@ -247,5 +246,4 @@ export default {
...fileMutations,
...treeMutations,
...branchMutations,
- ...alertMutations,
};
diff --git a/app/assets/javascripts/ide/stores/mutations/alert.js b/app/assets/javascripts/ide/stores/mutations/alert.js
deleted file mode 100644
index bb2d33a836b..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/alert.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import {
- DETECT_ENVIRONMENTS_GUIDANCE_ALERT,
- DISMISS_ENVIRONMENTS_GUIDANCE_ALERT,
-} from '../mutation_types';
-
-export default {
- [DETECT_ENVIRONMENTS_GUIDANCE_ALERT](state, stages) {
- if (!stages) {
- return;
- }
- const hasEnvironments = stages?.nodes?.some((stage) =>
- stage.groups.nodes.some((group) => group.jobs.nodes.some((job) => job.environment)),
- );
- const hasParsedCi = Array.isArray(stages.nodes);
-
- state.environmentsGuidanceAlertDetected = !hasEnvironments && hasParsedCi;
- },
- [DISMISS_ENVIRONMENTS_GUIDANCE_ALERT](state) {
- state.environmentsGuidanceAlertDismissed = true;
- },
-};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 356bbf28a48..6297231e252 100644
--- a/app/assets/javascripts/ide/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -28,8 +28,6 @@ export default () => ({
},
renderWhitespaceInCode: false,
editorTheme: DEFAULT_THEME,
- environmentsGuidanceAlertDismissed: false,
- environmentsGuidanceAlertDetected: false,
previewMarkdownPath: '',
userPreferencesPath: '',
});
diff --git a/app/assets/javascripts/import_entities/import_groups/components/import_status.vue b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
index cdb38cdf7f1..e2c5a4aadb8 100644
--- a/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
+++ b/app/assets/javascripts/import_entities/import_groups/components/import_status.vue
@@ -31,11 +31,6 @@ export default {
required: false,
default: false,
},
- showDetailsLink: {
- type: Boolean,
- required: false,
- default: false,
- },
status: {
type: String,
required: true,
@@ -56,7 +51,7 @@ export default {
},
showDetails() {
- return this.showDetailsLink && Boolean(this.detailsPathWithId) && this.hasFailures;
+ return Boolean(this.detailsPathWithId) && this.hasFailures;
},
detailsPathWithId() {
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 db354a01899..25d47f3ced3 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
@@ -18,7 +18,6 @@ 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';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import { getGroupPathAvailability } from '~/rest_api';
@@ -69,7 +68,6 @@ export default {
directives: {
GlTooltip: GlTooltipDirective,
},
- mixins: [glFeatureFlagsMixin()],
props: {
sourceUrl: {
type: String,
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 dead90eeb71..4f6ca5c5638 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_modal.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_modal.vue
@@ -7,11 +7,7 @@ import Api from '~/api';
import Tracking from '~/tracking';
import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
import { n__, s__, sprintf } from '~/locale';
-import {
- memberName,
- triggerExternalAlert,
- inviteMembersTrackingOptions,
-} from 'ee_else_ce/invite_members/utils/member_utils';
+import { memberName, triggerExternalAlert } from 'ee_else_ce/invite_members/utils/member_utils';
import { captureException } from '~/ci/runner/sentry_utils';
import {
USERS_FILTER_ALL,
@@ -142,9 +138,6 @@ export default {
isCelebration() {
return this.mode === 'celebrate';
},
- baseTrackingDetails() {
- return { label: this.source, celebrate: this.isCelebration };
- },
isTextForAdmin() {
return this.isCurrentUserAdmin && Boolean(this.newUsersUrl);
},
@@ -265,7 +258,7 @@ export default {
this.source = source;
this.$root.$emit(BV_SHOW_MODAL, this.modalId);
- this.track('render', inviteMembersTrackingOptions(this.baseTrackingDetails));
+ this.track('render', { label: this.source });
},
closeModal() {
this.$root.$emit(BV_HIDE_MODAL, this.modalId);
@@ -335,10 +328,10 @@ export default {
return this.newUsersToInvite.find((member) => memberName(member) === username)?.name;
},
onCancel() {
- this.track('click_cancel', inviteMembersTrackingOptions(this.baseTrackingDetails));
+ this.track('click_cancel', { label: this.source });
},
onClose() {
- this.track('click_x', inviteMembersTrackingOptions(this.baseTrackingDetails));
+ this.track('click_x', { label: this.source });
},
resetFields() {
this.clearValidation();
@@ -347,7 +340,7 @@ export default {
this.newUsersToInvite = [];
},
onInviteSuccess() {
- this.track('invite_successful', inviteMembersTrackingOptions(this.baseTrackingDetails));
+ this.track('invite_successful', { label: this.source });
if (this.reloadPageOnSubmit) {
reloadOnInvitationSuccess();
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 7f76b7ca1ac..5d9663abaf2 100644
--- a/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
+++ b/app/assets/javascripts/invite_members/components/invite_members_trigger.vue
@@ -94,7 +94,6 @@ export default {
<gl-dropdown-item
v-else-if="isDropdownWithEmojiTrigger"
v-bind="componentAttributes"
- button-class="top-nav-menu-item"
@click="openModal"
>
{{ displayText }}
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 00b7c3f4bdd..574bbacc498 100644
--- a/app/assets/javascripts/invite_members/components/invite_modal_base.vue
+++ b/app/assets/javascripts/invite_members/components/invite_modal_base.vue
@@ -215,6 +215,7 @@ export default {
this.$emit('reset');
},
onShowModal() {
+ this.$emit('shown');
if (this.usersLimitDataset.reachedLimit) {
this.track('render', { category: 'default', label: ON_SHOW_TRACK_LABEL });
}
diff --git a/app/assets/javascripts/invite_members/utils/member_utils.js b/app/assets/javascripts/invite_members/utils/member_utils.js
index 52fb5e98f27..7998cb69445 100644
--- a/app/assets/javascripts/invite_members/utils/member_utils.js
+++ b/app/assets/javascripts/invite_members/utils/member_utils.js
@@ -6,7 +6,3 @@ export function memberName(member) {
export function triggerExternalAlert() {
return false;
}
-
-export function inviteMembersTrackingOptions(options) {
- return { label: options.label };
-}
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 adc789a205b..8ac7990c28d 100644
--- a/app/assets/javascripts/issues/list/components/issues_list_app.vue
+++ b/app/assets/javascripts/issues/list/components/issues_list_app.vue
@@ -915,7 +915,7 @@ export default {
v-if="issuesDrawerEnabled"
:open="isIssuableSelected"
header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
- class="gl-w-full gl-sm-w-40p"
+ class="gl-w-full gl-sm-w-40p gl-reset-line-height"
@close="activeIssuable = null"
>
<template #title>
@@ -927,6 +927,7 @@ export default {
<work-item-detail
:key="activeIssuable.iid"
:work-item-iid="activeIssuable.iid"
+ class="gl-pt-0!"
@work-item-updated="updateIssuablesCache"
@work-item-emoji-updated="updateIssuableEmojis"
@addChild="refetchIssuables"
diff --git a/app/assets/javascripts/issues/list/index.js b/app/assets/javascripts/issues/list/index.js
index 5a836e3e40a..ab6ff825554 100644
--- a/app/assets/javascripts/issues/list/index.js
+++ b/app/assets/javascripts/issues/list/index.js
@@ -137,6 +137,7 @@ export async function mountIssuesListApp() {
hasOkrsFeature: parseBoolean(hasOkrsFeature),
initialSort,
isIssueRepositioningDisabled: parseBoolean(isIssueRepositioningDisabled),
+ isGroup: !parseBoolean(isProject),
isProject: parseBoolean(isProject),
isPublicVisibilityRestricted: parseBoolean(isPublicVisibilityRestricted),
isSignedIn: parseBoolean(isSignedIn),
diff --git a/app/assets/javascripts/issues/show/components/header_actions.vue b/app/assets/javascripts/issues/show/components/header_actions.vue
index dcdfd06fbf1..fb7058d95dc 100644
--- a/app/assets/javascripts/issues/show/components/header_actions.vue
+++ b/app/assets/javascripts/issues/show/components/header_actions.vue
@@ -14,6 +14,7 @@ import { mapActions, mapGetters, mapState } from 'vuex';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
+import { ISSUABLE_EDIT_DESCRIPTION } from '~/behaviors/shortcuts/keybindings';
import { STATUS_CLOSED, TYPE_ISSUE, issuableTypeText } from '~/issues/constants';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
@@ -196,6 +197,12 @@ export default {
href: this.submitAsSpamPath,
};
},
+ editShortcutKey() {
+ return ISSUABLE_EDIT_DESCRIPTION.defaultKeys[0];
+ },
+ editTooltip() {
+ return `${this.$options.i18n.editTitleAndDescription} <kbd class="glat gl-ml-1" aria-hidden=true>${this.editShortcutKey}</kbd>`;
+ },
},
created() {
eventHub.$on('toggle.issuable.state', this.toggleIssueState);
@@ -395,9 +402,10 @@ export default {
<gl-button
v-if="canUpdateIssue"
- v-gl-tooltip.bottom
- :title="$options.i18n.editTitleAndDescription"
+ v-gl-tooltip.viewport.html
+ :title="editTooltip"
:aria-label="$options.i18n.editTitleAndDescription"
+ :aria-keyshortcuts="editShortcutKey"
class="js-issuable-edit gl-display-none! gl-md-display-block!"
data-testid="edit-button"
@click="edit"
diff --git a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
index 52a12cc7771..e3fa0ce8073 100644
--- a/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
+++ b/app/assets/javascripts/jira_connect/branches/components/project_dropdown.vue
@@ -1,20 +1,20 @@
<script>
import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { debounce } from 'lodash';
+import produce from 'immer';
import { __ } from '~/locale';
import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
import { PROJECTS_PER_PAGE } from '../constants';
import getProjectsQuery from '../graphql/queries/get_projects.query.graphql';
export default {
- PROJECTS_PER_PAGE,
- projectQueryPageInfo: {
- endCursor: '',
- },
+ name: 'ProjectDropdown',
+
components: {
GlAvatarLabeled,
GlCollapsibleListbox,
},
+
props: {
selectedProject: {
type: Object,
@@ -22,27 +22,29 @@ export default {
default: null,
},
},
+
data() {
return {
initialProjectsLoading: true,
+ isLoadingMore: false,
projectSearchQuery: '',
selectedProjectId: this.selectedProject?.id,
};
},
+
apollo: {
projects: {
query: getProjectsQuery,
variables() {
return {
- search: this.projectSearchQuery,
- first: this.$options.PROJECTS_PER_PAGE,
- after: this.$options.projectQueryPageInfo.endCursor,
- searchNamespaces: true,
- sort: 'similarity',
+ ...this.queryVariables,
};
},
update(data) {
- return data?.projects?.nodes.filter((project) => !project.repository?.empty) ?? [];
+ return {
+ nodes: data?.projects?.nodes.filter((project) => !project.repository?.empty) ?? [],
+ pageInfo: data?.projects?.pageInfo,
+ };
},
result() {
this.initialProjectsLoading = false;
@@ -52,24 +54,37 @@ export default {
},
},
},
+
computed: {
- projectsLoading() {
- return Boolean(this.$apollo.queries.projects.loading);
+ queryVariables() {
+ return {
+ search: this.projectSearchQuery,
+ first: PROJECTS_PER_PAGE,
+ searchNamespaces: true,
+ sort: 'similarity',
+ };
+ },
+ isLoading() {
+ return this.$apollo.queries.projects.loading && !this.isLoadingMore;
},
projectDropdownText() {
return this.selectedProject?.nameWithNamespace || this.$options.i18n.selectProjectText;
},
projectList() {
- return (this.projects || []).map((project) => ({
+ return (this.projects?.nodes || []).map((project) => ({
...project,
text: project.nameWithNamespace,
value: String(project.id),
}));
},
+ hasNextPage() {
+ return this.projects?.pageInfo?.hasNextPage;
+ },
},
+
methods: {
findProjectById(id) {
- return this.projects.find((project) => id === project.id);
+ return this.projects?.nodes?.find((project) => id === project.id);
},
onProjectSelect(projectId) {
this.$emit('change', this.findProjectById(projectId));
@@ -77,13 +92,41 @@ export default {
onError({ message } = {}) {
this.$emit('error', { message });
},
+ async onBottomReached() {
+ if (!this.hasNextPage) return;
+
+ this.isLoadingMore = true;
+
+ try {
+ await this.$apollo.queries.projects.fetchMore({
+ variables: {
+ ...this.queryVariables,
+ after: this.projects.pageInfo?.endCursor,
+ },
+ updateQuery: (previousResult, { fetchMoreResult }) => {
+ return produce(fetchMoreResult, (draftData) => {
+ draftData.projects.nodes = [
+ ...previousResult.projects.nodes,
+ ...draftData.projects.nodes,
+ ];
+ });
+ },
+ });
+ } catch (error) {
+ this.onError({ message: __('Failed to load projects') });
+ } finally {
+ this.isLoadingMore = false;
+ }
+ },
onSearch: debounce(function debouncedSearch(query) {
this.projectSearchQuery = query;
}, 250),
},
+
i18n: {
selectProjectText: __('Select a project'),
},
+
AVATAR_SHAPE_OPTION_RECT,
};
</script>
@@ -97,8 +140,11 @@ export default {
:header-text="$options.i18n.selectProjectText"
:loading="initialProjectsLoading"
:searchable="true"
- :searching="projectsLoading"
+ :searching="isLoading"
fluid-width
+ infinite-scroll
+ :infinite-scroll-loading="isLoadingMore"
+ @bottom-reached="onBottomReached"
@search="onSearch"
@select="onProjectSelect"
>
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
index 0d219f915c9..bcc0ddf824a 100644
--- a/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_details.vue
@@ -14,8 +14,7 @@ export default {
item: {
type: Object,
required: true,
- validator: (item) =>
- ['name', 'kind', 'labels', 'annotations', 'status'].every((key) => item[key]),
+ validator: (item) => ['name', 'kind', 'labels', 'annotations'].every((key) => item[key]),
},
},
computed: {
@@ -51,7 +50,7 @@ export default {
<template>
<ul class="gl-list-style-none">
<workload-details-item :label="$options.i18n.name">
- {{ item.name }}
+ <span class="gl-word-break-word"> {{ item.name }}</span>
</workload-details-item>
<workload-details-item :label="$options.i18n.kind">
{{ item.kind }}
@@ -63,7 +62,7 @@ export default {
</gl-badge>
</div>
</workload-details-item>
- <workload-details-item :label="$options.i18n.status">
+ <workload-details-item v-if="item.status" :label="$options.i18n.status">
<gl-badge :variant="$options.WORKLOAD_STATUS_BADGE_VARIANTS[item.status]">{{
item.status
}}</gl-badge></workload-details-item
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
index 8c6a08ad504..6579e0229e6 100644
--- a/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_layout.vue
@@ -1,6 +1,7 @@
<script>
import { GlLoadingIcon, GlAlert, GlDrawer } from '@gitlab/ui';
import { DRAWER_Z_INDEX } from '~/lib/utils/constants';
+import { getContentWrapperHeight } from '~/lib/utils/dom_utils';
import WorkloadStats from './workload_stats.vue';
import WorkloadTable from './workload_table.vue';
import WorkloadDetails from './workload_details.vue';
@@ -33,6 +34,11 @@ export default {
type: Array,
required: true,
},
+ fields: {
+ type: Array,
+ required: false,
+ default: undefined,
+ },
},
data() {
return {
@@ -40,6 +46,11 @@ export default {
selectedItem: {},
};
},
+ computed: {
+ getDrawerHeaderHeight() {
+ return getContentWrapperHeight();
+ },
+ },
methods: {
closeDetailsDrawer() {
this.showDetailsDrawer = false;
@@ -59,16 +70,18 @@ export default {
</gl-alert>
<div v-else>
<workload-stats :stats="stats" />
- <workload-table :items="items" @select-item="onItemSelect" />
+ <workload-table :items="items" :fields="fields" @select-item="onItemSelect" />
<gl-drawer
:open="showDetailsDrawer"
- header-height="calc(var(--top-bar-height) + var(--performance-bar-height))"
+ :header-height="getDrawerHeaderHeight"
: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>
+ <h4 class="gl-font-weight-bold gl-font-size-h2 gl-m-0 gl-word-break-word">
+ {{ selectedItem.name }}
+ </h4>
</template>
<template #default>
<workload-details :item="selectedItem" />
diff --git a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue
index d3704863538..83940fb91c8 100644
--- a/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue
+++ b/app/assets/javascripts/kubernetes_dashboard/components/workload_table.vue
@@ -1,9 +1,9 @@
<script>
import { GlTable, GlBadge, GlPagination } from '@gitlab/ui';
+import { __ } from '~/locale';
import {
WORKLOAD_STATUS_BADGE_VARIANTS,
PAGE_SIZE,
- TABLE_HEADING_CLASSES,
DEFAULT_WORKLOAD_TABLE_FIELDS,
} from '../constants';
@@ -34,7 +34,6 @@ export default {
return this.fields.map((field) => {
return {
...field,
- thClass: TABLE_HEADING_CLASSES,
sortable: true,
};
});
@@ -45,6 +44,9 @@ export default {
this.$emit('select-item', item);
},
},
+ i18n: {
+ emptyText: __('No results found'),
+ },
PAGE_SIZE,
WORKLOAD_STATUS_BADGE_VARIANTS,
TABLE_CELL_CLASSES: 'gl-p-2',
@@ -58,9 +60,10 @@ export default {
:fields="tableFields"
:per-page="$options.PAGE_SIZE"
:current-page="currentPage"
+ :empty-text="$options.i18n.emptyText"
tbody-tr-class="gl-hover-cursor-pointer"
+ show-empty
stacked="md"
- bordered
hover
@row-clicked="selectItem"
>
diff --git a/app/assets/javascripts/kubernetes_dashboard/constants.js b/app/assets/javascripts/kubernetes_dashboard/constants.js
index b93740aec90..458a79cbcb6 100644
--- a/app/assets/javascripts/kubernetes_dashboard/constants.js
+++ b/app/assets/javascripts/kubernetes_dashboard/constants.js
@@ -1,10 +1,12 @@
-import { s__ } from '~/locale';
+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_COMPLETED = 'Completed';
+export const STATUS_SUSPENDED = 'Suspended';
export const STATUS_LABELS = {
[STATUS_RUNNING]: s__('KubernetesDashboard|Running'),
@@ -12,6 +14,8 @@ export const STATUS_LABELS = {
[STATUS_SUCCEEDED]: s__('KubernetesDashboard|Succeeded'),
[STATUS_FAILED]: s__('KubernetesDashboard|Failed'),
[STATUS_READY]: s__('KubernetesDashboard|Ready'),
+ [STATUS_COMPLETED]: s__('KubernetesDashboard|Completed'),
+ [STATUS_SUSPENDED]: s__('KubernetesDashboard|Suspended'),
};
export const WORKLOAD_STATUS_BADGE_VARIANTS = {
@@ -20,24 +24,27 @@ export const WORKLOAD_STATUS_BADGE_VARIANTS = {
[STATUS_SUCCEEDED]: 'success',
[STATUS_FAILED]: 'danger',
[STATUS_READY]: 'success',
+ [STATUS_COMPLETED]: 'success',
+ [STATUS_SUSPENDED]: 'neutral',
};
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'),
+ tdClass: 'gl-md-w-half gl-lg-w-40p gl-word-break-word',
},
{
key: 'status',
label: s__('KubernetesDashboard|Status'),
+ tdClass: 'gl-md-w-15',
},
{
key: 'namespace',
label: s__('KubernetesDashboard|Namespace'),
+ tdClass: 'gl-md-w-30p gl-lg-w-40p gl-word-break-word',
},
{
key: 'age',
@@ -47,3 +54,34 @@ export const DEFAULT_WORKLOAD_TABLE_FIELDS = [
export const STATUS_TRUE = 'True';
export const STATUS_FALSE = 'False';
+
+export const SERVICES_TABLE_FIELDS = [
+ {
+ key: 'name',
+ label: __('Name'),
+ },
+ {
+ key: 'namespace',
+ label: __('Namespace'),
+ },
+ {
+ key: 'type',
+ label: __('Type'),
+ },
+ {
+ key: 'clusterIP',
+ label: s__('Environment|Cluster IP'),
+ },
+ {
+ key: 'externalIP',
+ label: s__('Environment|External IP'),
+ },
+ {
+ key: 'ports',
+ label: s__('Environment|Ports'),
+ },
+ {
+ key: 'age',
+ label: s__('Environment|Age'),
+ },
+];
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
index 5894472d83b..9454465df9d 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/client.js
@@ -6,6 +6,9 @@ import k8sDeploymentsQuery from './queries/k8s_dashboard_deployments.query.graph
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 k8sJobsQuery from './queries/k8s_dashboard_jobs.query.graphql';
+import k8sCronJobsQuery from './queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sServicesQuery from './queries/k8s_dashboard_services.query.graphql';
import { resolvers } from './resolvers';
export const apolloProvider = () => {
@@ -14,16 +17,18 @@ export const apolloProvider = () => {
});
const { cache } = defaultClient;
+ const metadata = {
+ name: null,
+ namespace: null,
+ creationTimestamp: null,
+ labels: null,
+ annotations: null,
+ };
+
cache.writeQuery({
query: k8sPodsQuery,
data: {
- metadata: {
- name: null,
- namespace: null,
- creationTimestamp: null,
- labels: null,
- annotations: null,
- },
+ metadata,
status: {
phase: null,
},
@@ -33,13 +38,7 @@ export const apolloProvider = () => {
cache.writeQuery({
query: k8sDeploymentsQuery,
data: {
- metadata: {
- name: null,
- namespace: null,
- creationTimestamp: null,
- labels: null,
- annotations: null,
- },
+ metadata,
status: {
conditions: null,
},
@@ -49,13 +48,7 @@ export const apolloProvider = () => {
cache.writeQuery({
query: k8sStatefulSetsQuery,
data: {
- metadata: {
- name: null,
- namespace: null,
- creationTimestamp: null,
- labels: null,
- annotations: null,
- },
+ metadata,
status: {
readyReplicas: null,
},
@@ -68,13 +61,7 @@ export const apolloProvider = () => {
cache.writeQuery({
query: k8sReplicaSetsQuery,
data: {
- metadata: {
- name: null,
- namespace: null,
- creationTimestamp: null,
- labels: null,
- annotations: null,
- },
+ metadata,
status: {
readyReplicas: null,
},
@@ -87,13 +74,7 @@ export const apolloProvider = () => {
cache.writeQuery({
query: k8sDaemonSetsQuery,
data: {
- metadata: {
- name: null,
- namespace: null,
- creationTimestamp: null,
- labels: null,
- annotations: null,
- },
+ metadata,
status: {
numberMisscheduled: null,
numberReady: null,
@@ -102,6 +83,47 @@ export const apolloProvider = () => {
},
});
+ cache.writeQuery({
+ query: k8sJobsQuery,
+ data: {
+ metadata,
+ status: {
+ failed: null,
+ succeeded: null,
+ },
+ spec: {
+ completions: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sCronJobsQuery,
+ data: {
+ metadata,
+ status: {
+ active: null,
+ lastScheduleTime: null,
+ },
+ spec: {
+ suspend: null,
+ },
+ },
+ });
+
+ cache.writeQuery({
+ query: k8sServicesQuery,
+ data: {
+ metadata,
+ spec: {
+ type: null,
+ clusterIP: null,
+ externalIP: null,
+ ports: 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
index 47c2f543357..b9c195d83d0 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/helpers/resolver_helpers.js
@@ -43,6 +43,62 @@ export const mapSetItem = (item) => {
return { status, metadata, spec };
};
+export const mapJobItem = (item) => {
+ const metadata = {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ };
+
+ const status = {
+ failed: item.status?.failed || 0,
+ succeeded: item.status?.succeeded || 0,
+ };
+
+ return {
+ status,
+ metadata,
+ spec: item.spec,
+ };
+};
+
+export const mapServicesItems = (item) => {
+ const { type, clusterIP, externalIP, ports } = item.spec;
+
+ return {
+ metadata: {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ },
+ spec: {
+ type,
+ clusterIP: clusterIP || '-',
+ externalIP: externalIP || '-',
+ ports,
+ },
+ };
+};
+
+export const mapCronJobItem = (item) => {
+ const metadata = {
+ ...item.metadata,
+ annotations: item.metadata?.annotations || {},
+ labels: item.metadata?.labels || {},
+ };
+
+ const status = {
+ active: item.status?.active || 0,
+ lastScheduleTime: item.status?.lastScheduleTime || null,
+ };
+
+ return {
+ status,
+ metadata,
+ spec: item.spec,
+ };
+};
+
export const watchWorkloadItems = ({
client,
query,
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql
new file mode 100644
index 00000000000..fe20cd2e70e
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_cron_jobs.query.graphql
@@ -0,0 +1,18 @@
+query getK8sDashboardCronJobs($configuration: LocalConfiguration) {
+ k8sCronJobs(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ active
+ lastScheduleTime
+ }
+ spec {
+ suspend
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql
new file mode 100644
index 00000000000..86afb47f2f9
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_jobs.query.graphql
@@ -0,0 +1,18 @@
+query getK8sDashboardJobs($configuration: LocalConfiguration) {
+ k8sJobs(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ status {
+ failed
+ succeeded
+ }
+ spec {
+ completions
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql
new file mode 100644
index 00000000000..7d42d66183e
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/queries/k8s_dashboard_services.query.graphql
@@ -0,0 +1,17 @@
+query getK8sDashboardServices($configuration: LocalConfiguration) {
+ k8sServices(configuration: $configuration) @client {
+ metadata {
+ name
+ namespace
+ creationTimestamp
+ labels
+ annotations
+ }
+ spec {
+ type
+ clusterIP
+ externalIP
+ ports
+ }
+ }
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
index e59bed5581b..75285ad2cca 100644
--- a/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
+++ b/app/assets/javascripts/kubernetes_dashboard/graphql/resolvers/kubernetes.js
@@ -1,4 +1,4 @@
-import { Configuration, AppsV1Api } from '@gitlab/cluster-client';
+import { Configuration, CoreV1Api, AppsV1Api, BatchV1Api } from '@gitlab/cluster-client';
import {
getK8sPods,
@@ -7,12 +7,18 @@ import {
mapSetItem,
buildWatchPath,
watchWorkloadItems,
+ mapJobItem,
+ mapCronJobItem,
+ mapServicesItems,
} 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';
+import k8sJobsQuery from '../queries/k8s_dashboard_jobs.query.graphql';
+import k8sCronJobsQuery from '../queries/k8s_dashboard_cron_jobs.query.graphql';
+import k8sServicesQuery from '../queries/k8s_dashboard_services.query.graphql';
export default {
k8sPods(_, { configuration }, { client }) {
@@ -61,10 +67,10 @@ export default {
const config = new Configuration(configuration);
const appsV1api = new AppsV1Api(config);
- const deploymentsApi = namespace
+ const statefulSetsApi = namespace
? appsV1api.listAppsV1NamespacedStatefulSet({ namespace })
: appsV1api.listAppsV1StatefulSetForAllNamespaces();
- return deploymentsApi
+ return statefulSetsApi
.then((res) => {
const watchPath = buildWatchPath({
resource: 'statefulsets',
@@ -98,10 +104,10 @@ export default {
const config = new Configuration(configuration);
const appsV1api = new AppsV1Api(config);
- const deploymentsApi = namespace
+ const replicaSetsApi = namespace
? appsV1api.listAppsV1NamespacedReplicaSet({ namespace })
: appsV1api.listAppsV1ReplicaSetForAllNamespaces();
- return deploymentsApi
+ return replicaSetsApi
.then((res) => {
const watchPath = buildWatchPath({
resource: 'replicasets',
@@ -135,10 +141,10 @@ export default {
const config = new Configuration(configuration);
const appsV1api = new AppsV1Api(config);
- const deploymentsApi = namespace
+ const daemonSetsApi = namespace
? appsV1api.listAppsV1NamespacedDaemonSet({ namespace })
: appsV1api.listAppsV1DaemonSetForAllNamespaces();
- return deploymentsApi
+ return daemonSetsApi
.then((res) => {
const watchPath = buildWatchPath({
resource: 'daemonsets',
@@ -166,4 +172,114 @@ export default {
}
});
},
+
+ k8sJobs(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const batchV1api = new BatchV1Api(config);
+ const jobsApi = namespace
+ ? batchV1api.listBatchV1NamespacedJob({ namespace })
+ : batchV1api.listBatchV1JobForAllNamespaces();
+ return jobsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'jobs',
+ api: 'apis/batch/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sJobsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sJobs',
+ mapFn: mapJobItem,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapJobItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+
+ k8sCronJobs(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const batchV1api = new BatchV1Api(config);
+ const cronJobsApi = namespace
+ ? batchV1api.listBatchV1NamespacedCronJob({ namespace })
+ : batchV1api.listBatchV1CronJobForAllNamespaces();
+ return cronJobsApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'cronjobs',
+ api: 'apis/batch/v1',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sCronJobsQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sCronJobs',
+ mapFn: mapCronJobItem,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapCronJobItem);
+ })
+ .catch(async (err) => {
+ try {
+ await handleClusterError(err);
+ } catch (error) {
+ throw new Error(error.message);
+ }
+ });
+ },
+
+ k8sServices(_, { configuration, namespace = '' }, { client }) {
+ const config = new Configuration(configuration);
+
+ const coreV1Api = new CoreV1Api(config);
+ const servicesApi = namespace
+ ? coreV1Api.listCoreV1NamespacedService({ namespace })
+ : coreV1Api.listCoreV1ServiceForAllNamespaces();
+ return servicesApi
+ .then((res) => {
+ const watchPath = buildWatchPath({
+ resource: 'services',
+ namespace,
+ });
+ watchWorkloadItems({
+ client,
+ query: k8sServicesQuery,
+ configuration,
+ namespace,
+ watchPath,
+ queryField: 'k8sServices',
+ mapFn: mapServicesItems,
+ });
+
+ const data = res?.items || [];
+
+ return data.map(mapServicesItems);
+ })
+ .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
index 24f43e21506..d3116fd611a 100644
--- a/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
+++ b/app/assets/javascripts/kubernetes_dashboard/helpers/k8s_integration_helper.js
@@ -5,6 +5,8 @@ import {
STATUS_PENDING,
STATUS_READY,
STATUS_FAILED,
+ STATUS_COMPLETED,
+ STATUS_SUSPENDED,
} from '../constants';
export function getAge(creationTimestamp) {
@@ -58,3 +60,31 @@ export function calculateDaemonSetStatus(item) {
}
return STATUS_FAILED;
}
+
+export function calculateJobStatus(item) {
+ if (item.status.failed > 0 || item.status?.succeeded !== item.spec?.completions) {
+ return STATUS_FAILED;
+ }
+ return STATUS_COMPLETED;
+}
+
+export function calculateCronJobStatus(item) {
+ if (item.status?.active > 0 && !item.status?.lastScheduleTime) {
+ return STATUS_FAILED;
+ }
+ if (item.spec?.suspend) {
+ return STATUS_SUSPENDED;
+ }
+ return STATUS_READY;
+}
+
+export function generateServicePortsString(ports) {
+ if (!ports?.length) return '';
+
+ return ports
+ .map((port) => {
+ const nodePort = port.nodePort ? `:${port.nodePort}` : '';
+ return `${port.port}${nodePort}/${port.protocol}`;
+ })
+ .join(', ');
+}
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue
new file mode 100644
index 00000000000..2d57bfdc9fc
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/cron_jobs_page.vue
@@ -0,0 +1,84 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateCronJobStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sCronJobsQuery from '../graphql/queries/k8s_dashboard_cron_jobs.query.graphql';
+import { STATUS_FAILED, STATUS_READY, STATUS_SUSPENDED, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sCronJobs: {
+ query: k8sCronJobsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sCronJobs?.map((job) => {
+ return {
+ name: job.metadata?.name,
+ namespace: job.metadata?.namespace,
+ status: calculateCronJobStatus(job),
+ age: getAge(job.metadata?.creationTimestamp),
+ labels: job.metadata?.labels,
+ annotations: job.metadata?.annotations,
+ kind: s__('KubernetesDashboard|CronJob'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sCronJobs: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ cronJobsStats() {
+ return [
+ {
+ value: this.countJobsByStatus(STATUS_READY),
+ title: STATUS_LABELS[STATUS_READY],
+ },
+ {
+ value: this.countJobsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ {
+ value: this.countJobsByStatus(STATUS_SUSPENDED),
+ title: STATUS_LABELS[STATUS_SUSPENDED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sCronJobs.loading;
+ },
+ },
+ methods: {
+ countJobsByStatus(phase) {
+ const filteredJobs = this.k8sCronJobs.filter((item) => item.status === phase) || [];
+
+ return filteredJobs.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="cronJobsStats"
+ :items="k8sCronJobs"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue
new file mode 100644
index 00000000000..f9dbb53e8b4
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/jobs_page.vue
@@ -0,0 +1,80 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, calculateJobStatus } from '../helpers/k8s_integration_helper';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sJobsQuery from '../graphql/queries/k8s_dashboard_jobs.query.graphql';
+import { STATUS_FAILED, STATUS_COMPLETED, STATUS_LABELS } from '../constants';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sJobs: {
+ query: k8sJobsQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sJobs?.map((job) => {
+ return {
+ name: job.metadata?.name,
+ namespace: job.metadata?.namespace,
+ status: calculateJobStatus(job),
+ age: getAge(job.metadata?.creationTimestamp),
+ labels: job.metadata?.labels,
+ annotations: job.metadata?.annotations,
+ kind: s__('KubernetesDashboard|Job'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sJobs: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ jobsStats() {
+ return [
+ {
+ value: this.countJobsByStatus(STATUS_COMPLETED),
+ title: STATUS_LABELS[STATUS_COMPLETED],
+ },
+ {
+ value: this.countJobsByStatus(STATUS_FAILED),
+ title: STATUS_LABELS[STATUS_FAILED],
+ },
+ ];
+ },
+ loading() {
+ return this.$apollo.queries.k8sJobs.loading;
+ },
+ },
+ methods: {
+ countJobsByStatus(phase) {
+ const filteredJobs = this.k8sJobs.filter((item) => item.status === phase) || [];
+
+ return filteredJobs.length;
+ },
+ },
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="jobsStats"
+ :items="k8sJobs"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue
new file mode 100644
index 00000000000..4dc8fb6b6c0
--- /dev/null
+++ b/app/assets/javascripts/kubernetes_dashboard/pages/services_page.vue
@@ -0,0 +1,69 @@
+<script>
+import { s__ } from '~/locale';
+import { getAge, generateServicePortsString } from '../helpers/k8s_integration_helper';
+import { SERVICES_TABLE_FIELDS } from '../constants';
+import WorkloadLayout from '../components/workload_layout.vue';
+import k8sServicesQuery from '../graphql/queries/k8s_dashboard_services.query.graphql';
+
+export default {
+ components: {
+ WorkloadLayout,
+ },
+ inject: ['configuration'],
+ apollo: {
+ k8sServices: {
+ query: k8sServicesQuery,
+ variables() {
+ return {
+ configuration: this.configuration,
+ };
+ },
+ update(data) {
+ return (
+ data?.k8sServices?.map((service) => {
+ return {
+ name: service.metadata?.name,
+ namespace: service.metadata?.namespace,
+ type: service.spec?.type,
+ clusterIP: service.spec?.clusterIP,
+ externalIP: service.spec?.externalIP,
+ ports: generateServicePortsString(service?.spec?.ports),
+ age: getAge(service.metadata?.creationTimestamp),
+ labels: service.metadata?.labels,
+ annotations: service.metadata?.annotations,
+ kind: s__('KubernetesDashboard|Service'),
+ };
+ }) || []
+ );
+ },
+ error(err) {
+ this.errorMessage = err?.message;
+ },
+ },
+ },
+ data() {
+ return {
+ k8sServices: [],
+ errorMessage: '',
+ };
+ },
+ computed: {
+ loading() {
+ return this.$apollo.queries.k8sServices.loading;
+ },
+ servicesStats() {
+ return [];
+ },
+ },
+ SERVICES_TABLE_FIELDS,
+};
+</script>
+<template>
+ <workload-layout
+ :loading="loading"
+ :error-message="errorMessage"
+ :stats="servicesStats"
+ :items="k8sServices"
+ :fields="$options.SERVICES_TABLE_FIELDS"
+ />
+</template>
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/constants.js b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
index 700f501ade4..f02c01d7973 100644
--- a/app/assets/javascripts/kubernetes_dashboard/router/constants.js
+++ b/app/assets/javascripts/kubernetes_dashboard/router/constants.js
@@ -3,9 +3,15 @@ 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 JOBS_ROUTE_NAME = 'jobs';
+export const CRON_JOBS_ROUTE_NAME = 'cronJobs';
+export const SERVICES_ROUTE_NAME = 'services';
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';
+export const JOBS_ROUTE_PATH = '/jobs';
+export const CRON_JOBS_ROUTE_PATH = '/cronjobs';
+export const SERVICES_ROUTE_PATH = '/services';
diff --git a/app/assets/javascripts/kubernetes_dashboard/router/routes.js b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
index a1684a62ca4..7448508de8a 100644
--- a/app/assets/javascripts/kubernetes_dashboard/router/routes.js
+++ b/app/assets/javascripts/kubernetes_dashboard/router/routes.js
@@ -4,6 +4,10 @@ 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 JobsPage from '../pages/jobs_page.vue';
+import CronJobsPage from '../pages/cron_jobs_page.vue';
+import ServicesPage from '../pages/services_page.vue';
+
import {
PODS_ROUTE_NAME,
PODS_ROUTE_PATH,
@@ -15,6 +19,12 @@ import {
REPLICA_SETS_ROUTE_PATH,
DAEMON_SETS_ROUTE_NAME,
DAEMON_SETS_ROUTE_PATH,
+ JOBS_ROUTE_NAME,
+ JOBS_ROUTE_PATH,
+ CRON_JOBS_ROUTE_NAME,
+ CRON_JOBS_ROUTE_PATH,
+ SERVICES_ROUTE_NAME,
+ SERVICES_ROUTE_PATH,
} from './constants';
export default [
@@ -58,4 +68,28 @@ export default [
title: s__('KubernetesDashboard|DaemonSets'),
},
},
+ {
+ name: JOBS_ROUTE_NAME,
+ path: JOBS_ROUTE_PATH,
+ component: JobsPage,
+ meta: {
+ title: s__('KubernetesDashboard|Jobs'),
+ },
+ },
+ {
+ name: CRON_JOBS_ROUTE_NAME,
+ path: CRON_JOBS_ROUTE_PATH,
+ component: CronJobsPage,
+ meta: {
+ title: s__('KubernetesDashboard|CronJobs'),
+ },
+ },
+ {
+ name: SERVICES_ROUTE_NAME,
+ path: SERVICES_ROUTE_PATH,
+ component: ServicesPage,
+ meta: {
+ title: s__('KubernetesDashboard|Services'),
+ },
+ },
];
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 674a901aebc..7dc776a1446 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -634,7 +634,7 @@ export const roundDownFloat = (number, precision = 0) => {
* Represents navigation type constants of the Performance Navigation API.
* Detailed explanation see https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigation.
*/
-export const NavigationType = {
+export const navigationType = {
TYPE_NAVIGATE: 0,
TYPE_RELOAD: 1,
TYPE_BACK_FORWARD: 2,
diff --git a/app/assets/javascripts/lib/utils/constants.js b/app/assets/javascripts/lib/utils/constants.js
index d9ac0abf7b3..77986539403 100644
--- a/app/assets/javascripts/lib/utils/constants.js
+++ b/app/assets/javascripts/lib/utils/constants.js
@@ -19,10 +19,5 @@ export const DRAWER_Z_INDEX = 252;
export const MIN_USERNAME_LENGTH = 2;
-export const BYTES_FORMAT_BYTES = 'B';
-export const BYTES_FORMAT_KIB = 'KiB';
-export const BYTES_FORMAT_MIB = 'MiB';
-export const BYTES_FORMAT_GIB = 'GiB';
-
export const DEFAULT_CI_CONFIG_PATH = '.gitlab-ci.yml';
export const CI_CONFIG_PATH_EXTENSION = /(\.gitlab-ci\.yml)/;
diff --git a/app/assets/javascripts/lib/utils/headers.js b/app/assets/javascripts/lib/utils/headers.js
index 80ae3fb146f..fb635a124d6 100644
--- a/app/assets/javascripts/lib/utils/headers.js
+++ b/app/assets/javascripts/lib/utils/headers.js
@@ -1,3 +1,3 @@
-export const ContentTypeMultipartFormData = {
+export const contentTypeMultipartFormData = {
'Content-Type': 'multipart/form-data',
};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index d17719c0bc0..01c5bc1f1fc 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -1,12 +1,5 @@
import { sprintf, __ } from '~/locale';
-import {
- BYTES_IN_KIB,
- THOUSAND,
- BYTES_FORMAT_BYTES,
- BYTES_FORMAT_KIB,
- BYTES_FORMAT_MIB,
- BYTES_FORMAT_GIB,
-} from './constants';
+import { BYTES_IN_KIB, THOUSAND } from './constants';
/**
* Function that allows a number with an X amount of decimals
@@ -73,47 +66,47 @@ export function bytesToGiB(number) {
/**
* Formats the bytes in number into a more understandable
* representation. Returns an array with the first value being the human size
- * and the second value being the format (e.g., [1.5, 'KiB']).
+ * and the second value being the label (e.g., [1.5, 'KiB']).
*
- * @param {Number} size
- * @param {Number} digits - The number of digits to appear after the decimal point
- * @returns {String}
+ * @param {number} size
+ * @param {number} [digits=2] - The number of digits to appear after the decimal point
+ * @returns {string[]}
*/
export function numberToHumanSizeSplit(size, digits = 2) {
const abs = Math.abs(size);
if (abs < BYTES_IN_KIB) {
- return [size.toString(), BYTES_FORMAT_BYTES];
+ return [size.toString(), __('B')];
}
if (abs < BYTES_IN_KIB ** 2) {
- return [bytesToKiB(size).toFixed(digits), BYTES_FORMAT_KIB];
+ return [bytesToKiB(size).toFixed(digits), __('KiB')];
}
if (abs < BYTES_IN_KIB ** 3) {
- return [bytesToMiB(size).toFixed(digits), BYTES_FORMAT_MIB];
+ return [bytesToMiB(size).toFixed(digits), __('MiB')];
}
- return [bytesToGiB(size).toFixed(digits), BYTES_FORMAT_GIB];
+ return [bytesToGiB(size).toFixed(digits), __('GiB')];
}
/**
* Port of rails number_to_human_size
* Formats the bytes in number into a more understandable
- * representation (e.g., giving it 1500 yields 1.5 KB).
+ * representation (e.g., giving it 1536 yields 1.5 KiB).
*
- * @param {Number} size
- * @param {Number} digits - The number of digits to appear after the decimal point
- * @returns {String}
+ * @param {number} size
+ * @param {number} [digits=2] - The number of digits to appear after the decimal point
+ * @returns {string}
*/
export function numberToHumanSize(size, digits = 2) {
- const [humanSize, format] = numberToHumanSizeSplit(size, digits);
+ const [humanSize, label] = numberToHumanSizeSplit(size, digits);
- switch (format) {
- case BYTES_FORMAT_BYTES:
+ switch (label) {
+ case __('B'):
return sprintf(__('%{size} B'), { size: humanSize });
- case BYTES_FORMAT_KIB:
+ case __('KiB'):
return sprintf(__('%{size} KiB'), { size: humanSize });
- case BYTES_FORMAT_MIB:
+ case __('MiB'):
return sprintf(__('%{size} MiB'), { size: humanSize });
- case BYTES_FORMAT_GIB:
+ case __('GiB'):
return sprintf(__('%{size} GiB'), { size: humanSize });
default:
return '';
diff --git a/app/assets/javascripts/lib/utils/secret_detection.js b/app/assets/javascripts/lib/utils/secret_detection.js
index 4d8612aeeff..dad4af004cc 100644
--- a/app/assets/javascripts/lib/utils/secret_detection.js
+++ b/app/assets/javascripts/lib/utils/secret_detection.js
@@ -32,6 +32,14 @@ export const containsSensitiveToken = (message) => {
name: 'GitLab Deploy Token',
regex: `gldt-[0-9a-zA-Z_-]{20}`,
},
+ {
+ name: 'GitLab SCIM OAuth Access Token',
+ regex: `glsoat-[0-9a-zA-Z_-]{20}`,
+ },
+ {
+ name: 'GitLab CI Build (Job) Token',
+ regex: `glcbt-[0-9a-zA-Z]{1,5}_[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 6c30294cbbb..b30eba25aa8 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -173,7 +173,7 @@ export const truncateSha = (sha) => sha.substring(0, 8);
* @return {String}
*/
export function capitalizeFirstCharacter(text) {
- return `${text[0].toUpperCase()}${text.slice(1)}`;
+ return text?.length ? `${text[0].toUpperCase()}${text.slice(1)}` : '';
}
/**
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index c76e44a196d..b4933376d4e 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -13,7 +13,7 @@ export function initPortraitLogoDetection() {
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.replace('gl-h-10', 'gl-w-10');
}
img.classList.remove('gl-visibility-hidden');
},
diff --git a/app/assets/javascripts/merge_requests/components/compare_app.vue b/app/assets/javascripts/merge_requests/components/compare_app.vue
index 538aa090aa8..a753641ffd2 100644
--- a/app/assets/javascripts/merge_requests/components/compare_app.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_app.vue
@@ -42,6 +42,11 @@ export default {
required: false,
default: () => ({}),
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -120,6 +125,7 @@ export default {
:default="currentBranch"
:toggle-class="toggleClass.branch"
:data-qa-compare-side="compareSide"
+ :disabled="disabled"
data-testid="compare-dropdown"
@selected="selectBranch"
/>
diff --git a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
index 20989206a51..35fbf4bc4e6 100644
--- a/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
+++ b/app/assets/javascripts/merge_requests/components/compare_dropdown.vue
@@ -46,6 +46,11 @@ export default {
required: false,
default: '',
},
+ disabled: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -131,6 +136,7 @@ export default {
:toggle-text="current.text || dropdownHeader"
:header-text="dropdownHeader"
:searching="isLoading"
+ :disabled="disabled"
searchable
class="gl-w-full dropdown-target-project"
:toggle-class="[
diff --git a/app/assets/javascripts/merge_requests/components/sticky_header.vue b/app/assets/javascripts/merge_requests/components/sticky_header.vue
index 877e6142bae..e405a534cdc 100644
--- a/app/assets/javascripts/merge_requests/components/sticky_header.vue
+++ b/app/assets/javascripts/merge_requests/components/sticky_header.vue
@@ -70,6 +70,7 @@ export default {
title: { default: '' },
tabs: { default: () => [] },
isFluidLayout: { default: false },
+ blocksMerge: { default: false },
},
data() {
return {
@@ -226,7 +227,7 @@ export default {
</li>
</ul>
<div class="gl-display-none gl-lg-display-flex gl-align-items-center gl-ml-auto">
- <discussion-counter blocks-merge hide-options />
+ <discussion-counter :blocks-merge="blocksMerge" hide-options />
<div
v-if="isSignedIn"
:class="{ 'gl-display-flex gl-gap-3': isNotificationsTodosButtons }"
diff --git a/app/assets/javascripts/ml/model_registry/apps/index.js b/app/assets/javascripts/ml/model_registry/apps/index.js
index 92d159f68be..60fe5dfa804 100644
--- a/app/assets/javascripts/ml/model_registry/apps/index.js
+++ b/app/assets/javascripts/ml/model_registry/apps/index.js
@@ -1,5 +1,6 @@
import ShowMlModel from './show_ml_model.vue';
import ShowMlModelVersion from './show_ml_model_version.vue';
import IndexMlModels from './index_ml_models.vue';
+import NewMlModel from './new_ml_model.vue';
-export { ShowMlModel, ShowMlModelVersion, IndexMlModels };
+export { ShowMlModel, ShowMlModelVersion, IndexMlModels, NewMlModel };
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 e5e093db5ca..59b68fc0063 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,6 +1,6 @@
<script>
import { isEmpty } from 'lodash';
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlButton, GlTooltipDirective } 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';
@@ -10,6 +10,7 @@ import * as i18n from '../translations';
import { BASE_SORT_FIELDS, MODEL_ENTITIES } from '../constants';
import SearchBar from '../components/search_bar.vue';
import ModelRow from '../components/model_row.vue';
+import ActionsDropdown from '../components/actions_dropdown.vue';
export default {
name: 'IndexMlModels',
@@ -21,6 +22,16 @@ export default {
TitleArea,
GlBadge,
EmptyState,
+ GlButton,
+ ActionsDropdown,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ provide() {
+ return {
+ mlflowTrackingUrl: this.mlflowTrackingUrl,
+ };
},
props: {
models: {
@@ -31,11 +42,25 @@ export default {
type: Object,
required: true,
},
+ createModelPath: {
+ type: String,
+ required: true,
+ },
modelCount: {
type: Number,
required: false,
default: 0,
},
+ canWriteModelRegistry: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ mlflowTrackingUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
computed: {
hasModels() {
@@ -63,6 +88,13 @@ export default {
<template #metadata-models-count>
<metadata-item icon="machine-learning" :text="$options.i18n.modelsCountLabel(modelCount)" />
</template>
+ <template #right-actions>
+ <gl-button v-if="canWriteModelRegistry" :href="createModelPath">{{
+ $options.i18n.CREATE_MODEL_LABEL
+ }}</gl-button>
+
+ <actions-dropdown />
+ </template>
</title-area>
<template v-if="hasModels">
<search-bar :sortable-fields="$options.sortableFields" />
diff --git a/app/assets/javascripts/ml/model_registry/apps/new_ml_model.vue b/app/assets/javascripts/ml/model_registry/apps/new_ml_model.vue
new file mode 100644
index 00000000000..618d4cea1a5
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/apps/new_ml_model.vue
@@ -0,0 +1,127 @@
+<script>
+import {
+ GlForm,
+ GlFormGroup,
+ GlFormInput,
+ GlFormTextarea,
+ GlAlert,
+ GlButton,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import TitleArea from '~/vue_shared/components/registry/title_area.vue';
+import { visitUrl } from '~/lib/utils/url_utility';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import createModelMutation from '../graphql/mutations/create_model.mutation.graphql';
+import {
+ NEW_MODEL_LABEL,
+ ERROR_CREATING_MODEL_LABEL,
+ CREATE_MODEL_WITH_CLIENT_LABEL,
+ NAME_LABEL,
+ DESCRIPTION_LABEL,
+ CREATE_MODEL_LABEL,
+} from '../translations';
+
+export default {
+ name: 'NewMlModel',
+ components: {
+ TitleArea,
+ GlForm,
+ GlFormInput,
+ GlFormGroup,
+ GlFormTextarea,
+ GlAlert,
+ GlButton,
+ GlLink,
+ GlSprintf,
+ },
+ props: {
+ projectPath: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ errorMessage: '',
+ modelName: '',
+ modelDescription: '',
+ };
+ },
+ methods: {
+ async createModel() {
+ this.errorMessage = '';
+ try {
+ const variables = {
+ projectPath: this.projectPath,
+ name: this.modelName,
+ description: this.modelDescription,
+ };
+
+ const { data } = await this.$apollo.mutate({
+ mutation: createModelMutation,
+ variables,
+ });
+
+ const [error] = data?.mlModelCreate?.errors || [];
+
+ if (error) {
+ this.errorMessage = data.mlModelCreate.errors.join(', ');
+ } else {
+ visitUrl(data?.mlModelCreate?.model?._links?.showPath);
+ }
+ } catch (error) {
+ Sentry.captureException(error);
+ this.errorMessage = ERROR_CREATING_MODEL_LABEL;
+ }
+ },
+ },
+ i18n: {
+ NEW_MODEL_LABEL,
+ CREATE_MODEL_WITH_CLIENT_LABEL,
+ NAME_LABEL,
+ DESCRIPTION_LABEL,
+ CREATE_MODEL_LABEL,
+ },
+ docHref: helpPagePath('user/project/ml/model_registry/index.md'),
+};
+</script>
+
+<template>
+ <div>
+ <title-area :title="$options.i18n.NEW_MODEL_LABEL" />
+
+ <gl-alert variant="tip" icon="bulb" class="gl-mb-3" :dismissible="false">
+ <gl-sprintf :message="$options.i18n.CREATE_MODEL_WITH_CLIENT_LABEL">
+ <template #link="{ content }">
+ <gl-link :href="$options.docHref" target="_blank">{{ content }}</gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
+
+ <gl-alert
+ v-if="errorMessage"
+ :dismissible="false"
+ variant="danger"
+ class="gl-mb-3"
+ data-testid="new-model-errors"
+ >
+ {{ errorMessage }}
+ </gl-alert>
+
+ <gl-form @submit.prevent="createModel">
+ <gl-form-group :label="$options.i18n.NAME_LABEL">
+ <gl-form-input v-model="modelName" />
+ </gl-form-group>
+
+ <gl-form-group :label="$options.i18n.DESCRIPTION_LABEL" optional>
+ <gl-form-textarea v-model="modelDescription" />
+ </gl-form-group>
+
+ <gl-button type="submit" variant="confirm" class="js-no-auto-disable">{{
+ $options.i18n.CREATE_MODEL_LABEL
+ }}</gl-button>
+ </gl-form>
+ </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 51b8fca6511..e771639dade 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
@@ -1,5 +1,5 @@
<script>
-import { GlTab, GlTabs, GlBadge } from '@gitlab/ui';
+import { GlTab, GlTabs, GlBadge, GlLink } 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';
@@ -19,6 +19,7 @@ export default {
GlBadge,
MetadataItem,
ModelVersionDetail,
+ GlLink,
},
props: {
model: {
@@ -33,9 +34,6 @@ export default {
candidateCount() {
return this.model.candidateCount || 0;
},
- latestVersionTitle() {
- return `${i18n.LATEST_VERSION_LABEL}: ${this.model.latestVersion.version}`;
- },
},
i18n,
modelVersionEntity: MODEL_ENTITIES.modelVersion,
@@ -60,7 +58,14 @@ export default {
<gl-tabs class="gl-mt-4">
<gl-tab :title="$options.i18n.MODEL_DETAILS_TAB_LABEL">
<template v-if="model.latestVersion">
- <h3 class="gl-font-lg">{{ latestVersionTitle }}</h3>
+ <h3 class="gl-font-lg">
+ {{ $options.i18n.LATEST_VERSION_LABEL }}:
+
+ <gl-link :href="model.latestVersion.path" data-testid="model-version-link">
+ {{ model.latestVersion.version }}
+ </gl-link>
+ </h3>
+
<model-version-detail :model-version="model.latestVersion" />
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/actions_dropdown.vue b/app/assets/javascripts/ml/model_registry/components/actions_dropdown.vue
new file mode 100644
index 00000000000..5b4f9e27437
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/actions_dropdown.vue
@@ -0,0 +1,34 @@
+<script>
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { s__ } from '~/locale';
+
+export default {
+ components: {
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdown,
+ },
+ inject: ['mlflowTrackingUrl'],
+ computed: {
+ copyIdItem() {
+ return {
+ text: s__('MlModelRegistry|Copy MLflow tracking URL'),
+ action: () => {
+ this.$toast.show(s__('MlModelRegistry|Copied MLflow tracking URL to clipboard'));
+ },
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-disclosure-dropdown
+ placement="right"
+ category="tertiary"
+ :aria-label="__('More actions')"
+ icon="ellipsis_v"
+ no-caret
+ >
+ <gl-disclosure-dropdown-item :item="copyIdItem" :data-clipboard-text="mlflowTrackingUrl" />
+ </gl-disclosure-dropdown>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/candidate_list.vue b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue
index fc24a538293..fca4462d7d2 100644
--- a/app/assets/javascripts/ml/model_registry/components/candidate_list.vue
+++ b/app/assets/javascripts/ml/model_registry/components/candidate_list.vue
@@ -1,22 +1,17 @@
<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';
+import SearchableList from './searchable_list.vue';
+import CandidateListRow from './candidate_list_row.vue';
export default {
name: 'MlCandidateList',
components: {
- GlAlert,
CandidateListRow,
- PackagesListLoader,
- RegistryList,
+ SearchableList,
},
props: {
modelId: {
@@ -26,7 +21,7 @@ export default {
},
data() {
return {
- modelVersions: {},
+ candidates: {},
errorMessage: undefined,
};
},
@@ -49,18 +44,12 @@ export default {
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,
@@ -70,18 +59,12 @@ export default {
items() {
return this.candidates?.nodes ?? [];
},
- count() {
- return this.candidates?.count ?? 0;
- },
},
methods: {
- fetchPage({ first = null, last = null, before = null, after = null } = {}) {
+ fetchPage(newPageInfo) {
const variables = {
...this.queryVariables,
- first,
- last,
- before,
- after,
+ ...newPageInfo,
};
this.$apollo.queries.candidates.fetchMore({
@@ -91,18 +74,6 @@ export default {
},
});
},
- 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,
@@ -111,29 +82,19 @@ export default {
</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>
+ <searchable-list
+ :page-info="pageInfo"
+ :items="items"
+ :error-message="errorMessage"
+ @fetch-page="fetchPage"
+ >
+ <template #empty-state>
+ {{ $options.i18n.NO_CANDIDATES_LABEL }}
+ </template>
+
+ <template #item="{ item }">
+ <candidate-list-row :candidate="item" />
+ </template>
+ </searchable-list>
</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
index 6b44cb2f613..5a649a9596a 100644
--- a/app/assets/javascripts/ml/model_registry/components/model_version_list.vue
+++ b/app/assets/javascripts/ml/model_registry/components/model_version_list.vue
@@ -1,23 +1,18 @@
<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 SearchableList from './searchable_list.vue';
import EmptyState from './empty_state.vue';
import ModelVersionRow from './model_version_row.vue';
export default {
components: {
EmptyState,
- GlAlert,
ModelVersionRow,
- PackagesListLoader,
- RegistryList,
+ SearchableList,
},
props: {
modelId: {
@@ -50,18 +45,12 @@ export default {
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,
@@ -71,31 +60,12 @@ export default {
versions() {
return this.modelVersions?.nodes ?? [];
},
- count() {
- return this.modelVersions?.count ?? 0;
- },
},
methods: {
- fetchPreviousVersionsPage() {
+ fetchPage(pageInfo) {
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,
+ ...pageInfo,
};
this.$apollo.queries.modelVersions.fetchMore({
@@ -110,28 +80,18 @@ export default {
};
</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>
+ <searchable-list
+ :page-info="pageInfo"
+ :items="versions"
+ :error-message="errorMessage"
+ @fetch-page="fetchPage"
+ >
+ <template #empty-state>
+ <empty-state :entity-type="$options.modelVersionEntity" />
+ </template>
+
+ <template #item="{ item }">
+ <model-version-row :model-version="item" />
+ </template>
+ </searchable-list>
</template>
diff --git a/app/assets/javascripts/ml/model_registry/components/searchable_list.vue b/app/assets/javascripts/ml/model_registry/components/searchable_list.vue
new file mode 100644
index 00000000000..05062ae6fbf
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/components/searchable_list.vue
@@ -0,0 +1,79 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
+import { GRAPHQL_PAGE_SIZE } from '~/ml/model_registry/constants';
+
+export default {
+ name: 'SearchableList',
+ components: { PackagesListLoader, RegistryList, GlAlert },
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ pageInfo: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ errorMessage: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ isListEmpty() {
+ return this.items.length === 0;
+ },
+ },
+ methods: {
+ prevPage() {
+ const pageInfo = {
+ first: null,
+ last: GRAPHQL_PAGE_SIZE,
+ before: this.pageInfo.startCursor,
+ };
+
+ this.$emit('fetch-page', pageInfo);
+ },
+ nextPage() {
+ const pageInfo = {
+ first: GRAPHQL_PAGE_SIZE,
+ last: null,
+ after: this.pageInfo.endCursor,
+ };
+
+ this.$emit('fetch-page', pageInfo);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <packages-list-loader v-if="isLoading" />
+ <gl-alert v-else-if="errorMessage" variant="danger" :dismissible="false">
+ {{ errorMessage }}
+ </gl-alert>
+ <slot v-else-if="isListEmpty" name="empty-state"></slot>
+ <registry-list
+ v-else
+ :hidden-delete="true"
+ :is-loading="isLoading"
+ :items="items"
+ :pagination="pageInfo"
+ @prev-page="prevPage"
+ @next-page="nextPage"
+ >
+ <template #default="{ item }">
+ <slot name="item" :item="item"></slot>
+ </template>
+ </registry-list>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ml/model_registry/graphql/mutations/create_model.mutation.graphql b/app/assets/javascripts/ml/model_registry/graphql/mutations/create_model.mutation.graphql
new file mode 100644
index 00000000000..af801474e80
--- /dev/null
+++ b/app/assets/javascripts/ml/model_registry/graphql/mutations/create_model.mutation.graphql
@@ -0,0 +1,11 @@
+mutation createModel($projectPath: ID!, $name: String!, $description: String) {
+ mlModelCreate(input: { projectPath: $projectPath, name: $name, description: $description }) {
+ model {
+ id
+ _links {
+ showPath
+ }
+ }
+ errors
+ }
+}
diff --git a/app/assets/javascripts/ml/model_registry/translations.js b/app/assets/javascripts/ml/model_registry/translations.js
index 968ec83434d..006142979e2 100644
--- a/app/assets/javascripts/ml/model_registry/translations.js
+++ b/app/assets/javascripts/ml/model_registry/translations.js
@@ -32,6 +32,15 @@ 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 NEW_MODEL_LABEL = s__('MlModelRegistry|New model');
+export const CREATE_MODEL_LABEL = s__('MlModelRegistry|Create model');
+export const ERROR_CREATING_MODEL_LABEL = s__(
+ 'MlModelRegistry|An error has occurred when saving the model.',
+);
+export const CREATE_MODEL_WITH_CLIENT_LABEL = s__(
+ 'MlModelRegistry|Creating models is also possible through the MLflow client. %{linkStart}Follow the documentation to learn more.%{linkEnd}',
+);
+export const NAME_LABEL = __('Name');
export const makeLoadVersionsErrorMessage = (message) =>
sprintf(s__('MlModelRegistry|Failed to load model versions with error: %{message}'), {
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index 9aaae960b6f..77ce5ea5910 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -352,7 +352,7 @@ export default {
</template>
</gl-sprintf>
</div>
- <div class="flash-container timeline-content"></div>
+ <div class="flash-container"></div>
<form :data-line-code="lineCode" class="edit-note common-note-form js-quick-submit gfm-form">
<comment-field-layout :noteable-data="getNoteableData" :is-internal-note="isInternalNote">
<markdown-editor
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 2524b9efdb6..86f93ee425e 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -1,6 +1,7 @@
<script>
// eslint-disable-next-line no-restricted-imports
import { mapGetters, mapActions } from 'vuex';
+import { v4 as uuidv4 } from 'uuid';
import highlightCurrentUser from '~/behaviors/markdown/highlight_current_user';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
@@ -39,6 +40,11 @@ export default {
AiSummary: () => import('ee_component/notes/components/ai_summary.vue'),
},
mixins: [glFeatureFlagsMixin()],
+ provide() {
+ return {
+ summarizeClientSubscriptionId: uuidv4(),
+ };
+ },
props: {
noteableData: {
type: Object,
diff --git a/app/assets/javascripts/observability/client.js b/app/assets/javascripts/observability/client.js
index 3a793c9dc14..b7e4ae8e8ea 100644
--- a/app/assets/javascripts/observability/client.js
+++ b/app/assets/javascripts/observability/client.js
@@ -1,3 +1,4 @@
+import { isValidDate } from '~/lib/utils/datetime_utility';
import * as Sentry from '~/sentry/sentry_browser_wrapper';
import axios from '~/lib/utils/axios_utils';
import { logError } from '~/lib/logger';
@@ -128,6 +129,24 @@ function handleAttributeFilter(filterValue, filterOperator, searchParams) {
}
}
+function handlePeriodFilter(rawValue, filterName, filterParams) {
+ if (rawValue.trim().indexOf(' ') < 0) {
+ filterParams.append(filterName, rawValue.trim());
+ return;
+ }
+
+ const dateParts = rawValue.split(' - ');
+ if (dateParts.length === 2) {
+ const [start, end] = dateParts;
+ const startDate = new Date(start);
+ const endDate = new Date(end);
+ if (isValidDate(startDate) && isValidDate(endDate)) {
+ filterParams.append('start_time', startDate.toISOString());
+ filterParams.append('end_time', endDate.toISOString());
+ }
+ }
+}
+
/**
* Builds URLSearchParams from a filter object of type { [filterName]: undefined | null | Array<{operator: String, value: any} }
* e.g:
@@ -154,6 +173,8 @@ function filterObjToQueryParams(filterObj) {
validFilters.forEach(({ operator, value: rawValue }) => {
if (filterName === 'attribute') {
handleAttributeFilter(rawValue, operator, filterParams);
+ } else if (filterName === 'period') {
+ handlePeriodFilter(rawValue, filterName, filterParams);
} else {
const paramName = getFilterParamName(filterName, operator);
let value = rawValue;
diff --git a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
index dba738de5e1..ebe69925491 100644
--- a/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
+++ b/app/assets/javascripts/organizations/groups_and_projects/components/app.vue
@@ -172,6 +172,6 @@ export default {
</div>
</div>
</div>
- <component :is="routerView" />
+ <component :is="routerView" list-item-class="gl-px-5" />
</div>
</template>
diff --git a/app/assets/javascripts/organizations/mock_data.js b/app/assets/javascripts/organizations/mock_data.js
index 0c363cf7c7f..92381087917 100644
--- a/app/assets/javascripts/organizations/mock_data.js
+++ b/app/assets/javascripts/organizations/mock_data.js
@@ -39,14 +39,14 @@ export const organizationProjects = {
id: 'gid://gitlab/Project/8',
nameWithNamespace: 'Twitter / Typeahead.Js',
webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
- topics: ['JavaScript', 'Vue.js'],
+ topics: ['JavaScript', 'Vue.js', 'GraphQL', 'Jest', 'CSS', 'HTML'],
forksCount: 4,
avatarUrl: null,
starCount: 0,
visibility: 'public',
openIssuesCount: 48,
descriptionHtml:
- '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi. Sed sit amet iaculis neque. Morbi vel convallis elit. Aliquam vitae arcu orci. Aenean sem velit, dapibus eget enim id, tempor lobortis orci. Pellentesque dignissim nec velit eget sagittis. Maecenas lectus sapien, tincidunt ac cursus a, aliquam eu ipsum. Aliquam posuere maximus augue, ut vehicula elit vulputate condimentum. In libero leo, vehicula nec risus in, ullamcorper convallis risus. Phasellus sit amet lectus sit amet sem volutpat cursus. Nullam facilisis nulla nec lacus pretium, in pretium ex aliquam.</p>',
issuesAccessLevel: 'enabled',
forkingAccessLevel: 'enabled',
isForked: true,
@@ -141,7 +141,7 @@ export const organizationGroups = {
parent: null,
webUrl: 'http://127.0.0.1:3000/groups/Commit451',
descriptionHtml:
- '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa.</p>',
+ '<p data-sourcepos="1:1-1:52" dir="auto">Autem praesentium vel ut ratione itaque ullam culpa. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Suspendisse libero sem, congue ut sem id, semper pharetra ante. Sed at dui ac nunc pellentesque congue. Phasellus posuere, nisl non pellentesque dignissim, lacus nisi molestie ex, vel placerat neque ligula non libero. Curabitur ipsum enim, pretium eu dignissim vitae, euismod nec nibh. Praesent eget ipsum eleifend, pellentesque tortor vel, consequat neque. Etiam suscipit dolor massa, sed consectetur nunc tincidunt ut. Suspendisse posuere malesuada finibus. Maecenas iaculis et diam eu iaculis. Proin at nulla sit amet erat sollicitudin suscipit sit amet a libero.</p>',
avatarUrl: null,
descendantGroupsCount: 0,
projectsCount: 3,
diff --git a/app/assets/javascripts/organizations/new/components/app.vue b/app/assets/javascripts/organizations/new/components/app.vue
index f7f7b79d52b..3a2b786dbae 100644
--- a/app/assets/javascripts/organizations/new/components/app.vue
+++ b/app/assets/javascripts/organizations/new/components/app.vue
@@ -42,7 +42,15 @@ export default {
} = await this.$apollo.mutate({
mutation: organizationCreateMutation,
variables: {
- input: { name: formValues.name, path: formValues.path },
+ input: {
+ name: formValues.name,
+ path: formValues.path,
+ description: formValues.description,
+ avatar: formValues.avatar,
+ },
+ },
+ context: {
+ hasUpload: formValues.avatar instanceof File,
},
});
diff --git a/app/assets/javascripts/organizations/new/index.js b/app/assets/javascripts/organizations/new/index.js
index 9c7e5344800..563e366b2c6 100644
--- a/app/assets/javascripts/organizations/new/index.js
+++ b/app/assets/javascripts/organizations/new/index.js
@@ -13,7 +13,9 @@ export const initOrganizationsNew = () => {
const {
dataset: { appData },
} = el;
- const { organizationsPath, rootUrl } = convertObjectPropsToCamelCase(JSON.parse(appData));
+ const { organizationsPath, rootUrl, previewMarkdownPath } = convertObjectPropsToCamelCase(
+ JSON.parse(appData),
+ );
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -26,6 +28,7 @@ export const initOrganizationsNew = () => {
provide: {
organizationsPath,
rootUrl,
+ previewMarkdownPath,
},
render(createElement) {
return createElement(App);
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 1acc4c54f75..1cea2ceeb90 100644
--- a/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
+++ b/app/assets/javascripts/organizations/settings/general/components/organization_settings.vue
@@ -3,7 +3,12 @@ import { s__, __ } from '~/locale';
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 {
+ FORM_FIELD_NAME,
+ FORM_FIELD_ID,
+ FORM_FIELD_DESCRIPTION,
+ FORM_FIELD_AVATAR,
+} from '~/organizations/shared/constants';
import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import { TYPE_ORGANIZATION } from '~/graphql_shared/constants';
@@ -25,7 +30,7 @@ export default {
),
successMessage: s__('Organization|Organization was successfully updated.'),
},
- fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID],
+ fieldsToRender: [FORM_FIELD_NAME, FORM_FIELD_ID, FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR],
data() {
return {
loading: false,
@@ -33,9 +38,24 @@ export default {
};
},
methods: {
+ avatarInput(formValues) {
+ // Organization has an avatar and it is been explicitly removed.
+ if (this.organization.avatar && formValues.avatar === null) {
+ return { avatar: null };
+ }
+
+ // Avatar has been set or changed.
+ if (formValues.avatar instanceof File) {
+ return { avatar: formValues.avatar };
+ }
+
+ // Avatar has not been changed at all, do not include the `avatar` key in input.
+ return {};
+ },
async onSubmit(formValues) {
this.errors = [];
this.loading = true;
+
try {
const {
data: {
@@ -47,8 +67,13 @@ export default {
input: {
id: convertToGraphQLId(TYPE_ORGANIZATION, this.organization.id),
name: formValues.name,
+ description: formValues.description,
+ ...this.avatarInput(formValues),
},
},
+ context: {
+ hasUpload: formValues.avatar instanceof File,
+ },
});
if (errors.length) {
diff --git a/app/assets/javascripts/organizations/settings/general/index.js b/app/assets/javascripts/organizations/settings/general/index.js
index 138606a0aab..3ac1243ff0f 100644
--- a/app/assets/javascripts/organizations/settings/general/index.js
+++ b/app/assets/javascripts/organizations/settings/general/index.js
@@ -13,9 +13,12 @@ export const initOrganizationsSettingsGeneral = () => {
const {
dataset: { appData },
} = el;
- const { organization, organizationsPath, rootUrl } = convertObjectPropsToCamelCase(
- JSON.parse(appData),
- );
+ const {
+ organization,
+ organizationsPath,
+ rootUrl,
+ previewMarkdownPath,
+ } = convertObjectPropsToCamelCase(JSON.parse(appData));
const apolloProvider = new VueApollo({
defaultClient: createDefaultClient(),
@@ -29,6 +32,7 @@ export const initOrganizationsSettingsGeneral = () => {
organization,
organizationsPath,
rootUrl,
+ previewMarkdownPath,
},
render(createElement) {
return createElement(App);
diff --git a/app/assets/javascripts/organizations/shared/components/groups_view.vue b/app/assets/javascripts/organizations/shared/components/groups_view.vue
index eaa3017ef97..87bc79a5405 100644
--- a/app/assets/javascripts/organizations/shared/components/groups_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/groups_view.vue
@@ -32,6 +32,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -77,6 +82,11 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <groups-list v-else-if="groups.length" :groups="groups" show-group-icon />
+ <groups-list
+ v-else-if="groups.length"
+ :groups="groups"
+ show-group-icon
+ :list-item-class="listItemClass"
+ />
<gl-empty-state v-else v-bind="emptyStateProps" />
</template>
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 c5bb16b944a..49519369e9a 100644
--- a/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
+++ b/app/assets/javascripts/organizations/shared/components/new_edit_form.vue
@@ -3,10 +3,15 @@ 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 AvatarUploadDropzone from '~/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
import {
FORM_FIELD_NAME,
FORM_FIELD_ID,
FORM_FIELD_PATH,
+ FORM_FIELD_DESCRIPTION,
+ FORM_FIELD_AVATAR,
FORM_FIELD_PATH_VALIDATORS,
} from '../constants';
import OrganizationUrlField from './organization_url_field.vue';
@@ -18,12 +23,28 @@ export default {
GlFormFields,
GlButton,
OrganizationUrlField,
+ AvatarUploadDropzone,
+ MarkdownField,
},
i18n: {
cancel: __('Cancel'),
},
formId: 'new-organization-form',
- inject: ['organizationsPath'],
+ markdownDocsPath: helpPagePath('user/organization/index', {
+ anchor: 'organization-description-supported-markdown',
+ }),
+ restrictedToolBarItems: [
+ 'code',
+ 'quote',
+ 'bullet-list',
+ 'numbered-list',
+ 'task-list',
+ 'collapsible-section',
+ 'table',
+ 'attach-file',
+ 'full-screen',
+ ],
+ inject: ['organizationsPath', 'previewMarkdownPath'],
props: {
loading: {
type: Boolean,
@@ -36,6 +57,8 @@ export default {
return {
[FORM_FIELD_NAME]: '',
[FORM_FIELD_PATH]: '',
+ [FORM_FIELD_DESCRIPTION]: '',
+ [FORM_FIELD_AVATAR]: null,
};
},
},
@@ -43,7 +66,7 @@ export default {
type: Array,
required: false,
default() {
- return [FORM_FIELD_NAME, FORM_FIELD_PATH];
+ return [FORM_FIELD_NAME, FORM_FIELD_PATH, FORM_FIELD_DESCRIPTION, FORM_FIELD_AVATAR];
},
},
submitButtonText: {
@@ -98,6 +121,19 @@ export default {
class: 'gl-w-full',
},
},
+ [FORM_FIELD_DESCRIPTION]: {
+ label: s__('Organization|Organization description (optional)'),
+ groupAttrs: {
+ class: 'gl-w-full common-note-form',
+ },
+ },
+ [FORM_FIELD_AVATAR]: {
+ label: s__('Organization|Organization avatar'),
+ groupAttrs: {
+ class: 'gl-w-full',
+ labelSrOnly: true,
+ },
+ },
};
return Object.entries(fields).reduce((accumulator, [fieldKey, fieldDefinition]) => {
@@ -126,6 +162,7 @@ export default {
formFieldsInputEvent(event);
this.hasPathBeenManuallySet = true;
},
+ helpPagePath,
},
};
</script>
@@ -148,11 +185,46 @@ export default {
@blur="blur"
/>
</template>
+ <template #input(description)="{ id, value, input, blur }">
+ <div class="gl-md-form-input-xl">
+ <markdown-field
+ :can-attach-file="false"
+ :markdown-preview-path="previewMarkdownPath"
+ :markdown-docs-path="$options.markdownDocsPath"
+ :textarea-value="value"
+ :restricted-tool-bar-items="$options.restrictedToolBarItems"
+ >
+ <template #textarea>
+ <textarea
+ :id="id"
+ :value="value"
+ class="note-textarea js-gfm-input markdown-area"
+ maxlength="1024"
+ @input="input($event.target.value)"
+ @blur="blur"
+ ></textarea>
+ </template>
+ </markdown-field>
+ </div>
+ </template>
+ <template #input(avatar)="{ input, value }">
+ <avatar-upload-dropzone
+ :value="value"
+ :entity="formValues"
+ :label="fields.avatar.label"
+ @input="input"
+ />
+ </template>
</gl-form-fields>
<div class="gl-display-flex gl-gap-3">
- <gl-button type="submit" variant="confirm" class="js-no-auto-disable" :loading="loading">{{
- submitButtonText
- }}</gl-button>
+ <gl-button
+ type="submit"
+ variant="confirm"
+ class="js-no-auto-disable"
+ :loading="loading"
+ data-testid="submit-button"
+ >{{ submitButtonText }}</gl-button
+ >
<gl-button v-if="showCancelButton" :href="organizationsPath">{{
$options.i18n.cancel
}}</gl-button>
diff --git a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
index d36f62477e6..c4b31e6b8a6 100644
--- a/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
+++ b/app/assets/javascripts/organizations/shared/components/organization_url_field.vue
@@ -39,7 +39,7 @@ export default {
</script>
<template>
- <gl-form-input-group>
+ <gl-form-input-group class="gl-md-form-input-xl">
<template #prepend>
<gl-input-group-text class="organization-root-path">
<gl-truncate :text="baseUrl" position="middle" />
@@ -50,7 +50,7 @@ export default {
:id="id"
:value="value"
:placeholder="$options.i18n.pathPlaceholder"
- class="gl-h-auto! gl-md-form-input-lg"
+ class="gl-h-auto!"
@input="$emit('input', $event)"
@blur="$emit('blur', $event)"
/>
diff --git a/app/assets/javascripts/organizations/shared/components/projects_view.vue b/app/assets/javascripts/organizations/shared/components/projects_view.vue
index 9bf4e597884..323a8895821 100644
--- a/app/assets/javascripts/organizations/shared/components/projects_view.vue
+++ b/app/assets/javascripts/organizations/shared/components/projects_view.vue
@@ -36,6 +36,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -81,6 +86,11 @@ export default {
<template>
<gl-loading-icon v-if="isLoading" class="gl-mt-5" size="md" />
- <projects-list v-else-if="projects.length" :projects="projects" show-project-icon />
+ <projects-list
+ v-else-if="projects.length"
+ :projects="projects"
+ show-project-icon
+ :list-item-class="listItemClass"
+ />
<gl-empty-state v-else v-bind="emptyStateProps" />
</template>
diff --git a/app/assets/javascripts/organizations/shared/constants.js b/app/assets/javascripts/organizations/shared/constants.js
index 7287d84f99f..c4f4f348ff2 100644
--- a/app/assets/javascripts/organizations/shared/constants.js
+++ b/app/assets/javascripts/organizations/shared/constants.js
@@ -4,6 +4,8 @@ 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_DESCRIPTION = 'description';
+export const FORM_FIELD_AVATAR = 'avatar';
export const FORM_FIELD_PATH_VALIDATORS = [
formValidators.required(s__('Organization|Organization URL is required.')),
diff --git a/app/assets/javascripts/organizations/show/components/app.vue b/app/assets/javascripts/organizations/show/components/app.vue
index 47264d80454..4cd5ada1d66 100644
--- a/app/assets/javascripts/organizations/show/components/app.vue
+++ b/app/assets/javascripts/organizations/show/components/app.vue
@@ -1,11 +1,12 @@
<script>
import OrganizationAvatar from './organization_avatar.vue';
+import OrganizationDescription from './organization_description.vue';
import GroupsAndProjects from './groups_and_projects.vue';
import AssociationCounts from './association_counts.vue';
export default {
name: 'OrganizationShowApp',
- components: { OrganizationAvatar, GroupsAndProjects, AssociationCounts },
+ components: { OrganizationAvatar, OrganizationDescription, GroupsAndProjects, AssociationCounts },
props: {
organization: {
type: Object,
@@ -26,6 +27,7 @@ export default {
<template>
<div class="gl-py-6">
<organization-avatar :organization="organization" />
+ <organization-description :organization="organization" />
<association-counts
:association-counts="associationCounts"
:groups-and-projects-organization-path="groupsAndProjectsOrganizationPath"
diff --git a/app/assets/javascripts/organizations/show/components/organization_avatar.vue b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
index c57ee0ea5b5..d569af3e9b4 100644
--- a/app/assets/javascripts/organizations/show/components/organization_avatar.vue
+++ b/app/assets/javascripts/organizations/show/components/organization_avatar.vue
@@ -44,6 +44,7 @@ export default {
:entity-name="organization.name"
:shape="$options.AVATAR_SHAPE_OPTION_RECT"
:size="64"
+ :src="organization.avatar_url"
/>
<div class="gl-ml-3">
<div class="gl-display-flex gl-align-items-center">
diff --git a/app/assets/javascripts/organizations/show/components/organization_description.vue b/app/assets/javascripts/organizations/show/components/organization_description.vue
new file mode 100644
index 00000000000..6222bdcd082
--- /dev/null
+++ b/app/assets/javascripts/organizations/show/components/organization_description.vue
@@ -0,0 +1,24 @@
+<script>
+import SafeHtml from '~/vue_shared/directives/safe_html';
+
+export default {
+ name: 'OrganizationDescription',
+ directives: {
+ SafeHtml,
+ },
+ props: {
+ organization: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-if="organization.description_html"
+ v-safe-html="organization.description_html"
+ class="gl-mt-5 md"
+ ></div>
+</template>
diff --git a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
index 75af0286e12..7fd7c0fe542 100644
--- a/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
+++ b/app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/details_header.vue
@@ -142,7 +142,12 @@ export default {
</template>
<template v-if="formattedSize" #metadata-size>
- <metadata-item icon="disk" :text="formattedSize" data-testid="image-size" />
+ <metadata-item
+ icon="disk"
+ :text="formattedSize"
+ :text-tooltip="s__('ContainerRegistry|Includes both tagged and untagged images')"
+ data-testid="image-size"
+ />
</template>
<template #metadata-cleanup>
diff --git a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
index 6ff7d58fd9e..89ab184d808 100644
--- a/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
+++ b/app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue
@@ -1,6 +1,6 @@
<script>
import { GlAlert } from '@gitlab/ui';
-import { n__ } from '~/locale';
+import { __ } from '~/locale';
import PackagesSettings from '~/packages_and_registries/settings/group/components/packages_settings.vue';
import PackagesForwardingSettings from '~/packages_and_registries/settings/group/components/packages_forwarding_settings.vue';
import DependencyProxySettings from '~/packages_and_registries/settings/group/components/dependency_proxy_settings.vue';
@@ -50,21 +50,13 @@ export default {
dismissAlert() {
this.alertMessage = null;
},
- handleSuccess(amount = 1) {
- const successMessage = n__(
- 'Setting saved successfully',
- 'Settings saved successfully',
- amount,
- );
+ handleSuccess() {
+ const successMessage = __('Settings saved successfully.');
this.$toast.show(successMessage);
this.dismissAlert();
},
- handleError(amount = 1) {
- const errorMessage = n__(
- 'An error occurred while saving the setting',
- 'An error occurred while saving the settings',
- amount,
- );
+ handleError() {
+ const errorMessage = __('An error occurred while saving the settings.');
this.alertMessage = errorMessage;
},
},
@@ -81,14 +73,14 @@ export default {
class="settings-section-no-bottom"
:package-settings="packageSettings"
:is-loading="isLoading"
- @success="handleSuccess(2)"
- @error="handleError(2)"
+ @success="handleSuccess"
+ @error="handleError"
/>
<packages-forwarding-settings
:forward-settings="packageSettings"
- @success="handleSuccess(2)"
- @error="handleError(2)"
+ @success="handleSuccess"
+ @error="handleError"
/>
<dependency-proxy-settings
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
new file mode 100644
index 00000000000..d9177778803
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/components/packages_protection_rules.vue
@@ -0,0 +1,115 @@
+<script>
+import { GlCard, GlTable, GlLoadingIcon } from '@gitlab/ui';
+import packagesProtectionRuleQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql';
+import SettingsBlock from '~/packages_and_registries/shared/components/settings_block.vue';
+import { s__ } from '~/locale';
+
+const PAGINATION_DEFAULT_PER_PAGE = 10;
+
+export default {
+ components: {
+ SettingsBlock,
+ GlCard,
+ GlTable,
+ GlLoadingIcon,
+ },
+ inject: ['projectPath'],
+ i18n: {
+ settingBlockTitle: s__('PackageRegistry|Protected packages'),
+ settingBlockDescription: s__(
+ 'PackageRegistry|When a package is protected then only certain user roles are able to update and delete the protected package. This helps to avoid tampering with the package.',
+ ),
+ },
+ data() {
+ return {
+ fetchSettingsError: false,
+ packageProtectionRules: [],
+ };
+ },
+ computed: {
+ tableItems() {
+ return this.packageProtectionRules.map((packagesProtectionRule) => {
+ return {
+ col_1_package_name_pattern: packagesProtectionRule.packageNamePattern,
+ col_2_package_type: packagesProtectionRule.packageType,
+ col_3_push_protected_up_to_access_level:
+ packagesProtectionRule.pushProtectedUpToAccessLevel,
+ };
+ });
+ },
+ totalItems() {
+ return this.packageProtectionRules.length;
+ },
+ isLoadingPackageProtectionRules() {
+ return this.$apollo.queries.packageProtectionRules.loading;
+ },
+ },
+ apollo: {
+ packageProtectionRules: {
+ query: packagesProtectionRuleQuery,
+ variables() {
+ return {
+ projectPath: this.projectPath,
+ first: PAGINATION_DEFAULT_PER_PAGE,
+ };
+ },
+ update: (data) => {
+ return data.project?.packagesProtectionRules?.nodes || [];
+ },
+ error(e) {
+ this.fetchSettingsError = e;
+ },
+ },
+ },
+ fields: [
+ {
+ key: 'col_1_package_name_pattern',
+ label: s__('PackageRegistry|Package name pattern'),
+ },
+ { key: 'col_2_package_type', label: s__('PackageRegistry|Package type') },
+ {
+ key: 'col_3_push_protected_up_to_access_level',
+ label: s__('PackageRegistry|Push protected up to access level'),
+ },
+ ],
+};
+</script>
+
+<template>
+ <settings-block>
+ <template #title>{{ $options.i18n.settingBlockTitle }}</template>
+
+ <template #description>
+ {{ $options.i18n.settingBlockDescription }}
+ </template>
+
+ <template #default>
+ <gl-card
+ class="gl-new-card"
+ header-class="gl-new-card-header"
+ body-class="gl-new-card-body gl-px-0"
+ >
+ <template #header>
+ <div class="gl-new-card-title-wrapper gl-justify-content-space-between">
+ <h3 class="gl-new-card-title">{{ $options.i18n.settingBlockTitle }}</h3>
+ </div>
+ </template>
+
+ <template #default>
+ <gl-table
+ :items="tableItems"
+ :fields="$options.fields"
+ show-empty
+ stacked="md"
+ class="mb-3"
+ :busy="isLoadingPackageProtectionRules"
+ >
+ <template #table-busy>
+ <gl-loading-icon size="sm" class="gl-my-5" />
+ </template>
+ </gl-table>
+ </template>
+ </gl-card>
+ </template>
+ </settings-block>
+</template>
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 06af69ff250..8e4c50b199b 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
@@ -8,6 +8,7 @@ import {
} from '~/packages_and_registries/settings/project/constants';
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';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
export default {
components: {
@@ -18,7 +19,10 @@ export default {
),
GlAlert,
PackagesCleanupPolicy,
+ PackagesProtectionRules: () =>
+ import('~/packages_and_registries/settings/project/components/packages_protection_rules.vue'),
},
+ mixins: [glFeatureFlagsMixin()],
inject: [
'showContainerRegistrySettings',
'showPackageRegistrySettings',
@@ -32,6 +36,11 @@ export default {
showAlert: false,
};
},
+ computed: {
+ showProtectedPackagesSettings() {
+ return this.showPackageRegistrySettings && this.glFeatures.packagesProtectedPackages;
+ },
+ },
mounted() {
this.checkAlert();
},
@@ -60,6 +69,7 @@ export default {
>
{{ $options.i18n.UPDATE_SETTINGS_SUCCESS_MESSAGE }}
</gl-alert>
+ <packages-protection-rules v-if="showProtectedPackagesSettings" />
<packages-cleanup-policy v-if="showPackageRegistrySettings" />
<container-expiration-policy v-if="showContainerRegistrySettings" />
<dependency-proxy-packages-settings v-if="showDependencyProxySettings" />
diff --git a/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
new file mode 100644
index 00000000000..e0a072b93e4
--- /dev/null
+++ b/app/assets/javascripts/packages_and_registries/settings/project/graphql/queries/get_packages_protection_rules.query.graphql
@@ -0,0 +1,13 @@
+query getProjectPackageProtectionRules($projectPath: ID!, $first: Int) {
+ project(fullPath: $projectPath) {
+ id
+ packagesProtectionRules(first: $first) {
+ nodes {
+ id
+ packageNamePattern
+ packageType
+ pushProtectedUpToAccessLevel
+ }
+ }
+ }
+}
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 1d54dad43a9..e66040c5a99 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
@@ -1,6 +1,5 @@
<script>
import {
- GlButton,
GlEmptyState,
GlIcon,
GlLink,
@@ -22,7 +21,6 @@ import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { isImporting } from '../utils';
import { DEFAULT_ERROR } from '../utils/error_messages';
@@ -43,7 +41,6 @@ const tableCell = (config) => ({
export default {
components: {
- GlButton,
GlEmptyState,
GlIcon,
GlLink,
@@ -59,8 +56,6 @@ export default {
GlTooltip,
},
- mixins: [glFeatureFlagMixin()],
-
inject: ['realtimeChangesPath'],
data() {
@@ -107,10 +102,6 @@ export default {
.map((item) => item.bulk_import_id);
},
- showDetailsLink() {
- return this.glFeatures.bulkImportDetailsPage;
- },
-
paginationConfigCopy() {
return { ...this.paginationConfig };
},
@@ -265,7 +256,7 @@ export default {
<template #cell(created_at)="{ value }">
<time-ago :time="value" />
</template>
- <template #cell(status)="{ value, item, toggleDetails, detailsShowing }">
+ <template #cell(status)="{ value, item }">
<div
class="gl-display-flex gl-flex-direction-column gl-lg-flex-direction-row gl-align-items-flex-start gl-justify-content-space-between gl-gap-3"
>
@@ -273,20 +264,10 @@ export default {
:id="item.bulk_import_id"
:entity-id="item.id"
:has-failures="item.has_failures"
- :show-details-link="showDetailsLink"
:status="value"
/>
- <gl-button
- v-if="!showDetailsLink && item.failures.length"
- :selected="detailsShowing"
- @click="toggleDetails"
- >{{ __('Details') }}</gl-button
- >
</div>
</template>
- <template #row-details="{ item }">
- <pre><code>{{ item.failures }}</code></pre>
- </template>
</gl-table-lite>
<pagination-bar
:page-info="pageInfo"
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 8cb1462c883..3d877bb3abb 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -16,8 +16,9 @@ if (mrNewCompareNode) {
const sourceCompareEl = document.getElementById('js-source-project-dropdown');
const compareEl = document.querySelector('.js-merge-request-new-compare');
const targetBranch = Vue.observable({ name: '' });
-
const currentSourceBranch = JSON.parse(sourceCompareEl.dataset.currentBranch);
+ const sourceBranch = Vue.observable(currentSourceBranch);
+
// eslint-disable-next-line no-new
new Vue({
el: sourceCompareEl,
@@ -52,6 +53,9 @@ if (mrNewCompareNode) {
if (targetBranchName) {
targetBranch.name = targetBranchName;
}
+
+ sourceBranch.value = branchName;
+ sourceBranch.text = branchName;
},
},
render(h) {
@@ -102,9 +106,14 @@ if (mrNewCompareNode) {
return currentTargetBranch;
},
+ isDisabled() {
+ return !sourceBranch.value;
+ },
},
render(h) {
- return h(CompareApp, { props: { currentBranch: this.currentBranch } });
+ return h(CompareApp, {
+ props: { currentBranch: this.currentBranch, disabled: this.isDisabled },
+ });
},
});
} else {
diff --git a/app/assets/javascripts/pages/projects/merge_requests/page.js b/app/assets/javascripts/pages/projects/merge_requests/page.js
index a9d281fc899..69032455fe3 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/page.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/page.js
@@ -28,7 +28,15 @@ requestIdleCallback(() => {
if (el) {
const { data } = el.dataset;
- const { iid, projectPath, title, tabs, isFluidLayout, sourceProjectPath } = JSON.parse(data);
+ const {
+ iid,
+ projectPath,
+ title,
+ tabs,
+ isFluidLayout,
+ sourceProjectPath,
+ blocksMerge,
+ } = JSON.parse(data);
// eslint-disable-next-line no-new
new Vue({
@@ -42,6 +50,7 @@ requestIdleCallback(() => {
title,
tabs,
isFluidLayout: parseBoolean(isFluidLayout),
+ blocksMerge: parseBoolean(blocksMerge),
sourceProjectPath,
},
render(h) {
diff --git a/app/assets/javascripts/pages/projects/ml/models/index/index.js b/app/assets/javascripts/pages/projects/ml/models/index/index.js
index 3f8ef4910a7..54dcf28164f 100644
--- a/app/assets/javascripts/pages/projects/ml/models/index/index.js
+++ b/app/assets/javascripts/pages/projects/ml/models/index/index.js
@@ -1,4 +1,4 @@
import { initSimpleApp } from '~/helpers/init_simple_app_helper';
import { IndexMlModels } from '~/ml/model_registry/apps';
-initSimpleApp('#js-index-ml-models', IndexMlModels);
+initSimpleApp('#js-index-ml-models', IndexMlModels, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/ml/models/new/index.js b/app/assets/javascripts/pages/projects/ml/models/new/index.js
new file mode 100644
index 00000000000..8dc5c5aea4e
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/ml/models/new/index.js
@@ -0,0 +1,4 @@
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+import { NewMlModel } from '~/ml/model_registry/apps';
+
+initSimpleApp('#js-mount-new-ml-model', NewMlModel, { withApolloProvider: true });
diff --git a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
index dce40c1f322..ad6f84fbc07 100644
--- a/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/ci_cd/show/index.js
@@ -1,11 +1,9 @@
import initArtifactsSettings from '~/artifacts_settings';
-import SecretValues from '~/behaviors/secret_values';
import initSettingsPipelinesTriggers from '~/ci_settings_pipeline_triggers';
import initVariableList from '~/ci/ci_variable_list';
import initInheritedGroupCiVariables from '~/ci/inherited_ci_variables';
import initDeployFreeze from '~/deploy_freeze';
import registrySettingsApp from '~/packages_and_registries/settings/project/registry_settings_bundle';
-import { initInstallRunner } from '~/pages/shared/mount_runner_instructions';
import initSharedRunnersToggle from '~/projects/settings/mount_shared_runners_toggle';
import initRefSwitcherBadges from '~/projects/settings/mount_ref_switcher_badges';
import initSettingsPanels from '~/settings_panels';
@@ -18,14 +16,6 @@ import { initGeneralPipelinesOptions } from '~/ci_settings_general_pipeline';
// Initialize expandable settings panels
initSettingsPanels();
-const runnerToken = document.querySelector('.js-secret-runner-token');
-if (runnerToken) {
- const runnerTokenSecretValue = new SecretValues({
- container: runnerToken,
- });
- runnerTokenSecretValue.init();
-}
-
initVariableList();
initInheritedGroupCiVariables();
@@ -47,7 +37,6 @@ initArtifactsSettings();
initProjectRunnersRegistrationDropdown();
initSharedRunnersToggle();
initRefSwitcherBadges();
-initInstallRunner();
initTokenAccess();
initCiSecureFiles();
initGeneralPipelinesOptions();
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 32775ac553c..c35e10462a0 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
@@ -5,9 +5,9 @@ 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';
+import getCiCatalogSettingsQuery from '~/ci/catalog/graphql/queries/get_ci_catalog_settings.query.graphql';
+import catalogResourcesCreate from '~/ci/catalog/graphql/mutations/catalog_resources_create.mutation.graphql';
+import catalogResourcesDestroy from '~/ci/catalog/graphql/mutations/catalog_resources_destroy.mutation.graphql';
const i18n = {
catalogResourceQueryError: s__(
diff --git a/app/assets/javascripts/pages/projects/shared/permissions/index.js b/app/assets/javascripts/pages/projects/shared/permissions/index.js
index 4b4589dc46c..78dd456aad9 100644
--- a/app/assets/javascripts/pages/projects/shared/permissions/index.js
+++ b/app/assets/javascripts/pages/projects/shared/permissions/index.js
@@ -14,6 +14,9 @@ export default function initProjectPermissionsSettings() {
const mountPoint = document.querySelector('.js-project-permissions-form');
const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
+
+ if (!mountPoint) return null;
+
const componentProps = JSON.parse(componentPropsEl.innerHTML);
const {
diff --git a/app/assets/javascripts/pages/sessions/new/index.js b/app/assets/javascripts/pages/sessions/new/index.js
index 32df2911a48..ee1a7633a11 100644
--- a/app/assets/javascripts/pages/sessions/new/index.js
+++ b/app/assets/javascripts/pages/sessions/new/index.js
@@ -16,7 +16,7 @@ new SigninTabsMemoizer(); // eslint-disable-line no-new
new NoEmojiValidator(); // eslint-disable-line no-new
new OAuthRememberMe({
- container: $('.omniauth-container'),
+ container: $('.js-oauth-login'),
}).bindEvents();
// Save the URL fragment from the current window location. This will be present if the user was
diff --git a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
index bad8a7cedc6..3336b094560 100644
--- a/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
+++ b/app/assets/javascripts/pages/sessions/new/oauth_remember_me.js
@@ -20,8 +20,8 @@ export default class OAuthRememberMe {
toggleRememberMe(event) {
const rememberMe = $(event.target).is(':checked');
- $('.js-oauth-login', this.container).each((i, element) => {
- const $form = $(element).parent('form');
+ $('.js-oauth-login form', this.container).each((_, form) => {
+ const $form = $(form);
const href = $form.attr('action');
if (rememberMe) {
diff --git a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js
index 70e5e336e78..54ec3c52f62 100644
--- a/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js
+++ b/app/assets/javascripts/pages/sessions/new/preserve_url_fragment.js
@@ -12,7 +12,7 @@ export default function preserveUrlFragment(fragment = '') {
// Append the fragment to all sign-in/sign-up form actions so it is preserved when the user is
// eventually redirected back to the originally requested URL.
- const forms = document.querySelectorAll('#signin-container .tab-content form');
+ const forms = document.querySelectorAll('.js-non-oauth-login form');
Array.prototype.forEach.call(forms, (form) => {
const actionWithFragment = setUrlFragment(form.getAttribute('action'), `#${normalFragment}`);
form.setAttribute('action', actionWithFragment);
@@ -20,7 +20,7 @@ export default function preserveUrlFragment(fragment = '') {
// Append a redirect_fragment query param to all oauth provider links. The redirect_fragment
// query param will be available in the omniauth callback upon successful authentication
- const oauthForms = document.querySelectorAll('#signin-container .omniauth-container form');
+ const oauthForms = document.querySelectorAll('.js-oauth-login form');
Array.prototype.forEach.call(oauthForms, (oauthForm) => {
const newHref = mergeUrlParams(
{ redirect_fragment: normalFragment },
diff --git a/app/assets/javascripts/pages/shared/mount_badge_settings.js b/app/assets/javascripts/pages/shared/mount_badge_settings.js
index aeb9f2fb8d3..9c92d4f8f1e 100644
--- a/app/assets/javascripts/pages/shared/mount_badge_settings.js
+++ b/app/assets/javascripts/pages/shared/mount_badge_settings.js
@@ -5,6 +5,8 @@ import store from '~/badges/store';
export default (kind) => {
const badgeSettingsElement = document.getElementById('badge-settings');
+ if (!badgeSettingsElement) return null;
+
store.dispatch('loadBadges', {
kind,
apiEndpointUrl: badgeSettingsElement.dataset.apiEndpointUrl,
diff --git a/app/assets/javascripts/pages/shared/mount_runner_instructions.js b/app/assets/javascripts/pages/shared/mount_runner_instructions.js
deleted file mode 100644
index e83c73edfde..00000000000
--- a/app/assets/javascripts/pages/shared/mount_runner_instructions.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createDefaultClient from '~/lib/graphql';
-import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
-
-Vue.use(VueApollo);
-
-export function initInstallRunner(componentId = 'js-install-runner') {
- const installRunnerEl = document.getElementById(componentId);
-
- if (installRunnerEl) {
- const defaultClient = createDefaultClient();
-
- const apolloProvider = new VueApollo({
- defaultClient,
- });
-
- // eslint-disable-next-line no-new
- new Vue({
- el: installRunnerEl,
- apolloProvider,
- render(createElement) {
- return createElement(RunnerInstructions);
- },
- });
- }
-}
diff --git a/app/assets/javascripts/pages/users/index.js b/app/assets/javascripts/pages/users/index.js
index d2c31314bba..134962721d2 100644
--- a/app/assets/javascripts/pages/users/index.js
+++ b/app/assets/javascripts/pages/users/index.js
@@ -8,7 +8,7 @@ import UserTabs from './user_tabs';
function initUserProfile(action) {
// TODO: Remove both Vue and legacy JS tabs code/feature flag uses with the
// removal of the old navigation.
- // See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+ // See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
if (gon.features?.profileTabsVue) {
initProfileTabs();
diff --git a/app/assets/javascripts/pages/users/user_tabs.js b/app/assets/javascripts/pages/users/user_tabs.js
index 79eb3902116..f9e22808b0d 100644
--- a/app/assets/javascripts/pages/users/user_tabs.js
+++ b/app/assets/javascripts/pages/users/user_tabs.js
@@ -1,5 +1,5 @@
// TODO: Remove this with the removal of the old navigation.
-// See https://gitlab.com/groups/gitlab-org/-/epics/11875.
+// See https://gitlab.com/gitlab-org/gitlab/-/issues/435899.
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import $ from 'jquery';
diff --git a/app/assets/javascripts/performance_bar/components/request_warning.vue b/app/assets/javascripts/performance_bar/components/request_warning.vue
index 96c11ea9e4e..c4e27d89f49 100644
--- a/app/assets/javascripts/performance_bar/components/request_warning.vue
+++ b/app/assets/javascripts/performance_bar/components/request_warning.vue
@@ -37,6 +37,7 @@ export default {
:id="htmlId"
v-gl-tooltip.viewport="warningMessage"
data-name="warning"
+ data-testid="warning"
class="gl-ml-2"
/>
</span>
diff --git a/app/assets/javascripts/persistent_user_callouts.js b/app/assets/javascripts/persistent_user_callouts.js
index 7420542a065..95ef04ceb30 100644
--- a/app/assets/javascripts/persistent_user_callouts.js
+++ b/app/assets/javascripts/persistent_user_callouts.js
@@ -24,7 +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',
+ '.js-code-suggestions-ga-alert',
];
const initCallouts = () => {
diff --git a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
index 2fc1f99c183..8d20ef1989e 100644
--- a/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
+++ b/app/assets/javascripts/profile/preferences/components/profile_preferences.vue
@@ -1,6 +1,6 @@
<script>
import { GlButton } from '@gitlab/ui';
-import { createAlert, VARIANT_DANGER, VARIANT_INFO } from '~/alert';
+import { createAlert, VARIANT_DANGER } from '~/alert';
import { INTEGRATION_VIEW_CONFIGS, i18n } from '../constants';
import IntegrationView from './integration_view.vue';
@@ -94,9 +94,8 @@ export default {
return;
}
updateClasses(this.bodyClasses, this.getSelectedTheme().css_class, this.selectedLayout);
- const { message = this.$options.i18n.defaultSuccess, variant = VARIANT_INFO } =
- customEvent?.detail?.[0] || {};
- createAlert({ message, variant });
+ const message = customEvent?.detail?.[0]?.message || this.$options.i18n.defaultSuccess || '';
+ this.$toast.show(message);
this.isSubmitEnabled = true;
},
handleError(customEvent) {
diff --git a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
index 8e4d42a42c6..3ba34078cd7 100644
--- a/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
+++ b/app/assets/javascripts/profile/preferences/profile_preferences_bundle.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { GlToast } from '@gitlab/ui';
import { initListboxInputs } from '~/vue_shared/components/listbox_input/init_listbox_inputs';
import ProfilePreferences from './components/profile_preferences.vue';
@@ -21,6 +22,8 @@ export default () => {
{ formEl },
);
+ Vue.use(GlToast);
+
return new Vue({
el,
name: 'ProfilePreferencesApp',
diff --git a/app/assets/javascripts/projects/commit/components/commit_comments_button.vue b/app/assets/javascripts/projects/commit/components/commit_comments_button.vue
deleted file mode 100644
index 67b5e1e512c..00000000000
--- a/app/assets/javascripts/projects/commit/components/commit_comments_button.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
-import { GlButton, GlTooltipDirective } from '@gitlab/ui';
-import { n__ } from '~/locale';
-
-export default {
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: {
- GlButton,
- },
- props: {
- commentsCount: {
- type: Number,
- required: true,
- },
- },
- computed: {
- tooltipText() {
- return n__('%d comment on this commit', '%d comments on this commit', this.commentsCount);
- },
- showCommentButton() {
- return this.commentsCount > 0;
- },
- },
-};
-</script>
-
-<template>
- <span
- v-if="showCommentButton"
- v-gl-tooltip
- class="gl-display-none gl-sm-display-inline-block"
- tabindex="0"
- :title="tooltipText"
- data-testid="comment-button-wrapper"
- >
- <gl-button icon="comment" class="gl-mr-3" disabled>
- {{ commentsCount }}
- </gl-button>
- </span>
-</template>
diff --git a/app/assets/javascripts/projects/commit/index.js b/app/assets/javascripts/projects/commit/index.js
index d8d30c4332c..4eb51d566c5 100644
--- a/app/assets/javascripts/projects/commit/index.js
+++ b/app/assets/javascripts/projects/commit/index.js
@@ -1,11 +1,9 @@
import initCherryPickCommitModal from './init_cherry_pick_commit_modal';
-import initCommitCommentsButton from './init_commit_comments_button';
import initCommitOptionsDropdown from './init_commit_options_dropdown';
import initRevertCommitModal from './init_revert_commit_modal';
export default () => {
initRevertCommitModal();
initCherryPickCommitModal();
- initCommitCommentsButton();
initCommitOptionsDropdown();
};
diff --git a/app/assets/javascripts/projects/commit/init_commit_comments_button.js b/app/assets/javascripts/projects/commit/init_commit_comments_button.js
deleted file mode 100644
index d70f7cb65f3..00000000000
--- a/app/assets/javascripts/projects/commit/init_commit_comments_button.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import Vue from 'vue';
-import CommitCommentsButton from './components/commit_comments_button.vue';
-
-export default function initCommitCommentsButton() {
- const el = document.querySelector('#js-commit-comments-button');
-
- if (!el) {
- return false;
- }
-
- const { commentsCount } = el.dataset;
-
- return new Vue({
- el,
- render: (createElement) =>
- createElement(CommitCommentsButton, { props: { commentsCount: Number(commentsCount) } }),
- });
-}
diff --git a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
index 25af4cc8082..684ae5343b0 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/commit_refs.vue
@@ -107,7 +107,7 @@ export default {
</script>
<template>
- <div class="gl-ml-7">
+ <div>
<refs-list
v-if="hasBranches"
:has-containing-refs="hasContainingBranches"
diff --git a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
index 8ceab9cb60b..7a926c3f4e6 100644
--- a/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
+++ b/app/assets/javascripts/projects/commit_box/info/components/refs_list.vue
@@ -1,6 +1,6 @@
<script>
import { GlCollapse, GlBadge, GlButton, GlIcon, GlSkeletonLoader } from '@gitlab/ui';
-import { CONTAINING_COMMIT, FETCH_CONTAINING_REFS_EVENT } from '../constants';
+import { CONTAINING_COMMIT, FETCH_CONTAINING_REFS_EVENT, BRANCHES_REF_TYPE } from '../constants';
export default {
name: 'RefsList',
@@ -55,6 +55,9 @@ export default {
isLoadingRefs() {
return this.isLoading && !this.containingRefs.length;
},
+ refIcon() {
+ return this.refType === BRANCHES_REF_TYPE ? 'branch' : 'tag';
+ },
},
methods: {
toggleCollapse() {
@@ -75,7 +78,8 @@ export default {
</script>
<template>
- <div class="gl-pt-4">
+ <div class="gl-p-5 gl-border-b gl-border-gray-50">
+ <gl-icon :name="refIcon" :size="14" class="gl-ml-2 gl-mr-3" />
<span data-testid="title" class="gl-mr-2">{{ namespace }}</span>
<gl-badge
v-for="ref in tippingRefs"
diff --git a/app/assets/javascripts/projects/new/components/new_project_url_select.vue b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
index 84a2ddfce07..c9a502bb6d2 100644
--- a/app/assets/javascripts/projects/new/components/new_project_url_select.vue
+++ b/app/assets/javascripts/projects/new/components/new_project_url_select.vue
@@ -1,20 +1,11 @@
<script>
-import {
- GlButton,
- GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlButton, GlButtonGroup, GlTruncate, GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
import { joinPaths, PATH_SEPARATOR } from '~/lib/utils/url_utility';
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import Tracking from '~/tracking';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import { s__ } from '~/locale';
+import { __, s__, n__ } from '~/locale';
import searchNamespacesWhereUserCanCreateProjectsQuery from '../queries/search_namespaces_where_user_can_create_projects.query.graphql';
import eventHub from '../event_hub';
@@ -22,12 +13,9 @@ export default {
components: {
GlButton,
GlButtonGroup,
- GlDropdown,
- GlDropdownItem,
- GlDropdownText,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
GlTruncate,
+ GlCollapsibleListbox,
+ GlIcon,
},
mixins: [Tracking.mixin()],
apollo: {
@@ -91,12 +79,61 @@ export default {
!this.groupPathToFilterBy
);
},
- hasNoMatches() {
- return !this.hasGroupMatches && !this.hasNamespaceMatches;
+ items() {
+ return this.groupsItems.concat(this.namespaceItems);
+ },
+ groupsItems() {
+ if (this.hasGroupMatches) {
+ return [
+ {
+ text: __('Groups'),
+ options: this.filteredGroups.map((group) => ({
+ value: group.id,
+ text: group.fullPath,
+ })),
+ },
+ ];
+ }
+
+ return [];
+ },
+ allItems() {
+ return this.filteredGroups.concat(this.currentUser.namespace);
+ },
+ namespaceItems() {
+ if (this.hasNamespaceMatches && this.userNamespaceUniqueId)
+ return [
+ {
+ text: __('Users'),
+ options: [
+ {
+ value: this.userNamespace.id,
+ text: this.userNamespace.fullPath,
+ },
+ ],
+ },
+ ];
+ return [];
},
dropdownPlaceholderClass() {
return this.selectedNamespace.id ? '' : 'gl-text-gray-500!';
},
+ dropdownText() {
+ if (this.selectedNamespace && this.selectedNamespace?.fullPath) {
+ return this.selectedNamespace.fullPath;
+ }
+ return null;
+ },
+ loading() {
+ return this.$apollo.queries.currentUser.loading;
+ },
+ searchSummary() {
+ return n__(
+ 'ProjectsNew|%d group or namespace found',
+ 'ProjectsNew|%d groups or namespaces found',
+ this.items.length,
+ );
+ },
},
created() {
eventHub.$on('select-template', this.handleSelectTemplate);
@@ -109,15 +146,18 @@ export default {
if (this.shouldSkipQuery) {
this.shouldSkipQuery = false;
}
- this.$refs.search.focusInput();
- },
- handleDropdownItemClick(namespace) {
- eventHub.$emit('update-visibility', {
- name: namespace.name,
- visibility: namespace.visibility,
- showPath: namespace.webUrl,
- editPath: joinPaths(namespace.webUrl, '-', 'edit'),
- });
+ },
+ handleDropdownItemClick(namespaceId) {
+ const namespace = this.allItems.find((item) => item.id === namespaceId);
+
+ if (namespace) {
+ eventHub.$emit('update-visibility', {
+ name: namespace.name,
+ visibility: namespace.visibility,
+ showPath: namespace.webUrl,
+ editPath: joinPaths(namespace.webUrl, '-', 'edit'),
+ });
+ }
this.setNamespace(namespace);
},
handleSelectTemplate(id, fullPath) {
@@ -137,6 +177,12 @@ export default {
this.track('activate_form_input', { label: this.trackLabel, property: 'project_path' });
}
},
+ onSearch(query) {
+ this.search = query;
+ },
+ },
+ i18n: {
+ emptySearchResult: __('No matches found'),
},
emptyNameSpace: {
id: undefined,
@@ -154,48 +200,38 @@ export default {
>{{ rootUrl }}</gl-button
>
- <gl-dropdown
- class="js-group-namespace-dropdown gl-flex-grow-1"
- :toggle-class="`gl-rounded-top-right-base! gl-rounded-bottom-right-base! gl-w-20 ${dropdownPlaceholderClass}`"
+ <gl-collapsible-listbox
+ searchable
+ fluid-width
+ :searching="loading"
+ :items="items"
+ class="js-group-namespace-dropdown group-namespace-dropdown gl-flex-grow-1"
+ :toggle-text="dropdownText"
+ :no-results-text="$options.i18n.emptySearchResult"
data-testid="select-namespace-dropdown"
@show="trackDropdownShow"
@shown="handleDropdownShown"
+ @select="handleDropdownItemClick"
+ @search="onSearch"
>
- <template #button-text>
- <gl-truncate
- v-if="selectedNamespace.fullPath"
- :text="selectedNamespace.fullPath"
- position="start"
- with-tooltip
- />
+ <template #toggle>
+ <gl-button
+ class="gl-flex-basis-full! gl-rounded-left-none! gl-w-20"
+ :class="dropdownPlaceholderClass"
+ >
+ <gl-truncate
+ :text="dropdownText"
+ position="start"
+ class="gl-overflow-hidden gl-mr-auto"
+ with-tooltip
+ />
+ <gl-icon class="gl-button-icon dropdown-chevron gl-mr-0! gl-ml-2!" name="chevron-down" />
+ </gl-button>
</template>
- <gl-search-box-by-type
- ref="search"
- v-model.trim="search"
- :is-loading="$apollo.queries.currentUser.loading"
- data-testid="select-namespace-dropdown-search-field"
- />
- <template v-if="!$apollo.queries.currentUser.loading">
- <template v-if="hasGroupMatches">
- <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="group of filteredGroups"
- :key="group.id"
- @click="handleDropdownItemClick(group)"
- >
- {{ group.fullPath }}
- </gl-dropdown-item>
- </template>
- <template v-if="hasNamespaceMatches && userNamespaceUniqueId">
- <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="handleDropdownItemClick(userNamespace)">
- {{ userNamespace.fullPath }}
- </gl-dropdown-item>
- </template>
- <gl-dropdown-text v-if="hasNoMatches">{{ __('No matches found') }}</gl-dropdown-text>
+ <template #search-summary-sr-only>
+ {{ searchSummary }}
</template>
- </gl-dropdown>
-
+ </gl-collapsible-listbox>
<input type="hidden" name="project[selected_namespace_id]" :value="selectedNamespace.id" />
<input
diff --git a/app/assets/javascripts/projects/project_new.js b/app/assets/javascripts/projects/project_new.js
index 2b5e2dcb301..9e71e662d70 100644
--- a/app/assets/javascripts/projects/project_new.js
+++ b/app/assets/javascripts/projects/project_new.js
@@ -70,7 +70,8 @@ const onProjectPathChange = ($projectNameInput, $projectPathInput, hasExistingPr
};
const selectedNamespaceId = () => document.querySelector('[name="project[selected_namespace_id]"]');
-const dropdownButton = () => document.querySelector('.js-group-namespace-dropdown > button');
+const dropdownButton = () =>
+ document.querySelector('.js-group-namespace-dropdown .gl-new-dropdown-custom-toggle > button');
const namespaceButton = () => document.querySelector('.js-group-namespace-button');
const namespaceError = () => document.querySelector('.js-group-namespace-error');
diff --git a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
index df99aac6b9e..89ae2a82c6d 100644
--- a/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
+++ b/app/assets/javascripts/projects/settings/api/access_dropdown_api.js
@@ -25,10 +25,11 @@ export const getUsers = (query, states) => {
});
};
-export const getGroups = () => {
+export const getGroups = ({ withProjectAccess = false }) => {
return axios.get(buildUrl(gon.relative_url_root || '', GROUPS_PATH), {
params: {
project_id: gon.current_project_id,
+ with_project_access: withProjectAccess,
},
});
};
diff --git a/app/assets/javascripts/projects/settings/components/access_dropdown.vue b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
index 2dd7633e2c8..c863973cd2e 100644
--- a/app/assets/javascripts/projects/settings/components/access_dropdown.vue
+++ b/app/assets/javascripts/projects/settings/components/access_dropdown.vue
@@ -94,6 +94,11 @@ export default {
required: false,
default: true,
},
+ groupsWithProjectAccess: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -229,7 +234,9 @@ export default {
Promise.all([
getDeployKeys(this.query),
getUsers(this.query),
- this.groups.length ? Promise.resolve({ data: this.groups }) : getGroups(),
+ this.groups.length
+ ? Promise.resolve({ data: this.groups })
+ : getGroups({ withProjectAccess: this.groupsWithProjectAccess }),
])
.then(([deployKeysResponse, usersResponse, groupsResponse]) => {
this.consolidateData(deployKeysResponse.data, usersResponse.data, groupsResponse.data);
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
index 7d9ad83a1c6..cb6bea76762 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/app.vue
@@ -1,9 +1,20 @@
<script>
-import { GlButton, GlModal, GlModalDirective, GlCard, GlIcon } from '@gitlab/ui';
+import {
+ GlButton,
+ GlModal,
+ GlModalDirective,
+ GlCard,
+ GlIcon,
+ GlDisclosureDropdown,
+ GlCollapsibleListbox,
+ GlFormGroup,
+} from '@gitlab/ui';
import { createAlert } from '~/alert';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { expandSection } from '~/settings_panels';
import { scrollToElement } from '~/lib/utils/common_utils';
+import createBranchRuleMutation from './graphql/mutations/create_branch_rule.mutation.graphql';
import BranchRule from './components/branch_rule.vue';
import { I18N, PROTECTED_BRANCHES_ANCHOR, BRANCH_PROTECTION_MODAL_ID } from './constants';
@@ -14,12 +25,16 @@ export default {
BranchRule,
GlButton,
GlModal,
+ GlFormGroup,
GlCard,
GlIcon,
+ GlCollapsibleListbox,
+ GlDisclosureDropdown,
},
directives: {
GlModal: GlModalDirective,
},
+ mixins: [glFeatureFlagsMixin()],
apollo: {
branchRules: {
query: branchRulesQuery,
@@ -38,18 +53,87 @@ export default {
},
inject: {
projectPath: { default: '' },
+ branchRulesPath: { default: '' },
},
data() {
return {
branchRules: [],
+ branchRuleName: '',
+ searchQuery: '',
};
},
+ computed: {
+ addRuleItems() {
+ return [{ text: this.$options.i18n.branchName, action: () => this.openCreateRuleModal() }];
+ },
+ createRuleItems() {
+ return this.isWildcardAvailable ? [this.wildcardItem] : this.filteredOpenBranches;
+ },
+ filteredOpenBranches() {
+ const openBranches = window.gon.open_branches.map((item) => ({
+ text: item.text,
+ value: item.text,
+ }));
+ return openBranches.filter((item) => item.text.includes(this.searchQuery));
+ },
+ wildcardItem() {
+ return { text: this.$options.i18n.createWildcard, value: this.searchQuery };
+ },
+ isWildcardAvailable() {
+ return this.searchQuery.includes('*');
+ },
+ createRuleText() {
+ return this.branchRuleName || this.$options.i18n.branchNamePlaceholder;
+ },
+ branchRuleEditPath() {
+ return `${this.branchRulesPath}?branch=${encodeURIComponent(this.branchRuleName)}`;
+ },
+ primaryProps() {
+ return {
+ text: this.$options.i18n.createProtectedBranch,
+ attributes: {
+ variant: 'confirm',
+ disabled: !this.branchRuleName,
+ },
+ };
+ },
+ cancelProps() {
+ return {
+ text: this.$options.i18n.createBranchRule,
+ };
+ },
+ },
methods: {
showProtectedBranches() {
// Protected branches section is on the same page as the branch rules section.
expandSection(this.$options.protectedBranchesAnchor);
scrollToElement(this.$options.protectedBranchesAnchor);
},
+ openCreateRuleModal() {
+ this.$refs[this.$options.modalId].show();
+ },
+ handleBranchRuleSearch(query) {
+ this.searchQuery = query;
+ },
+ addBranchRule() {
+ this.$apollo
+ .mutate({
+ mutation: createBranchRuleMutation,
+ variables: {
+ projectPath: this.projectPath,
+ name: this.branchRuleName,
+ },
+ })
+ .then(() => {
+ window.location.assign(this.branchRuleEditPath);
+ })
+ .catch(() => {
+ createAlert({ message: this.$options.i18n.createBranchRuleError });
+ });
+ },
+ selectBranchRuleName(branchName) {
+ this.branchRuleName = branchName;
+ },
},
modalId: BRANCH_PROTECTION_MODAL_ID,
protectedBranchesAnchor: PROTECTED_BRANCHES_ANCHOR,
@@ -72,7 +156,15 @@ export default {
{{ branchRules.length }}
</div>
</div>
+ <gl-disclosure-dropdown
+ v-if="glFeatures.addBranchRule"
+ :toggle-text="$options.i18n.addBranchRule"
+ :items="addRuleItems"
+ size="small"
+ no-caret
+ />
<gl-button
+ v-else
v-gl-modal="$options.modalId"
size="small"
class="gl-ml-3"
@@ -99,6 +191,37 @@ export default {
</div>
</ul>
<gl-modal
+ v-if="glFeatures.addBranchRule"
+ :ref="$options.modalId"
+ :modal-id="$options.modalId"
+ :title="$options.i18n.createBranchRule"
+ :action-primary="primaryProps"
+ :action-cancel="cancelProps"
+ @primary="addBranchRule"
+ @change="searchQuery = ''"
+ >
+ <gl-form-group
+ :label="$options.i18n.branchName"
+ :description="$options.i18n.branchNameDescription"
+ >
+ <gl-collapsible-listbox
+ v-model="branchRuleName"
+ searchable
+ :items="createRuleItems"
+ :toggle-text="createRuleText"
+ block
+ @search="handleBranchRuleSearch"
+ @select="selectBranchRuleName"
+ >
+ <template v-if="isWildcardAvailable" #list-item="{ item }">
+ {{ item.text }}
+ <code>{{ searchQuery }}</code>
+ </template>
+ </gl-collapsible-listbox>
+ </gl-form-group>
+ </gl-modal>
+ <gl-modal
+ v-else
:ref="$options.modalId"
:modal-id="$options.modalId"
:title="$options.i18n.addBranchRule"
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js
index 4413d8eab4e..d16894e7d6f 100644
--- a/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/constants.js
@@ -15,6 +15,16 @@ export const I18N = {
'BranchRules|After a protected branch is created, it will show up in the list as a branch rule.',
),
createProtectedBranch: s__('BranchRules|Create protected branch'),
+ createBranchRule: s__('BranchRules|Create branch rule'),
+ branchName: s__('BranchRules|Branch name or pattern'),
+ branchNamePlaceholder: s__('BranchRules|Select Branch or create wildcard'),
+ branchNameDescription: s__(
+ 'BranchRules|Wildcards such as *-stable or production/* are supported',
+ ),
+ createBranchRuleError: s__('BranchRules|Something went wrong while creating branch rule.'),
+ createBranchRuleSuccess: s__('BranchRules|Branch rule created.'),
+ createWildcard: s__('BranchRules|Create wildcard'),
+ cancel: s__('BranchRules|Cancel'),
};
export const PROTECTED_BRANCHES_ANCHOR = '#js-protected-branches-settings';
diff --git a/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql
new file mode 100644
index 00000000000..e5fb79e0176
--- /dev/null
+++ b/app/assets/javascripts/projects/settings/repository/branch_rules/graphql/mutations/create_branch_rule.mutation.graphql
@@ -0,0 +1,8 @@
+mutation createBranchRule($projectPath: ID!, $name: String!) {
+ branchRuleCreate(input: { projectPath: $projectPath, name: $name }) {
+ errors
+ branchRule {
+ name
+ }
+ }
+}
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 49c55efca7b..612f801300e 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -83,6 +83,7 @@ export default class ProtectedBranchCreate {
block: true,
accessLevel,
accessLevelsData,
+ groupsWithProjectAccess: true,
testId,
});
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 66da3de516a..67ae33e1fc8 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -97,6 +97,7 @@ export default class ProtectedBranchEdit {
block: true,
accessLevel,
accessLevelsData,
+ groupsWithProjectAccess: true,
testId,
});
diff --git a/app/assets/javascripts/protected_tags/protected_tag_create.js b/app/assets/javascripts/protected_tags/protected_tag_create.js
index b3754cecce4..e1cd069fd92 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_create.js
+++ b/app/assets/javascripts/protected_tags/protected_tag_create.js
@@ -40,6 +40,7 @@ export default class ProtectedTagCreate {
hasLicense: this.hasLicense,
accessLevel: ACCESS_LEVELS.CREATE,
accessLevelsData: gon.create_access_levels,
+ groupsWithProjectAccess: true,
searchEnabled: dropdownEl.dataset.filter !== undefined,
testId: 'allowed-to-create-dropdown',
});
diff --git a/app/assets/javascripts/protected_tags/protected_tag_edit.vue b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
index 7fe1dc9c01a..d5ec88c171c 100644
--- a/app/assets/javascripts/protected_tags/protected_tag_edit.vue
+++ b/app/assets/javascripts/protected_tags/protected_tag_edit.vue
@@ -107,6 +107,7 @@ export default {
:access-levels-data="accessLevelsData"
:preselected-items="selected"
:search-enabled="searchEnabled"
+ groups-with-project-access
:block="true"
@hidden="updatePermissions"
/>
diff --git a/app/assets/javascripts/releases/components/app_edit_new.vue b/app/assets/javascripts/releases/components/app_edit_new.vue
index 228007dd7d6..6fce9b4a129 100644
--- a/app/assets/javascripts/releases/components/app_edit_new.vue
+++ b/app/assets/javascripts/releases/components/app_edit_new.vue
@@ -55,6 +55,7 @@ export default {
'groupId',
'groupMilestonesAvailable',
'tagNotes',
+ 'isFetchingTagNotes',
]),
...mapGetters('editNew', ['isValid', 'formattedReleaseNotes']),
showForm() {
@@ -113,7 +114,7 @@ export default {
return this.isExistingRelease ? __('Save changes') : __('Create release');
},
isFormSubmissionDisabled() {
- return this.isUpdatingRelease || !this.isValid;
+ return this.isUpdatingRelease || !this.isValid || this.isFetchingTagNotes;
},
milestoneComboboxExtraLinks() {
return [
diff --git a/app/assets/javascripts/releases/components/app_index.vue b/app/assets/javascripts/releases/components/app_index.vue
index eebaeeea286..31c7ed8482d 100644
--- a/app/assets/javascripts/releases/components/app_index.vue
+++ b/app/assets/javascripts/releases/components/app_index.vue
@@ -1,13 +1,15 @@
<script>
-import { GlButton } from '@gitlab/ui';
+import { GlAlert, GlButton, GlLink, GlSprintf } from '@gitlab/ui';
import { createAlert } from '~/alert';
import { historyPushState } from '~/lib/utils/common_utils';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { setUrlParams, getParameterByName } from '~/lib/utils/url_utility';
-import { __ } from '~/locale';
+import { __, s__ } from '~/locale';
import { PAGE_SIZE, DEFAULT_SORT } from '~/releases/constants';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
import { popDeleteReleaseNotification } from '~/releases/release_notification_service';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import getCiCatalogSettingsQuery from '~/ci/catalog/graphql/queries/get_ci_catalog_settings.query.graphql';
import allReleasesQuery from '../graphql/queries/all_releases.query.graphql';
import ReleaseBlock from './release_block.vue';
import ReleaseSkeletonLoader from './release_skeleton_loader.vue';
@@ -18,7 +20,10 @@ import ReleasesSort from './releases_sort.vue';
export default {
name: 'ReleasesIndexApp',
components: {
+ GlAlert,
GlButton,
+ GlLink,
+ GlSprintf,
ReleaseBlock,
ReleaseSkeletonLoader,
ReleasesEmptyState,
@@ -79,6 +84,20 @@ export default {
});
},
},
+ isCatalogResource: {
+ query: getCiCatalogSettingsQuery,
+ variables() {
+ return {
+ fullPath: this.projectPath,
+ };
+ },
+ update({ project }) {
+ return project?.isCatalogResource || false;
+ },
+ error() {
+ createAlert({ message: this.$options.i18n.catalogResourceQueryError });
+ },
+ },
},
data() {
return {
@@ -227,13 +246,44 @@ export default {
},
},
i18n: {
- newRelease: __('New release'),
+ alertButtonLink: helpPagePath('ci/components/index', { anchor: 'release-a-component' }),
+ alertButtonText: __('View the publishing guide'),
+ alertInfoMessage: s__(
+ 'CiCatalog|The CI/CD components in this project can be published in the CI/CD Catalog by creating a release. We recommend using the %{linkStart}release%{linkEnd} keyword in a CI/CD job to release new component versions for the Catalog.',
+ ),
+ alertInfoMessageLink: helpPagePath('ci/yaml/index.html', { anchor: 'release' }),
+ alertTitle: __('Publish the CI/CD components in this project to the CI/CD Catalog'),
+ catalogResourceQueryError: s__(
+ 'CiCatalog|There was a problem fetching the CI/CD Catalog setting.',
+ ),
errorMessage: __('An error occurred while fetching the releases. Please try again.'),
+ newRelease: __('New release'),
},
};
</script>
<template>
<div class="gl-display-flex gl-flex-direction-column gl-mt-3">
+ <gl-alert
+ v-if="isCatalogResource"
+ :title="$options.i18n.alertTitle"
+ :primary-button-text="$options.i18n.alertButtonText"
+ :primary-button-link="$options.i18n.alertButtonLink"
+ :dismissible="false"
+ variant="warning"
+ class="mb-3 mt-2"
+ >
+ <gl-sprintf :message="$options.i18n.alertInfoMessage">
+ <template #link="{ content }">
+ <gl-link
+ :href="$options.i18n.alertInfoMessageLink"
+ target="_blank"
+ class="gl-text-decoration-none!"
+ >
+ {{ content }}
+ </gl-link>
+ </template>
+ </gl-sprintf>
+ </gl-alert>
<releases-empty-state v-if="shouldRenderEmptyState" />
<div v-else class="gl-align-self-end gl-mb-3">
<releases-sort :value="sort" class="gl-mr-2" @input="onSortChanged" />
diff --git a/app/assets/javascripts/releases/components/tag_field_new.vue b/app/assets/javascripts/releases/components/tag_field_new.vue
index 04f3d73235b..370e920be02 100644
--- a/app/assets/javascripts/releases/components/tag_field_new.vue
+++ b/app/assets/javascripts/releases/components/tag_field_new.vue
@@ -43,6 +43,9 @@ export default {
return this.newTagName ? this.$options.i18n.createTag : this.$options.i18n.typeNew;
},
},
+ mounted() {
+ this.newTagName = this.release?.tagName || '';
+ },
methods: {
...mapActions('editNew', [
'setSearching',
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
index 8bdfb057adc..a0d782a02a1 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/actions.js
@@ -3,6 +3,7 @@ import { getTag } from '~/rest_api';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import AccessorUtilities from '~/lib/utils/accessor';
+import { HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { s__ } from '~/locale';
import createReleaseMutation from '~/releases/graphql/mutations/create_release.mutation.graphql';
import deleteReleaseMutation from '~/releases/graphql/mutations/delete_release.mutation.graphql';
@@ -245,7 +246,7 @@ export const updateRelease = async ({ commit, dispatch, state, getters }) => {
}
};
-export const fetchTagNotes = ({ commit, state }, tagName) => {
+export const fetchTagNotes = ({ commit, state, dispatch }, tagName) => {
commit(types.REQUEST_TAG_NOTES);
return getTag(state.projectId, tagName)
@@ -253,11 +254,15 @@ export const fetchTagNotes = ({ commit, state }, tagName) => {
commit(types.RECEIVE_TAG_NOTES_SUCCESS, data);
})
.catch((error) => {
+ if (error?.response?.status === HTTP_STATUS_NOT_FOUND) {
+ commit(types.RECEIVE_TAG_NOTES_SUCCESS, {});
+ return Promise.all([dispatch('setNewTag'), dispatch('setCreating')]);
+ }
createAlert({
message: s__('Release|Unable to fetch the tag notes.'),
});
- commit(types.RECEIVE_TAG_NOTES_ERROR, error);
+ return commit(types.RECEIVE_TAG_NOTES_ERROR, error);
});
};
@@ -326,7 +331,7 @@ export const clearDraftRelease = ({ getters }) => {
}
};
-export const loadDraftRelease = ({ commit, getters, state }) => {
+export const loadDraftRelease = ({ commit, getters, state, dispatch }) => {
try {
const release = window.localStorage.getItem(getters.localStorageKey);
const createFrom = window.localStorage.getItem(getters.localStorageCreateFromKey);
@@ -340,6 +345,10 @@ export const loadDraftRelease = ({ commit, getters, state }) => {
: state.originalReleasedAt,
});
commit(types.UPDATE_CREATE_FROM, JSON.parse(createFrom));
+
+ if (parsedRelease.tagName) {
+ dispatch('fetchTagNotes', parsedRelease.tagName);
+ }
} else {
commit(types.INITIALIZE_EMPTY_RELEASE);
}
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
index 0b37c2b81d1..d1cde8b9029 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/getters.js
@@ -170,13 +170,11 @@ export const releaseDeleteMutationVariables = (state) => ({
},
});
-export const formattedReleaseNotes = ({
- includeTagNotes,
- release: { description, tagMessage },
- tagNotes,
- showCreateFrom,
-}) => {
- const notes = showCreateFrom ? tagMessage : tagNotes;
+export const formattedReleaseNotes = (
+ { includeTagNotes, release: { description, tagMessage }, tagNotes },
+ { isNewTag },
+) => {
+ const notes = isNewTag ? tagMessage : tagNotes;
return includeTagNotes && notes
? `${description}\n\n### ${s__('Releases|Tag message')}\n\n${notes}\n`
: description;
diff --git a/app/assets/javascripts/releases/stores/modules/edit_new/state.js b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
index 7bd3968dd93..a02949568b2 100644
--- a/app/assets/javascripts/releases/stores/modules/edit_new/state.js
+++ b/app/assets/javascripts/releases/stores/modules/edit_new/state.js
@@ -61,6 +61,7 @@ export default ({
updateError: null,
tagNotes: '',
+ isFetchingTagNotes: false,
includeTagNotes: false,
existingRelease: null,
originalReleasedAt: new Date(),
diff --git a/app/assets/javascripts/repository/components/upload_blob_modal.vue b/app/assets/javascripts/repository/components/upload_blob_modal.vue
index 4ca625bc0de..c9ac5a19697 100644
--- a/app/assets/javascripts/repository/components/upload_blob_modal.vue
+++ b/app/assets/javascripts/repository/components/upload_blob_modal.vue
@@ -11,7 +11,7 @@ import {
} from '@gitlab/ui';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { ContentTypeMultipartFormData } from '~/lib/utils/headers';
+import { contentTypeMultipartFormData } from '~/lib/utils/headers';
import { numberToHumanSize } from '~/lib/utils/number_utils';
import { visitUrl, joinPaths } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
@@ -159,7 +159,7 @@ export default {
url,
data: this.formData(),
headers: {
- ...ContentTypeMultipartFormData,
+ ...contentTypeMultipartFormData,
},
})
.then((response) => {
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 874803a720d..eb56113a4dd 100644
--- a/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
+++ b/app/assets/javascripts/search/sidebar/components/scope_sidebar_navigation.vue
@@ -1,6 +1,6 @@
<script>
// eslint-disable-next-line no-restricted-imports
-import { mapActions, mapState, mapGetters } from 'vuex';
+import { mapActions, mapGetters } from 'vuex';
import { s__ } from '~/locale';
import eventHub from '~/super_sidebar/event_hub';
import NavItem from '~/super_sidebar/components/nav_item.vue';
@@ -15,15 +15,11 @@ export default {
NavItem,
},
computed: {
- ...mapState(['navigation', 'urlQuery']),
...mapGetters(['navigationItems']),
},
created() {
eventHub.$emit('toggle-menu-header', false);
-
- if (this.urlQuery?.search) {
- this.fetchSidebarCount();
- }
+ this.fetchSidebarCount();
},
methods: {
...mapActions(['fetchSidebarCount']),
diff --git a/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
index c1f0bfc59f3..32327a39de0 100644
--- a/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
+++ b/app/assets/javascripts/search/sidebar/components/searchable_dropdown.vue
@@ -185,6 +185,7 @@ export default {
:searching="loading"
:reset-button-label="$options.i18n.reset"
:toggle-aria-labelled-by="labelId"
+ fluid-width
searchable
block
@shown="openDropdown"
diff --git a/app/assets/javascripts/search/store/actions.js b/app/assets/javascripts/search/store/actions.js
index 211bbaf82cd..d5e275b8a19 100644
--- a/app/assets/javascripts/search/store/actions.js
+++ b/app/assets/javascripts/search/store/actions.js
@@ -1,7 +1,13 @@
import Api from '~/api';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
-import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
+import {
+ visitUrl,
+ setUrlParams,
+ getBaseURL,
+ queryToObject,
+ objectToQuery,
+} from '~/lib/utils/url_utility';
import { logError } from '~/lib/logger';
import { __ } from '~/locale';
import { labelFilterData } from '~/search/sidebar/components/label_filter/data';
@@ -135,19 +141,38 @@ export const setLabelFilterSearch = ({ commit }, { value }) => {
commit(types.SET_LABEL_SEARCH_STRING, value);
};
+const injectWildCardSearch = (state, link) => {
+ const urlObject = new URL(`${getBaseURL()}${link}`);
+ if (!state.urlQuery.search) {
+ const queryObject = queryToObject(urlObject.search);
+ urlObject.search = objectToQuery({ ...queryObject, search: '*' });
+ }
+
+ return urlObject.href;
+};
+
export const fetchSidebarCount = ({ commit, state }) => {
- const promises = Object.values(state.navigation).map((navItem) => {
- // active nav item has count already so we skip it
- if (!navItem.active && navItem.count_link) {
- return axios
- .get(navItem.count_link)
- .then(({ data: { count } }) => {
- commit(types.RECEIVE_NAVIGATION_COUNT, { key: navItem.scope, count });
- })
- .catch((e) => logError(e));
- }
- return Promise.resolve();
- });
+ const items = Object.values(state.navigation)
+ .filter((navigationItem) => !navigationItem.active && navigationItem.count_link)
+ .map((navItem) => {
+ const navigationItem = { ...navItem };
+
+ if (navigationItem.count_link) {
+ navigationItem.count_link = injectWildCardSearch(state, navigationItem.count_link);
+ }
+
+ return navigationItem;
+ });
+
+ const promises = items.map((navigationItem) =>
+ axios
+ .get(navigationItem.count_link)
+ .then(({ data: { count } }) => {
+ commit(types.RECEIVE_NAVIGATION_COUNT, { key: navigationItem.scope, count });
+ })
+ .catch((e) => logError(e)),
+ );
+
return Promise.all(promises);
};
diff --git a/app/assets/javascripts/security_configuration/constants.js b/app/assets/javascripts/security_configuration/constants.js
index 94bcf81a3eb..f8ab8b4685a 100644
--- a/app/assets/javascripts/security_configuration/constants.js
+++ b/app/assets/javascripts/security_configuration/constants.js
@@ -7,15 +7,7 @@ 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';
@@ -23,132 +15,35 @@ import configureSastIacMutation from './graphql/configure_iac.mutation.graphql';
import configureSecretDetectionMutation from './graphql/configure_secret_detection.mutation.graphql';
/**
- * Translations & helpPagePaths for Security Configuration Page
+ * Translations 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 DAST_HELP_PATH = helpPagePath('user/application_security/dast/index');
-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');
@@ -166,105 +61,6 @@ export const SCANNER_NAMES_MAP = {
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',
diff --git a/app/assets/javascripts/security_configuration/index.js b/app/assets/javascripts/security_configuration/index.js
index 8086b200891..40c82661305 100644
--- a/app/assets/javascripts/security_configuration/index.js
+++ b/app/assets/javascripts/security_configuration/index.js
@@ -3,7 +3,6 @@ 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 './constants';
import { augmentFeatures } from './utils';
export const initSecurityConfiguration = (el) => {
@@ -28,10 +27,7 @@ export const initSecurityConfiguration = (el) => {
vulnerabilityTrainingDocsPath,
} = el.dataset;
- const { augmentedSecurityFeatures } = augmentFeatures(
- securityFeatures,
- features ? JSON.parse(features) : [],
- );
+ const { augmentedSecurityFeatures } = augmentFeatures(features ? JSON.parse(features) : []);
return new Vue({
el,
diff --git a/app/assets/javascripts/security_configuration/utils.js b/app/assets/javascripts/security_configuration/utils.js
index 59b49cb3820..23f86b30445 100644
--- a/app/assets/javascripts/security_configuration/utils.js
+++ b/app/assets/javascripts/security_configuration/utils.js
@@ -1,33 +1,41 @@
+import { isEmpty } from 'lodash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { SCANNER_NAMES_MAP } from '~/security_configuration/constants';
import { REPORT_TYPE_DAST } from '~/vue_shared/security_reports/constants';
/**
- * This function takes in 3 arrays of objects, securityFeatures and features.
- * securityFeatures are static arrays living in the constants.
+ * This function takes in a arrays of features.
* features is dynamic and coming from the backend.
- * This function builds a superset of those arrays.
- * It looks for matching keys within the dynamic and the static arrays
- * and will enrich the objects with the available static data.
- * @param [{}] securityFeatures
+ * securityFeatures is nested in features and are static arrays living in backend constants
+ * This function takes the nested securityFeatures config and flattens it to the top level object.
+ * It then filters out any scanner features that lack a security config for rednering in the UI
* @param [{}] features
* @returns {Object} Object with enriched features from constants divided into Security and Compliance Features
*/
-export const augmentFeatures = (securityFeatures, features = []) => {
+export const augmentFeatures = (features = []) => {
const featuresByType = features.reduce((acc, feature) => {
acc[feature.type] = convertObjectPropsToCamelCase(feature, { deep: true });
return acc;
}, {});
+ /**
+ * Track feature configs that are used as nested elements in the UI
+ * so they aren't rendered at the top level as a seperate card
+ */
+ const secondaryFeatures = [];
+
+ // Modify each feature
const augmentFeature = (feature) => {
const augmented = {
...feature,
...featuresByType[feature.type],
};
+ // Secondary layer copies some values from the first layer
if (augmented.secondary) {
augmented.secondary = { ...augmented.secondary, ...featuresByType[feature.secondary.type] };
+ secondaryFeatures.push(feature.secondary.type);
}
if (augmented.type === REPORT_TYPE_DAST && !augmented.onDemandAvailable) {
@@ -41,8 +49,20 @@ export const augmentFeatures = (securityFeatures, features = []) => {
return augmented;
};
+ // Filter out any features that lack a security feature definition or is used as a nested UI element
+ const filterFeatures = (feature) => {
+ return !secondaryFeatures.includes(feature.type) && !isEmpty(feature.securityFeatures || {});
+ };
+
+ // Convert backend provided properties to camelCase, and spread nested security config to the root
+ // level for UI rendering.
+ const flattenFeatures = (feature) => {
+ const flattenedFeature = convertObjectPropsToCamelCase(feature, { deep: true });
+ return augmentFeature({ ...flattenedFeature, ...flattenedFeature.securityFeatures });
+ };
+
return {
- augmentedSecurityFeatures: securityFeatures.map((feature) => augmentFeature(feature)),
+ augmentedSecurityFeatures: features.map(flattenFeatures).filter(filterFeatures),
};
};
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
index 4ff12824008..0ac6208c7d3 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees_widget.vue
@@ -6,6 +6,7 @@ import { TYPE_ALERT, TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, n__ } from '~/locale';
import UserSelect from '~/vue_shared/components/user_select/user_select.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
+import { ISSUE_MR_CHANGE_ASSIGNEE } from '~/behaviors/shortcuts/keybindings';
import { assigneesQueries } from '../../queries/constants';
import SidebarEditableItem from '../sidebar_editable_item.vue';
import SidebarAssigneesRealtime from './assignees_realtime.vue';
@@ -156,6 +157,12 @@ export default {
issuableAuthor() {
return this.issuable?.author;
},
+ assigneeShortcutDescription() {
+ return ISSUE_MR_CHANGE_ASSIGNEE.description;
+ },
+ assigneeShortcutKey() {
+ return ISSUE_MR_CHANGE_ASSIGNEE.defaultKeys[0];
+ },
},
watch: {
iid(_, oldIid) {
@@ -246,6 +253,9 @@ export default {
:loading="isSettingAssignees"
:initial-loading="isAssigneesLoading"
:title="assigneeText"
+ :edit-tooltip="`${assigneeShortcutDescription} <kbd class='flat ml-1' aria-hidden=true>${assigneeShortcutKey}</kbd>`"
+ :edit-aria-label="assigneeShortcutDescription"
+ :edit-keyshortcuts="assigneeShortcutKey"
:is-dirty="isDirty"
@open="showDropdown"
@close="saveAssignees"
diff --git a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
index 93e3cfba309..f3209d1a02f 100644
--- a/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
+++ b/app/assets/javascripts/sidebar/components/labels/labels_select_widget/dropdown_contents_create_view.vue
@@ -1,17 +1,11 @@
<script>
import { get } from 'lodash';
-import {
- GlAlert,
- GlTooltipDirective,
- GlButton,
- GlFormInput,
- GlLink,
- GlLoadingIcon,
-} from '@gitlab/ui';
+import { GlAlert, GlTooltipDirective, GlButton, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
import produce from 'immer';
import { createAlert } from '~/alert';
import { WORKSPACE_GROUP } from '~/issues/constants';
import { __ } from '~/locale';
+import SidebarColorPicker from '../../sidebar_color_picker.vue';
import { workspaceLabelsQueries, workspaceCreateLabelMutation } from '../../../queries/constants';
import { DEFAULT_LABEL_COLOR } from './constants';
@@ -22,8 +16,8 @@ export default {
GlAlert,
GlButton,
GlFormInput,
- GlLink,
GlLoadingIcon,
+ SidebarColorPicker,
},
directives: {
GlTooltip: GlTooltipDirective,
@@ -84,15 +78,6 @@ export default {
},
},
methods: {
- getColorCode(color) {
- return Object.keys(color).pop();
- },
- getColorName(color) {
- return Object.values(color).pop();
- },
- handleColorClick(color) {
- this.selectedColor = this.getColorCode(color);
- },
updateLabelsInCache(store, label) {
const { query, dataPath } = workspaceLabelsQueries[this.workspaceType];
@@ -163,34 +148,7 @@ export default {
data-testid="label-title-input"
/>
</div>
- <div class="dropdown-content gl-px-3">
- <div class="suggest-colors suggest-colors-dropdown gl-mt-0! gl-mb-3! gl-mb-0">
- <gl-link
- v-for="(color, index) in suggestedColors"
- :key="index"
- v-gl-tooltip:tooltipcontainer
- :style="{ backgroundColor: getColorCode(color) }"
- :title="getColorName(color)"
- @click.prevent="handleColorClick(color)"
- />
- </div>
- <div class="color-input-container gl-display-flex">
- <gl-form-input
- v-model.trim="selectedColor"
- class="gl-rounded-top-right-none gl-rounded-bottom-right-none gl-mr-n1 gl-mb-2 gl-w-8"
- type="color"
- :value="selectedColor"
- :placeholder="__('Select color')"
- data-testid="selected-color"
- />
- <gl-form-input
- v-model.trim="selectedColor"
- class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
- :placeholder="__('Use custom color #FF0000')"
- data-testid="selected-color-text"
- />
- </div>
- </div>
+ <sidebar-color-picker v-model.trim="selectedColor" />
<div class="dropdown-actions gl-display-flex gl-justify-content-space-between gl-pt-3 gl-px-3">
<gl-button
:disabled="disableCreate"
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 e0d7400f7a6..cbaa8a59813 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
@@ -1,12 +1,13 @@
<script>
import { debounce } from 'lodash';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
-import { MutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { mutationOperationMode, getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createAlert } from '~/alert';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { TYPE_EPIC, TYPE_ISSUE, TYPE_MERGE_REQUEST, TYPE_TEST_CASE } from '~/issues/constants';
import { __ } from '~/locale';
+import { ISSUABLE_CHANGE_LABEL } from '~/behaviors/shortcuts/keybindings';
import { issuableLabelsQueries } from '../../../queries/constants';
import SidebarEditableItem from '../../sidebar_editable_item.vue';
import { DEBOUNCE_DROPDOWN_DELAY, VARIANT_SIDEBAR } from './constants';
@@ -159,6 +160,12 @@ export default {
isLockOnMergeSupported() {
return this.issuableSupportsLockOnMerge || this.issuable?.supportsLockOnMerge;
},
+ labelShortcutDescription() {
+ return ISSUABLE_CHANGE_LABEL.description;
+ },
+ labelShortcutKey() {
+ return ISSUABLE_CHANGE_LABEL.defaultKeys[0];
+ },
},
apollo: {
issuable: {
@@ -275,7 +282,7 @@ export default {
case TYPE_MERGE_REQUEST:
return {
...updateVariables,
- operationMode: MutationOperationMode.Replace,
+ operationMode: mutationOperationMode.replace,
};
case TYPE_EPIC:
return {
@@ -336,7 +343,7 @@ export default {
return {
...removeVariables,
labelIds: [labelId],
- operationMode: MutationOperationMode.Remove,
+ operationMode: mutationOperationMode.remove,
};
case TYPE_EPIC:
return {
@@ -375,6 +382,9 @@ export default {
<sidebar-editable-item
ref="editable"
:title="__('Labels')"
+ :edit-tooltip="`${labelShortcutDescription} <kbd class='flat ml-1' aria-hidden=true>${labelShortcutKey}</kbd>`"
+ :edit-aria-label="labelShortcutDescription"
+ :edit-keyshortcuts="labelShortcutKey"
:loading="isLoading"
:can-edit="allowLabelEdit"
@open="oldIid = null"
diff --git a/app/assets/javascripts/sidebar/components/sidebar_color_picker.vue b/app/assets/javascripts/sidebar/components/sidebar_color_picker.vue
new file mode 100644
index 00000000000..95b1febb575
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/sidebar_color_picker.vue
@@ -0,0 +1,75 @@
+<script>
+import { GlFormInput, GlLink, GlTooltipDirective } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlFormInput,
+ GlLink,
+ },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
+ props: {
+ value: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ computed: {
+ suggestedColors() {
+ const colorsMap = gon.suggested_label_colors;
+ return Object.keys(colorsMap).map((color) => ({ [color]: colorsMap[color] }));
+ },
+ selectedColor: {
+ get() {
+ return this.value;
+ },
+ set(color) {
+ this.handleColorClick(color);
+ },
+ },
+ },
+ methods: {
+ handleColorClick(color) {
+ this.$emit('input', color);
+ },
+ getColorCode(color) {
+ return Object.keys(color).pop();
+ },
+ getColorName(color) {
+ return Object.values(color).pop();
+ },
+ },
+};
+</script>
+<template>
+ <div class="dropdown-content gl-px-3">
+ <div class="suggest-colors suggest-colors-dropdown gl-mt-0!">
+ <gl-link
+ v-for="(color, index) in suggestedColors"
+ :key="index"
+ v-gl-tooltip:tooltipcontainer
+ :style="{ backgroundColor: getColorCode(color) }"
+ :title="getColorName(color)"
+ @click.prevent="handleColorClick(getColorCode(color))"
+ />
+ </div>
+ <div class="color-input-container gl-display-flex">
+ <gl-form-input
+ v-model.trim="selectedColor"
+ class="gl-rounded-top-right-none gl-rounded-bottom-right-none gl-mr-n1 gl-mb-2 gl-w-8"
+ type="color"
+ :value="selectedColor"
+ :placeholder="__('Select color')"
+ data-testid="selected-color"
+ />
+ <gl-form-input
+ v-model.trim="selectedColor"
+ class="gl-rounded-top-left-none gl-rounded-bottom-left-none gl-mb-2"
+ :placeholder="__('Use custom color #FF0000')"
+ data-testid="selected-color-text"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
index ad83866ceb2..c887d5d292e 100644
--- a/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
+++ b/app/assets/javascripts/sidebar/components/sidebar_editable_item.vue
@@ -1,5 +1,5 @@
<script>
-import { GlButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
import { __ } from '~/locale';
export default {
@@ -7,6 +7,9 @@ export default {
unassigned: __('Unassigned'),
},
components: { GlButton, GlLoadingIcon },
+ directives: {
+ GlTooltip: GlTooltipDirective,
+ },
inject: {
canUpdate: {},
isClassicSidebar: {
@@ -58,6 +61,21 @@ export default {
required: false,
default: false,
},
+ editTooltip: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ editAriaLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ editKeyshortcuts: {
+ type: String,
+ required: false,
+ default: '',
+ },
},
data() {
return {
@@ -68,6 +86,15 @@ export default {
editButtonText() {
return this.isDirty ? __('Apply') : __('Edit');
},
+ editTooltipText() {
+ return this.isDirty ? '' : this.editTooltip;
+ },
+ editAriaLabelText() {
+ return this.isDirty ? this.editButtonText : this.editAriaLabel;
+ },
+ editKeyshortcutsText() {
+ return this.isDirty ? __('Escape') : this.editKeyshortcuts;
+ },
},
destroyed() {
window.removeEventListener('click', this.collapseWhenOffClick);
@@ -150,9 +177,13 @@ export default {
<gl-button
v-if="canUpdate && !initialLoading && canEdit"
:id="buttonId"
+ v-gl-tooltip.viewport.html
category="tertiary"
size="small"
class="gl-text-gray-900! gl-ml-auto hide-collapsed gl-mr-n2 shortcut-sidebar-dropdown-toggle"
+ :title="editTooltipText"
+ :aria-label="editAriaLabelText"
+ :aria-keyshortcuts="editKeyshortcutsText"
data-testid="edit-button"
:data-track-action="tracking.event"
:data-track-label="tracking.label"
diff --git a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
index f2257adb79c..3dfe5b6cc15 100644
--- a/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
+++ b/app/assets/javascripts/sidebar/components/todo_toggle/sidebar_todo_widget.vue
@@ -6,7 +6,7 @@ import { TYPE_MERGE_REQUEST } from '~/issues/constants';
import { __, sprintf } from '~/locale';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import Tracking from '~/tracking';
-import { TodoMutationTypes } from '../../constants';
+import { todoMutationTypes } from '../../constants';
import { todoQueries, todoMutations } from '../../queries/constants';
import { todoLabel } from '../../utils';
import TodoButton from './todo_button.vue';
@@ -104,9 +104,9 @@ export default {
},
todoMutationType() {
if (this.hasTodo) {
- return TodoMutationTypes.MarkDone;
+ return todoMutationTypes.markDone;
}
- return TodoMutationTypes.Create;
+ return todoMutationTypes.create;
},
collapsedButtonIcon() {
return this.hasTodo ? 'todo-done' : 'todo-add';
diff --git a/app/assets/javascripts/sidebar/constants.js b/app/assets/javascripts/sidebar/constants.js
index f13f613733b..953684b5c93 100644
--- a/app/assets/javascripts/sidebar/constants.js
+++ b/app/assets/javascripts/sidebar/constants.js
@@ -44,9 +44,9 @@ export const IssuableAttributeState = {
[IssuableAttributeType.Milestone]: 'active',
};
-export const TodoMutationTypes = {
- Create: 'create',
- MarkDone: 'mark-done',
+export const todoMutationTypes = {
+ create: 'create',
+ markDone: 'mark-done',
};
export function dropdowni18nText(issuableAttribute, issuableType) {
diff --git a/app/assets/javascripts/sidebar/queries/constants.js b/app/assets/javascripts/sidebar/queries/constants.js
index 6bcdc01a003..dbb2f14880a 100644
--- a/app/assets/javascripts/sidebar/queries/constants.js
+++ b/app/assets/javascripts/sidebar/queries/constants.js
@@ -22,7 +22,7 @@ import groupLabelsQuery from '../components/labels/labels_select_widget/graphql/
import issueLabelsQuery from '../components/labels/labels_select_widget/graphql/issue_labels.query.graphql';
import mergeRequestLabelsQuery from '../components/labels/labels_select_widget/graphql/merge_request_labels.query.graphql';
import projectLabelsQuery from '../components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import { IssuableAttributeType, TodoMutationTypes } from '../constants';
+import { IssuableAttributeType, todoMutationTypes } from '../constants';
import epicConfidentialQuery from './epic_confidential.query.graphql';
import epicDueDateQuery from './epic_due_date.query.graphql';
import epicParticipantsQuery from './epic_participants.query.graphql';
@@ -282,8 +282,8 @@ export const todoQueries = {
};
export const todoMutations = {
- [TodoMutationTypes.Create]: todoCreateMutation,
- [TodoMutationTypes.MarkDone]: todoMarkDoneMutation,
+ [todoMutationTypes.create]: todoCreateMutation,
+ [todoMutationTypes.markDone]: todoMarkDoneMutation,
};
export const escalationStatusQuery = getEscalationStatusQuery;
diff --git a/app/assets/javascripts/super_sidebar/components/pinned_section.vue b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
index 05040218164..2be9f9e9f7d 100644
--- a/app/assets/javascripts/super_sidebar/components/pinned_section.vue
+++ b/app/assets/javascripts/super_sidebar/components/pinned_section.vue
@@ -116,7 +116,11 @@ export default {
@pin-remove="onPinRemove(item.id, item.title)"
/>
</draggable>
- <li v-else class="gl-text-secondary gl-font-sm gl-py-3" style="margin-left: 2.5rem">
+ <li
+ v-else
+ class="gl-text-secondary gl-font-sm gl-py-3 super-sidebar-empty-pinned-text"
+ style="margin-left: 2.5rem"
+ >
{{ $options.i18n.emptyHint }}
</li>
</menu-section>
diff --git a/app/assets/javascripts/super_sidebar/components/user_menu.vue b/app/assets/javascripts/super_sidebar/components/user_menu.vue
index f129d067cdc..cc558edfd68 100644
--- a/app/assets/javascripts/super_sidebar/components/user_menu.vue
+++ b/app/assets/javascripts/super_sidebar/components/user_menu.vue
@@ -30,6 +30,8 @@ export default {
oneOfGroupsRunningOutOfPipelineMinutes: s__('CurrentUser|One of your groups is running out'),
gitlabNext: s__('CurrentUser|Switch to GitLab Next'),
startTrial: s__('CurrentUser|Start an Ultimate trial'),
+ enterAdminMode: s__('CurrentUser|Enter Admin Mode'),
+ leaveAdminMode: s__('CurrentUser|Leave Admin Mode'),
signOut: __('Sign out'),
},
components: {
@@ -142,6 +144,27 @@ export default {
},
};
},
+ enterAdminModeItem() {
+ return {
+ text: this.$options.i18n.enterAdminMode,
+ href: this.data.admin_mode.enter_admin_mode_url,
+ extraAttrs: {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'enter_admin_mode',
+ },
+ };
+ },
+ leaveAdminModeItem() {
+ return {
+ text: this.$options.i18n.leaveAdminMode,
+ href: this.data.admin_mode.leave_admin_mode_url,
+ extraAttrs: {
+ ...USER_MENU_TRACKING_DEFAULTS,
+ 'data-track-label': 'leave_admin_mode',
+ 'data-method': 'post',
+ },
+ };
+ },
signOutGroup() {
return {
items: [
@@ -184,6 +207,20 @@ export default {
}
: {};
},
+ showEnterAdminModeItem() {
+ return (
+ this.data.admin_mode.user_is_admin &&
+ this.data.admin_mode.admin_mode_feature_enabled &&
+ !this.data.admin_mode.admin_mode_active
+ );
+ },
+ showLeaveAdminModeItem() {
+ return (
+ this.data.admin_mode.user_is_admin &&
+ this.data.admin_mode.admin_mode_feature_enabled &&
+ this.data.admin_mode.admin_mode_active
+ );
+ },
showNotificationDot() {
return this.data.pipeline_minutes?.show_notification_dot;
},
@@ -248,7 +285,11 @@ export default {
@shown="onShow"
>
<template #toggle>
- <gl-button category="tertiary" class="user-bar-dropdown-toggle btn-with-notification">
+ <gl-button
+ category="tertiary"
+ class="user-bar-dropdown-toggle btn-with-notification"
+ data-testid="user-menu-toggle"
+ >
<span class="gl-sr-only">{{ toggleText }}</span>
<gl-avatar
:size="24"
@@ -320,6 +361,17 @@ export default {
:item="gitlabNextItem"
data-testid="gitlab-next-item"
/>
+
+ <gl-disclosure-dropdown-item
+ v-if="showEnterAdminModeItem"
+ :item="enterAdminModeItem"
+ data-testid="enter-admin-mode-item"
+ />
+ <gl-disclosure-dropdown-item
+ v-if="showLeaveAdminModeItem"
+ :item="leaveAdminModeItem"
+ data-testid="leave-admin-mode-item"
+ />
</gl-disclosure-dropdown-group>
<gl-disclosure-dropdown-group
diff --git a/app/assets/javascripts/tracking/constants.js b/app/assets/javascripts/tracking/constants.js
index bc416b20e80..06d324a970f 100644
--- a/app/assets/javascripts/tracking/constants.js
+++ b/app/assets/javascripts/tracking/constants.js
@@ -31,8 +31,6 @@ export const REFERRER_TTL = 24 * 60 * 60 * 1000;
export const GOOGLE_ANALYTICS_ID_COOKIE_NAME = '_ga';
-export const GITLAB_INTERNAL_EVENT_CATEGORY = 'InternalEventTracking';
-
export const SERVICE_PING_SCHEMA = 'iglu:com.gitlab/gitlab_service_ping/jsonschema/1-0-1';
export const SERVICE_PING_SECURITY_CONFIGURATION_THREAT_MANAGEMENT_VISIT =
diff --git a/app/assets/javascripts/tracking/internal_events.js b/app/assets/javascripts/tracking/internal_events.js
index a6d14bfbfd8..7da6da16d6f 100644
--- a/app/assets/javascripts/tracking/internal_events.js
+++ b/app/assets/javascripts/tracking/internal_events.js
@@ -1,11 +1,7 @@
import API from '~/api';
import Tracking from './tracking';
-import {
- GITLAB_INTERNAL_EVENT_CATEGORY,
- LOAD_INTERNAL_EVENTS_SELECTOR,
- SERVICE_PING_SCHEMA,
-} from './constants';
+import { LOAD_INTERNAL_EVENTS_SELECTOR, SERVICE_PING_SCHEMA } from './constants';
import { Tracker } from './tracker';
import { InternalEventHandler, createInternalEventPayload } from './utils';
@@ -16,7 +12,7 @@ const InternalEvents = {
*/
trackEvent(event) {
API.trackInternalEvent(event);
- Tracking.event(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
+ Tracking.event(undefined, event, {
context: {
schema: SERVICE_PING_SCHEMA,
data: {
diff --git a/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue
new file mode 100644
index 00000000000..ddf240fcafa
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/namespace_storage_app.vue
@@ -0,0 +1,67 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import StorageUsageStatistics from 'ee_else_ce/usage_quotas/storage/components/storage_usage_statistics.vue';
+
+export default {
+ name: 'NamespaceStorageApp',
+ components: {
+ GlAlert,
+ StorageUsageStatistics,
+ },
+ props: {
+ namespaceLoadingError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ projectsLoadingError: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isNamespaceStorageStatisticsLoading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ namespace: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ computed: {
+ usedStorage() {
+ return (
+ // This is the coefficient adjusted forked repo size, only used in EE
+ this.namespace.rootStorageStatistics?.costFactoredStorageSize ??
+ // This is the actual storage size value, used in CE or when the above is disabled
+ this.namespace.rootStorageStatistics?.storageSize
+ );
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <gl-alert
+ v-if="namespaceLoadingError || projectsLoadingError"
+ variant="danger"
+ :dismissible="false"
+ class="gl-mt-4"
+ >
+ {{
+ s__(
+ 'UsageQuota|An error occured while loading the storage usage details. Please refresh the page to try again.',
+ )
+ }}
+ </gl-alert>
+ <storage-usage-statistics
+ :additional-purchased-storage-size="namespace.additionalPurchasedStorageSize"
+ :used-storage="usedStorage"
+ :loading="isNamespaceStorageStatisticsLoading"
+ />
+
+ <slot name="ee-storage-app"></slot>
+ </div>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue
new file mode 100644
index 00000000000..6e73fc0ba69
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_overview_card.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlCard, GlSkeletonLoader } from '@gitlab/ui';
+import NumberToHumanSize from '~/vue_shared/components/number_to_human_size/number_to_human_size.vue';
+
+export default {
+ name: 'StorageUsageOverviewCard',
+ components: {
+ GlCard,
+ GlSkeletonLoader,
+ NumberToHumanSize,
+ },
+ props: {
+ usedStorage: {
+ type: Number,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-card>
+ <gl-skeleton-loader v-if="loading" :height="64">
+ <rect width="140" height="30" x="5" y="0" rx="4" />
+ <rect width="240" height="10" x="5" y="40" rx="4" />
+ <rect width="340" height="10" x="5" y="54" rx="4" />
+ </gl-skeleton-loader>
+
+ <div v-else>
+ <div class="gl-font-weight-bold" data-testid="namespace-storage-card-title">
+ {{ s__('UsageQuota|Namespace storage used') }}
+ </div>
+ <div class="gl-font-size-h-display gl-font-weight-bold gl-line-height-ratio-1000 gl-my-3">
+ <number-to-human-size label-class="gl-font-lg" :value="Number(usedStorage)" plain-zero />
+ </div>
+ <hr class="gl-my-4" />
+ <p>
+ {{
+ s__(
+ 'UsageQuota|Namespace total storage represents the sum of storage consumed by all projects, Container Registry, and Dependency Proxy.',
+ )
+ }}
+ </p>
+ </div>
+ </gl-card>
+</template>
diff --git a/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue
new file mode 100644
index 00000000000..d7e550dd715
--- /dev/null
+++ b/app/assets/javascripts/usage_quotas/storage/components/storage_usage_statistics.vue
@@ -0,0 +1,33 @@
+<script>
+import StorageUsageOverviewCard from './storage_usage_overview_card.vue';
+
+export default {
+ name: 'StorageUsageStatistics',
+ components: {
+ StorageUsageOverviewCard,
+ },
+ props: {
+ usedStorage: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ loading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <h3 data-testid="overview-subtitle">{{ s__('UsageQuota|Namespace overview') }}</h3>
+ <div class="gl-display-grid gl-md-grid-template-columns-2 gl-gap-5 gl-py-4">
+ <storage-usage-overview-card
+ :used-storage="usedStorage"
+ :loading="loading"
+ data-testid="namespace-usage-total"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index a4afdee4d49..1aed3362c42 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -48,7 +48,7 @@ export default {
variables() {
return this.mergeRequestQueryVariables;
},
- update: (data) => data.project.mergeRequest,
+ update: (data) => data.project?.mergeRequest || {},
},
},
components: {
@@ -90,7 +90,7 @@ export default {
return this.state.rebaseInProgress;
},
canPushToSourceBranch() {
- return this.state.userPermissions.pushToSourceBranch;
+ return this.state.userPermissions?.pushToSourceBranch || false;
},
targetBranch() {
return this.state.targetBranch;
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 1516b63f96d..af7453d84d6 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
@@ -119,7 +119,11 @@ export default {
},
) {
if (mergeRequestMergeStatusUpdated) {
- this.state = mergeRequestMergeStatusUpdated;
+ this.state = {
+ ...mergeRequestMergeStatusUpdated,
+ mergeRequestsFfOnlyEnabled: this.state.mergeRequestsFfOnlyEnabled,
+ onlyAllowMergeIfPipelineSucceeds: this.state.onlyAllowMergeIfPipelineSucceeds,
+ };
if (!this.commitMessageIsTouched) {
this.commitMessage = mergeRequestMergeStatusUpdated.defaultMergeCommitMessage;
@@ -277,10 +281,7 @@ export default {
return __('Merge');
},
showAutoMergeHelperText() {
- return (
- !(this.status === PIPELINE_FAILED_STATE || this.isPipelineFailed) &&
- this.isAutoMergeAvailable
- );
+ return this.isAutoMergeAvailable;
},
hasPipelineMustSucceedConflict() {
return !this.hasCI && this.state.onlyAllowMergeIfPipelineSucceeds;
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 7413e2237c3..afdb9e9ff08 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
@@ -72,6 +72,11 @@ export default {
return this.actionButtons.length > 0;
},
},
+ methods: {
+ hasHeader() {
+ return Boolean(this.$scopedSlots.header || this.header || this.shouldShowHeaderActions);
+ },
+ },
i18n: {
learnMore: __('Learn more'),
},
@@ -91,9 +96,9 @@ export default {
:icon-name="statusIconName"
/>
<div class="gl-w-full gl-min-w-0">
- <div class="gl-display-flex">
+ <div v-if="hasHeader()" class="gl-display-flex">
<slot name="header">
- <div v-if="header" class="gl-mb-2">
+ <div class="gl-mb-2">
<strong v-safe-html="generatedHeader" class="gl-display-block"></strong
><span
v-if="generatedSubheader"
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 5e9b72e13cf..db1daa3ce01 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -46,6 +46,7 @@ export default () => {
gl.mrWidgetData.can_create_pipeline_in_target_project,
),
commitPathTemplate: gl.mrWidgetData.commit_path_template,
+ canAdminVulnerability: gl.mrWidgetData.can_admin_vulnerability,
dismissalDescriptions,
},
...MrWidgetOptions,
diff --git a/app/assets/javascripts/vue_shared/components/file_row.vue b/app/assets/javascripts/vue_shared/components/file_row.vue
index 6ac75230d88..3419e6eb1b6 100644
--- a/app/assets/javascripts/vue_shared/components/file_row.vue
+++ b/app/assets/javascripts/vue_shared/components/file_row.vue
@@ -1,5 +1,5 @@
<script>
-import { GlTruncate } from '@gitlab/ui';
+import { GlTruncate, GlIcon } from '@gitlab/ui';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import FileHeader from '~/vue_shared/components/file_row_header.vue';
@@ -10,6 +10,7 @@ export default {
FileHeader,
FileIcon,
GlTruncate,
+ GlIcon,
},
props: {
file: {
@@ -141,6 +142,7 @@ export default {
data-testid="file-row-name-container"
:class="[fileClasses, { 'str-truncated': !truncateMiddle, 'gl-min-w-0': truncateMiddle }]"
>
+ <gl-icon v-if="file.pinned" name="thumbtack" :size="16" />
<file-icon
class="file-row-icon"
:class="{ 'text-secondary': file.type === 'tree' }"
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 5362ceac9ee..6549de96c98 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
@@ -67,6 +67,7 @@ export const TOKEN_TITLE_AUTHOR = __('Author');
export const TOKEN_TITLE_CONFIDENTIAL = __('Confidential');
export const TOKEN_TITLE_CONTACT = s__('Crm|Contact');
export const TOKEN_TITLE_GROUP = __('Group');
+export const TOKEN_TITLE_GROUP_INVITE = __('Group invite');
export const TOKEN_TITLE_LABEL = __('Label');
export const TOKEN_TITLE_PROJECT = __('Project');
export const TOKEN_TITLE_MILESTONE = __('Milestone');
@@ -90,6 +91,7 @@ export const TOKEN_TYPE_AUTHOR = 'author';
export const TOKEN_TYPE_CONFIDENTIAL = 'confidential';
export const TOKEN_TYPE_CONTACT = 'contact';
export const TOKEN_TYPE_GROUP = 'group';
+export const TOKEN_TYPE_GROUP_INVITE = 'group-invite';
export const TOKEN_TYPE_EPIC = 'epic';
// As health status gets reused between issue lists and boards
// this is in the shared constants. Until we have not decoupled the EE filtered search bar
diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/daterange_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/daterange_token.vue
new file mode 100644
index 00000000000..8da5f0cca4a
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/daterange_token.vue
@@ -0,0 +1,158 @@
+<script>
+import {
+ GlIcon,
+ GlDaterangePicker,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ GlOutsideDirective,
+} from '@gitlab/ui';
+import { __ } from '~/locale';
+import { formatDate } from '~/lib/utils/datetime/date_format_utility';
+
+const CUSTOM_DATE_FILTER_TYPE = 'custom-date';
+
+export default {
+ directives: { Outside: GlOutsideDirective },
+ components: {
+ GlIcon,
+ GlDaterangePicker,
+ GlFilteredSearchToken,
+ GlFilteredSearchSuggestion,
+ },
+ props: {
+ active: {
+ type: Boolean,
+ required: true,
+ },
+ value: {
+ type: Object,
+ required: true,
+ },
+ config: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ datePickerShown: false,
+ };
+ },
+ computed: {
+ isActive() {
+ return this.datePickerShown || this.active;
+ },
+ computedValue() {
+ if (this.datePickerShown) {
+ return {
+ ...this.value,
+ data: '',
+ };
+ }
+ return this.value;
+ },
+ dataSegmentInputAttributes() {
+ const id = 'time_range_data_segment_input';
+ if (this.datePickerShown) {
+ return {
+ id,
+ placeholder: 'YYYY-MM-DD - YYYY-MM-DD', // eslint-disable-line @gitlab/require-i18n-strings
+ style: 'padding-left: 23px;',
+ };
+ }
+ return {
+ id,
+ };
+ },
+ computedConfig() {
+ return {
+ ...this.config,
+ options: undefined, // remove options from config to avoid default options being rendered
+ };
+ },
+ suggestions() {
+ const suggestions = this.config.options.map((option) => ({
+ value: option.value,
+ text: option.title,
+ }));
+ suggestions.push({ value: CUSTOM_DATE_FILTER_TYPE, text: __('Custom') });
+ return suggestions;
+ },
+ defaultStartDate() {
+ return new Date();
+ },
+ },
+ methods: {
+ hideDatePicker() {
+ this.datePickerShown = false;
+ },
+ showDatePicker() {
+ this.datePickerShown = true;
+ },
+ handleClickOutside() {
+ this.hideDatePicker();
+ },
+ handleComplete(value) {
+ if (value === CUSTOM_DATE_FILTER_TYPE) {
+ this.showDatePicker();
+ }
+ },
+ selectValue(inputValue, submitValue) {
+ let value = inputValue;
+ if (typeof inputValue === 'object' && inputValue.startDate && inputValue.endDate) {
+ const { startDate, endDate } = inputValue;
+ const format = 'yyyy-mm-dd';
+ value = `${formatDate(startDate, format)} - ${formatDate(endDate, format)}`;
+ }
+ submitValue(value);
+ this.hideDatePicker();
+ },
+ },
+ CUSTOM_DATE_FILTER_TYPE: 'custom-date',
+};
+</script>
+
+<template>
+ <gl-filtered-search-token
+ :data-segment-input-attributes="dataSegmentInputAttributes"
+ v-bind="{ ...$props, ...$attrs }"
+ :view-only="datePickerShown"
+ :active="isActive"
+ :value="computedValue"
+ :config="computedConfig"
+ v-on="$listeners"
+ @complete="handleComplete"
+ >
+ <template #before-data-segment-input="{ submitValue }">
+ <gl-icon
+ v-if="datePickerShown"
+ class="gl-text-gray-500"
+ name="calendar"
+ style="margin-left: 5px; margin-right: -15px; z-index: 1; pointer-events: none"
+ />
+ <div
+ v-if="datePickerShown"
+ v-outside="handleClickOutside"
+ class="gl-absolute gl-z-index-1 gl-bg-white gl-border-1 gl-border-gray-200 gl-my-2 gl-p-4 gl-rounded-base gl-shadow-x0-y2-b4-s0 gl-top-full"
+ >
+ <gl-daterange-picker
+ start-opened
+ :default-start-date="defaultStartDate"
+ @input="selectValue($event, submitValue)"
+ />
+ </div>
+ </template>
+
+ <template #suggestions>
+ <div v-if="!datePickerShown">
+ <gl-filtered-search-suggestion
+ v-for="token in suggestions"
+ :key="token.value"
+ :value="token.value"
+ >
+ {{ token.text }}
+ </gl-filtered-search-suggestion>
+ </div>
+ </template>
+ </gl-filtered-search-token>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/gl_countdown.vue b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
index 1769a283d8c..0e8ecc36f37 100644
--- a/app/assets/javascripts/vue_shared/components/gl_countdown.vue
+++ b/app/assets/javascripts/vue_shared/components/gl_countdown.vue
@@ -30,6 +30,11 @@ export default {
mounted() {
const updateRemainingTime = () => {
const remainingMilliseconds = calculateRemainingMilliseconds(this.endDateString);
+
+ if (remainingMilliseconds < 1) {
+ this.$emit('timer-expired');
+ }
+
this.remainingTime = formatTime(remainingMilliseconds);
};
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
index a375a167c68..e84a810c0b0 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list.vue
@@ -13,6 +13,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -24,6 +29,7 @@ export default {
:key="group.id"
:group="group"
:show-group-icon="showGroupIcon"
+ :class="listItemClass"
@delete="$emit('delete', $event)"
/>
</ul>
diff --git a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
index ca1e7400f2d..ace3846723c 100644
--- a/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/groups_list/groups_list_item.vue
@@ -1,10 +1,9 @@
<script>
-import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText } from '@gitlab/ui';
+import { GlAvatarLabeled, GlIcon, GlTooltipDirective, GlTruncateText, GlBadge } from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, GROUP_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import SafeHtml from '~/vue_shared/directives/safe_html';
@@ -20,15 +19,12 @@ export default {
showMore: __('Show more'),
showLess: __('Show less'),
},
- avatarSize: { default: 32, md: 48 },
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
+ truncateTextToggleButtonProps: { class: 'gl-font-sm!' },
components: {
GlAvatarLabeled,
GlIcon,
- UserAccessRoleBadge,
GlTruncateText,
+ GlBadge,
ListActions,
DangerConfirmModal,
},
@@ -76,7 +72,7 @@ export default {
return this.group.parent ? 'subgroup' : 'group';
},
statsPadding() {
- return this.showGroupIcon ? 'gl-pl-11' : 'gl-pl-8';
+ return this.showGroupIcon ? 'gl-pl-12' : 'gl-pl-10';
},
descendantGroupsCount() {
return numberToMetricPrefix(this.group.descendantGroupsCount);
@@ -113,21 +109,22 @@ export default {
</script>
<template>
- <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
- <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <li class="groups-list-item gl-py-5 gl-border-b gl-display-flex">
+ <div class="gl-md-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
+ <div
v-if="showGroupIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- :name="groupIconName"
- />
+ class="gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-h-9 gl-mr-3"
+ >
+ <gl-icon class="gl-text-secondary" :name="groupIconName" />
+ </div>
<gl-avatar-labeled
:entity-id="group.id"
:entity-name="group.fullName"
:label="group.fullName"
:label-link="group.webUrl"
shape="rect"
- :size="$options.avatarSize"
+ :size="48"
>
<template #meta>
<div class="gl-px-2">
@@ -141,9 +138,9 @@ export default {
/>
</div>
<div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
- }}</user-access-role-badge>
+ }}</gl-badge>
</div>
</div>
</div>
@@ -154,57 +151,57 @@ export default {
:mobile-lines="2"
:show-more-text="$options.i18n.showMore"
:show-less-text="$options.i18n.showLess"
- class="gl-mt-2"
+ :toggle-button-props="$options.truncateTextToggleButtonProps"
+ class="gl-mt-2 gl-max-w-88"
>
<div
- v-safe-html:[$options.safeHtmlConfig]="group.descriptionHtml"
- class="gl-font-sm md"
+ v-safe-html="group.descriptionHtml"
+ class="gl-font-sm gl-text-secondary md"
data-testid="group-description"
></div>
</gl-truncate-text>
</gl-avatar-labeled>
</div>
<div
- class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3"
+ class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0 gl-md-ml-3 gl-md-h-9"
:class="statsPadding"
>
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
- <div
- v-gl-tooltip="$options.i18n.subgroups"
- :aria-label="$options.i18n.subgroups"
- class="gl-text-secondary"
- data-testid="subgroups-count"
- >
- <gl-icon name="subgroup" />
- <span>{{ descendantGroupsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.projects"
- :aria-label="$options.i18n.projects"
- class="gl-text-secondary"
- data-testid="projects-count"
- >
- <gl-icon name="project" />
- <span>{{ projectsCount }}</span>
- </div>
- <div
- v-gl-tooltip="$options.i18n.directMembers"
- :aria-label="$options.i18n.directMembers"
- class="gl-text-secondary"
- data-testid="members-count"
- >
- <gl-icon name="users" />
- <span>{{ groupMembersCount }}</span>
- </div>
+ <div
+ v-gl-tooltip="$options.i18n.subgroups"
+ :aria-label="$options.i18n.subgroups"
+ class="gl-text-secondary"
+ data-testid="subgroups-count"
+ >
+ <gl-icon name="subgroup" />
+ <span>{{ descendantGroupsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.projects"
+ :aria-label="$options.i18n.projects"
+ class="gl-text-secondary"
+ data-testid="projects-count"
+ >
+ <gl-icon name="project" />
+ <span>{{ projectsCount }}</span>
+ </div>
+ <div
+ v-gl-tooltip="$options.i18n.directMembers"
+ :aria-label="$options.i18n.directMembers"
+ class="gl-text-secondary"
+ data-testid="members-count"
+ >
+ <gl-icon name="users" />
+ <span>{{ groupMembersCount }}</span>
</div>
</div>
</div>
- <list-actions
- v-if="hasActions"
- class="gl-ml-3 gl-md-align-self-center"
- :actions="actions"
- :available-actions="group.availableActions"
- />
+ <div class="gl-display-flex gl-align-items-center gl-h-9 gl-ml-3">
+ <list-actions
+ v-if="hasActions"
+ :actions="actions"
+ :available-actions="group.availableActions"
+ />
+ </div>
<danger-confirm-modal
v-if="hasActionDelete"
diff --git a/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.stories.js b/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.stories.js
new file mode 100644
index 00000000000..0576e9796fc
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.stories.js
@@ -0,0 +1,35 @@
+import HelpPageLink from './help_page_link.vue';
+
+export default {
+ component: HelpPageLink,
+ title: 'vue_shared/help_page_link',
+};
+
+const Template = (args, { argTypes }) => ({
+ components: { HelpPageLink },
+ props: Object.keys(argTypes),
+ template: '<help-page-link v-bind="$props">link</help-page-link>',
+});
+
+export const Default = Template.bind({});
+Default.args = {
+ href: 'user/usage_quotas',
+};
+
+export const LinkWithAnAnchor = Template.bind({});
+LinkWithAnAnchor.args = {
+ ...Default.args,
+ anchor: 'namespace-storage-limit',
+};
+
+export const LinkWithAnchorInPath = Template.bind({});
+LinkWithAnchorInPath.args = {
+ ...Default.args,
+ href: 'user/usage_quotas#namespace-storage-limit',
+};
+
+export const CustomAttributes = Template.bind({});
+CustomAttributes.args = {
+ ...Default.args,
+ target: '_blank',
+};
diff --git a/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.vue b/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.vue
new file mode 100644
index 00000000000..11b269855ad
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/help_page_link/help_page_link.vue
@@ -0,0 +1,44 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import { helpPagePath } from '~/helpers/help_page_helper';
+
+/**
+ * Component to link to GitLab docs.
+ *
+ * @example
+ * <help-page-link href="user/usage_quotas">
+ * Usage Quotas help.
+ * <help-page-link>
+ */
+export default {
+ name: 'HelpPageLink',
+ components: {
+ GlLink,
+ },
+ props: {
+ href: {
+ type: String,
+ required: true,
+ },
+ anchor: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ computed: {
+ compiledHref() {
+ return helpPagePath(this.href, { anchor: this.anchor });
+ },
+ attributes() {
+ const { href, anchor, ...attrs } = this.$attrs;
+ return attrs;
+ },
+ },
+};
+</script>
+<template>
+ <gl-link v-bind="attributes" :href="compiledHref" v-on="$listeners">
+ <slot></slot>
+ </gl-link>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index cffd8471d18..525f684df86 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -382,6 +382,7 @@ export default {
@click="handleQuote"
/>
<toolbar-button
+ v-if="!restrictedToolBarItems.includes('code')"
v-show="!previewMarkdown"
tag="`"
tag-block="```"
diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
index 73c030b23dc..8fedc816502 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue
@@ -55,7 +55,7 @@ export default {
uploadsPath: {
type: String,
required: false,
- default: () => window.uploads_path,
+ default: () => window.uploads_path || '',
},
enableContentEditor: {
type: Boolean,
@@ -190,9 +190,9 @@ export default {
renderMarkdown(markdown) {
const url = setUrlParams(
{ render_quick_actions: this.supportsQuickActions },
- joinPaths(window.location.origin, gon.relative_url_root, this.renderMarkdownPath),
+ joinPaths(window.location.origin, this.renderMarkdownPath),
);
- return axios.post(url, { text: markdown }).then(({ data }) => data.body);
+ return axios.post(url, { text: markdown }).then(({ data }) => data.body || data.html);
},
onEditingModeChange(editingMode) {
this.editingMode = editingMode;
diff --git a/app/assets/javascripts/vue_shared/components/notes/system_note.vue b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
index 6a5884e4857..8f35fbdff6e 100644
--- a/app/assets/javascripts/vue_shared/components/notes/system_note.vue
+++ b/app/assets/javascripts/vue_shared/components/notes/system_note.vue
@@ -160,13 +160,12 @@ export default {
v-if="canSeeDescriptionVersion || note.outdated_line_change_path"
#extra-controls
>
- &middot;
<gl-button
v-if="canSeeDescriptionVersion"
variant="link"
:icon="descriptionVersionToggleIcon"
data-testid="compare-btn"
- class="gl-vertical-align-text-bottom gl-font-sm!"
+ class="gl-vertical-align-text-bottom gl-font-sm! gl-ml-3"
@click="toggleDescriptionVersion"
>{{ __('Compare with previous version') }}</gl-button
>
@@ -190,8 +189,11 @@ export default {
class="note-text md"
></div>
<div v-if="hasMoreCommits" class="flex-list">
- <div class="system-note-commit-list-toggler flex-row" @click="expanded = !expanded">
- <gl-icon :name="toggleIcon" :size="8" class="gl-mr-2" />
+ <div
+ class="system-note-commit-list-toggler flex-row gl-pl-4 gl-pt-3"
+ @click="expanded = !expanded"
+ >
+ <gl-icon :name="toggleIcon" :size="12" class="gl-mr-2" />
<span>{{ __('Toggle commit list') }}</span>
</div>
</div>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
index 3a4da54c84c..d06024638fa 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list.vue
@@ -35,6 +35,11 @@ export default {
required: false,
default: false,
},
+ listItemClass: {
+ type: [String, Array, Object],
+ required: false,
+ default: '',
+ },
},
};
</script>
@@ -46,6 +51,7 @@ export default {
:key="project.id"
:project="project"
:show-project-icon="showProjectIcon"
+ :class="listItemClass"
@delete="$emit('delete', $event)"
/>
</ul>
diff --git a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
index ce75e305473..3a077d09e40 100644
--- a/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
+++ b/app/assets/javascripts/vue_shared/components/projects_list/projects_list_item.vue
@@ -7,13 +7,13 @@ import {
GlTooltipDirective,
GlPopover,
GlSprintf,
+ GlTruncateText,
} from '@gitlab/ui';
import uniqueId from 'lodash/uniqueId';
import { VISIBILITY_TYPE_ICON, PROJECT_VISIBILITY_TYPE } from '~/visibility_level/constants';
import { ACCESS_LEVEL_LABELS } from '~/access_level/constants';
import { FEATURABLE_ENABLED } from '~/featurable/constants';
-import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { __ } from '~/locale';
import { numberToMetricPrefix } from '~/lib/utils/number_utils';
import { truncate } from '~/lib/utils/text_utility';
@@ -37,19 +37,18 @@ export default {
moreTopics: __('More topics'),
updated: __('Updated'),
actions: __('Actions'),
+ showMore: __('Show more'),
+ showLess: __('Show less'),
},
- avatarSize: { default: 32, md: 48 },
- safeHtmlConfig: {
- ADD_TAGS: ['gl-emoji'],
- },
+ truncateTextToggleButtonProps: { class: 'gl-font-sm!' },
components: {
GlAvatarLabeled,
GlIcon,
- UserAccessRoleBadge,
GlLink,
GlBadge,
GlPopover,
GlSprintf,
+ GlTruncateText,
TimeAgoTooltip,
DeleteModal,
ListActions,
@@ -203,21 +202,22 @@ export default {
</script>
<template>
- <li class="projects-list-item gl-py-5 gl-border-b gl-display-flex gl-align-items-flex-start">
- <div class="gl-md-display-flex gl-align-items-center gl-flex-grow-1">
+ <li class="projects-list-item gl-py-5 gl-border-b gl-display-flex">
+ <div class="gl-md-display-flex gl-flex-grow-1">
<div class="gl-display-flex gl-flex-grow-1">
- <gl-icon
+ <div
v-if="showProjectIcon"
- class="gl-mr-3 gl-mt-3 gl-md-mt-5 gl-flex-shrink-0 gl-text-secondary"
- name="project"
- />
+ class="gl-display-flex gl-align-items-center gl-flex-shrink-0 gl-h-9 gl-mr-3"
+ >
+ <gl-icon class="gl-text-secondary" name="project" />
+ </div>
<gl-avatar-labeled
:entity-id="project.id"
:entity-name="project.name"
:label="project.name"
:label-link="project.webUrl"
shape="rect"
- :size="$options.avatarSize"
+ :size="48"
>
<template #meta>
<div class="gl-px-2">
@@ -231,33 +231,46 @@ export default {
/>
</div>
<div class="gl-px-2">
- <user-access-role-badge v-if="shouldShowAccessLevel">{{
+ <gl-badge v-if="shouldShowAccessLevel" size="sm" class="gl-display-block">{{
accessLevelLabel
- }}</user-access-role-badge>
+ }}</gl-badge>
</div>
</div>
</div>
</template>
- <div
+ <gl-truncate-text
v-if="project.descriptionHtml"
- v-safe-html:[$options.safeHtmlConfig]="project.descriptionHtml"
- class="gl-font-sm gl-overflow-hidden gl-line-height-20 description md"
- data-testid="project-description"
- ></div>
+ :lines="2"
+ :mobile-lines="2"
+ :show-more-text="$options.i18n.showMore"
+ :show-less-text="$options.i18n.showLess"
+ :toggle-button-props="$options.truncateTextToggleButtonProps"
+ class="gl-mt-2 gl-max-w-88"
+ >
+ <div
+ v-safe-html="project.descriptionHtml"
+ class="gl-font-sm gl-text-secondary md"
+ data-testid="project-description"
+ ></div>
+ </gl-truncate-text>
<div v-if="hasTopics" class="gl-mt-3" data-testid="project-topics">
<div
class="gl-w-full gl-display-inline-flex gl-flex-wrap gl-font-base gl-font-weight-normal gl-align-items-center gl-mx-n2 gl-my-n2"
>
- <span class="gl-p-2 gl-text-secondary">{{ $options.i18n.topics }}:</span>
+ <span class="gl-p-2 gl-font-sm gl-text-secondary">{{ $options.i18n.topics }}:</span>
<div v-for="topic in visibleTopics" :key="topic" class="gl-p-2">
- <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ <gl-badge
+ v-gl-tooltip="topicTooltipTitle(topic)"
+ size="sm"
+ :href="topicPath(topic)"
+ >
{{ topicTitle(topic) }}
</gl-badge>
</div>
<template v-if="popoverTopics.length">
<div
:id="topicsPopoverTarget"
- class="gl-p-2 gl-text-secondary"
+ class="gl-p-2 gl-font-sm gl-text-secondary"
role="button"
tabindex="0"
>
@@ -272,7 +285,11 @@ export default {
:key="topic"
class="gl-p-2 gl-display-inline-block"
>
- <gl-badge v-gl-tooltip="topicTooltipTitle(topic)" :href="topicPath(topic)">
+ <gl-badge
+ v-gl-tooltip="topicTooltipTitle(topic)"
+ size="sm"
+ :href="topicPath(topic)"
+ >
{{ topicTitle(topic) }}
</gl-badge>
</div>
@@ -285,9 +302,9 @@ export default {
</div>
<div
class="gl-md-display-flex gl-flex-direction-column gl-align-items-flex-end gl-flex-shrink-0 gl-mt-3 gl-md-pl-0 gl-md-mt-0"
- :class="showProjectIcon ? 'gl-pl-11' : 'gl-pl-8'"
+ :class="showProjectIcon ? 'gl-pl-12' : 'gl-pl-10'"
>
- <div class="gl-display-flex gl-align-items-center gl-gap-x-3">
+ <div class="gl-display-flex gl-align-items-center gl-gap-x-3 gl-md-h-9">
<gl-badge v-if="project.archived" variant="warning">{{
$options.i18n.archived
}}</gl-badge>
@@ -323,19 +340,20 @@ export default {
</div>
<div
v-if="project.updatedAt"
- class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3"
+ class="gl-font-sm gl-white-space-nowrap gl-text-secondary gl-mt-3 gl-md-mt-0"
>
<span>{{ $options.i18n.updated }}</span>
<time-ago-tooltip :time="project.updatedAt" />
</div>
</div>
</div>
- <list-actions
- v-if="hasActions"
- class="gl-ml-3 gl-md-align-self-center"
- :actions="actions"
- :available-actions="project.availableActions"
- />
+ <div class="gl-display-flex gl-align-items-center gl-h-9 gl-ml-3">
+ <list-actions
+ v-if="hasActions"
+ :actions="actions"
+ :available-actions="project.availableActions"
+ />
+ </div>
<delete-modal
v-if="hasActionDelete"
diff --git a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue b/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
deleted file mode 100644
index 06852f511bf..00000000000
--- a/app/assets/javascripts/vue_shared/components/runner_instructions/runner_instructions.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { GlButton, GlModalDirective } from '@gitlab/ui';
-import { s__ } from '~/locale';
-import RunnerInstructionsModal from './runner_instructions_modal.vue';
-
-export default {
- components: {
- GlButton,
- RunnerInstructionsModal,
- },
- directives: {
- GlModal: GlModalDirective,
- },
- modalId: 'runner-instructions-modal',
- i18n: {
- buttonText: s__('Runners|Show runner installation instructions'),
- },
-};
-</script>
-<template>
- <div>
- <gl-button v-gl-modal="$options.modalId" class="gl-mt-4" data-testid="show-modal-button">
- {{ $options.i18n.buttonText }}
- </gl-button>
- <runner-instructions-modal :modal-id="$options.modalId" />
- </div>
-</template>
diff --git a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
index e0e8200580a..fff70d003b7 100644
--- a/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
+++ b/app/assets/javascripts/vue_shared/components/segmented_control_button_group.vue
@@ -43,6 +43,7 @@ export default {
:key="opt.value"
:disabled="!!opt.disabled"
:selected="value === opt.value"
+ v-bind="opt.props"
@click="$emit('input', opt.value)"
>
<slot name="button-content" v-bind="opt">{{ opt.text }}</slot>
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 c497224cde3..0067a0ca0d9 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
@@ -1,10 +1,16 @@
#import "~/graphql_shared/fragments/author.fragment.graphql"
-query getBlameData($fullPath: ID!, $filePath: String!, $fromLine: Int, $toLine: Int) {
+query getBlameData(
+ $fullPath: ID!
+ $filePath: String!
+ $fromLine: Int
+ $toLine: Int
+ $ref: String!
+) {
project(fullPath: $fullPath) {
id
repository {
- blobs(paths: [$filePath]) {
+ blobs(ref: $ref, paths: [$filePath]) {
nodes {
id
blame(fromLine: $fromLine, toLine: $toLine) {
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 bc46f11ab2d..e62f38d9ca3 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
@@ -118,6 +118,7 @@ export default {
const { data } = await this.$apollo.query({
query: blameDataQuery,
variables: {
+ ref: this.currentRef,
fullPath: this.projectPath,
filePath: this.blob.path,
fromLine: chunk.startingFrom + 1,
diff --git a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
index 6764ad4ce73..d4d241b12ec 100644
--- a/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
+++ b/app/assets/javascripts/vue_shared/components/timezone_dropdown/timezone_dropdown.vue
@@ -110,6 +110,7 @@ export default {
:no-results-text="$options.translations.noResultsText"
:selected="tzValue"
block
+ fluid-width
searchable
@search="setSearchTerm"
@select="selectTimezone"
diff --git a/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue
new file mode 100644
index 00000000000..944a48df279
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/upload_dropzone/avatar_upload_dropzone.vue
@@ -0,0 +1,112 @@
+<script>
+import { GlButton, GlAvatar, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { __ } from '~/locale';
+import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
+import { AVATAR_SHAPE_OPTION_RECT } from '~/vue_shared/constants';
+
+export default {
+ i18n: {
+ uploadText: __('Drop or %{linkStart}upload%{linkEnd} an avatar.'),
+ maxFileSize: __('Max file size is 200 KiB.'),
+ removeAvatar: __('Remove avatar'),
+ },
+ AVATAR_SHAPE_OPTION_RECT,
+ components: { GlButton, GlAvatar, GlSprintf, GlTruncate, UploadDropzone },
+ props: {
+ entity: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ value: {
+ type: [String, File],
+ required: false,
+ default: '',
+ },
+ label: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ avatarObjectUrl: null,
+ };
+ },
+ computed: {
+ avatarSrc() {
+ if (this.avatarObjectUrl) {
+ return this.avatarObjectUrl;
+ }
+
+ if (this.isValueAFile) {
+ return null;
+ }
+
+ return this.value;
+ },
+ isValueAFile() {
+ return this.value instanceof File;
+ },
+ },
+ watch: {
+ value(newValue) {
+ this.revokeAvatarObjectUrl();
+
+ if (newValue instanceof File) {
+ this.avatarObjectUrl = URL.createObjectURL(newValue);
+ } else {
+ this.avatarObjectUrl = null;
+ }
+ },
+ },
+ beforeDestroy() {
+ this.revokeAvatarObjectUrl();
+ },
+ methods: {
+ revokeAvatarObjectUrl() {
+ if (this.avatarObjectUrl === null) {
+ return;
+ }
+
+ URL.revokeObjectURL(this.avatarObjectUrl);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="gl-display-flex gl-column-gap-5">
+ <gl-avatar
+ :entity-id="entity.id || null"
+ :entity-name="entity.name || 'organization'"
+ :shape="$options.AVATAR_SHAPE_OPTION_RECT"
+ :size="96"
+ :src="avatarSrc"
+ />
+ <div class="gl-min-w-0">
+ <p class="gl-font-weight-bold gl-line-height-1 gl-mb-3">
+ {{ label }}
+ </p>
+ <div v-if="value" class="gl-display-flex gl-align-items-center gl-column-gap-3">
+ <gl-button @click="$emit('input', null)">{{ $options.i18n.removeAvatar }}</gl-button>
+ <gl-truncate
+ v-if="isValueAFile"
+ class="gl-text-secondary gl-max-w-48 gl-min-w-0"
+ position="middle"
+ :text="value.name"
+ />
+ </div>
+ <upload-dropzone v-else single-file-selection @change="$emit('input', $event)">
+ <template #upload-text>
+ <gl-sprintf :message="$options.i18n.uploadText">
+ <template #link="{ content }">
+ <span class="gl-link gl-hover-text-decoration-underline">{{ content }}</span>
+ </template>
+ </gl-sprintf>
+ </template>
+ </upload-dropzone>
+ <p class="gl-mb-0 gl-mt-3 gl-text-secondary">{{ $options.i18n.maxFileSize }}</p>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
index 863c43b0e55..a113a5ccc66 100644
--- a/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
+++ b/app/assets/javascripts/vue_shared/components/user_select/user_select.vue
@@ -252,6 +252,7 @@ export default {
selected.push(user);
this.$emit('input', selected);
}
+ this.clearAndFocusSearch();
},
unassign() {
this.$emit('input', []);
@@ -260,6 +261,7 @@ export default {
unselect(name) {
const selected = this.value.filter((user) => user.username !== name);
this.$emit('input', selected);
+ this.clearAndFocusSearch();
},
focusSearch() {
this.$refs.search.focusInput();
@@ -296,6 +298,10 @@ export default {
}
return user.canMerge ? '' : __('Cannot merge');
},
+ clearAndFocusSearch() {
+ this.search = '';
+ this.focusSearch();
+ },
},
};
</script>
diff --git a/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
index 5d86f90880d..5f197066cb5 100644
--- a/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
+++ b/app/assets/javascripts/vue_shared/components/users_table/user_avatar.vue
@@ -1,5 +1,6 @@
<script>
import { GlAvatarLabeled, GlBadge, GlIcon, GlTooltipDirective } from '@gitlab/ui';
+import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { truncate } from '~/lib/utils/text_utility';
import { USER_AVATAR_SIZE, LENGTH_OF_USER_NOTE_TOOLTIP } from './constants';
@@ -12,6 +13,7 @@ export default {
GlBadge,
GlIcon,
},
+ mixins: [glFeatureFlagsMixin()],
props: {
user: {
type: Object,
@@ -65,7 +67,12 @@ export default {
<div v-if="user.note" class="gl-text-gray-500 gl-p-1">
<gl-icon v-gl-tooltip="userNoteShort" name="document" />
</div>
- <div v-for="(badge, idx) in user.badges" :key="idx" class="gl-p-1">
+ <div
+ v-for="(badge, idx) in user.badges"
+ :key="idx"
+ class="gl-p-1"
+ :class="{ 'gl-pb-0': glFeatures.simplifiedBadges }"
+ >
<gl-badge class="gl-display-flex!" size="sm" :variant="badge.variant">{{
badge.text
}}</gl-badge>
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 3514a9c2d5d..1a3e5208508 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -11,6 +11,7 @@ import { s__, __ } from '~/locale';
import { visitUrl } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import ConfirmForkModal from '~/vue_shared/components/web_ide/confirm_fork_modal.vue';
+import { GO_TO_PROJECT_WEBIDE } from '~/behaviors/shortcuts/keybindings';
import { KEY_EDIT, KEY_WEB_IDE, KEY_GITPOD, KEY_PIPELINE_EDITOR } from './constants';
export const i18n = {
@@ -197,6 +198,9 @@ export default {
...handleOptions,
};
},
+ webIdeActionShortcutKey() {
+ return GO_TO_PROJECT_WEBIDE.defaultKeys[0];
+ },
webIdeActionText() {
if (this.webIdeText) {
return this.webIdeText;
@@ -234,6 +238,7 @@ export default {
key: KEY_WEB_IDE,
text: this.webIdeActionText,
secondaryText: this.$options.i18n.webIdeText,
+ shortcut: this.webIdeActionShortcutKey,
tracking: {
action: TRACKING_ACTION_NAME,
label: 'web_ide',
@@ -357,9 +362,14 @@ export default {
>
<template #list-item>
<div class="gl-display-flex gl-flex-direction-column">
- <span data-testid="action-primary-text" class="gl-font-weight-bold gl-mb-2">{{
- action.text
- }}</span>
+ <span
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-mb-2"
+ >
+ <span data-testid="action-primary-text" class="gl-font-weight-bold">{{
+ action.text
+ }}</span>
+ <kbd v-if="action.shortcut" class="flat">{{ action.shortcut }}</kbd>
+ </span>
<span data-testid="action-secondary-text" class="gl-font-sm gl-text-secondary">
{{ action.secondaryText }}
</span>
diff --git a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
index bb36df0a778..de2f7887237 100644
--- a/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
+++ b/app/assets/javascripts/vue_shared/issuable/list/components/issuable_item.vue
@@ -1,19 +1,27 @@
<script>
-import { GlLink, GlIcon, GlLabel, GlFormCheckbox, GlSprintf, GlTooltipDirective } from '@gitlab/ui';
-
+import {
+ GlBadge,
+ GlLink,
+ GlIcon,
+ GlLabel,
+ GlFormCheckbox,
+ GlSprintf,
+ GlTooltipDirective,
+} from '@gitlab/ui';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { STATUS_CLOSED } from '~/issues/constants';
+import { STATUS_OPEN, STATUS_CLOSED } from '~/issues/constants';
import { isScopedLabel } from '~/lib/utils/common_utils';
import { isExternal, setUrlFragment } from '~/lib/utils/url_utility';
import { __, n__, sprintf } from '~/locale';
import IssuableAssignees from '~/issuable/components/issue_assignees.vue';
import timeagoMixin from '~/vue_shared/mixins/timeago';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import { STATE_CLOSED } from '~/work_items/constants';
+import { STATE_OPEN, STATE_CLOSED } from '~/work_items/constants';
import { isAssigneesWidget, isLabelsWidget } from '~/work_items/utils';
export default {
components: {
+ GlBadge,
GlLink,
GlIcon,
GlLabel,
@@ -120,8 +128,11 @@ export default {
createdAt() {
return this.timeFormatted(this.issuable.createdAt);
},
+ isNotOpen() {
+ return ![STATUS_OPEN, STATE_OPEN].includes(this.issuable.state);
+ },
isClosed() {
- return this.issuable.state === STATUS_CLOSED || this.issuable.state === STATE_CLOSED;
+ return [STATUS_CLOSED, STATE_CLOSED].includes(this.issuable.state);
},
timestamp() {
return this.isClosed && this.issuable.closedAt
@@ -351,8 +362,12 @@ export default {
</div>
<div class="issuable-meta">
<ul v-if="showIssuableMeta" class="controls">
- <li v-if="hasSlotContents('status')">
- <slot name="status"></slot>
+ <!-- eslint-disable-next-line @gitlab/vue-prefer-dollar-scopedslots -->
+ <li v-if="$slots.status" data-testid="issuable-status">
+ <gl-badge v-if="isNotOpen" size="sm" variant="info">
+ <slot name="status"></slot>
+ </gl-badge>
+ <slot v-else name="status"></slot>
</li>
<li v-if="assignees.length">
<issuable-assignees
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 ad908a674d3..dd203283b3b 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
@@ -406,9 +406,7 @@ export default {
<slot name="timeframe" :issuable="issuable"></slot>
</template>
<template #status>
- <gl-badge size="sm" variant="info">
- <slot name="status" :issuable="issuable"></slot>
- </gl-badge>
+ <slot name="status" :issuable="issuable"></slot>
</template>
<template #statistics>
<slot name="statistics" :issuable="issuable"></slot>
diff --git a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
index 1e6bd9ff1ac..9ede327b2f1 100644
--- a/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
+++ b/app/assets/javascripts/work_items/components/notes/work_item_comment_form.vue
@@ -235,7 +235,9 @@ export default {
:work-item-id="workItemId"
:work-item-state="workItemState"
:work-item-type="workItemType"
+ :has-comment="!!commentText.length"
can-update
+ @submit-comment="$emit('submitForm', { commentText, isNoteInternal })"
@error="$emit('error', $event)"
/>
<gl-button
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 503328f7b03..78afb9a04ef 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
@@ -131,7 +131,7 @@ export default {
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"
>
- <div class="item-contents gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
+ <div class="gl-display-flex gl-flex-grow-1 gl-flex-wrap gl-min-w-0">
<div
class="gl-display-flex gl-flex-grow-1 gl-flex-wrap flex-xl-nowrap gl-align-items-center gl-justify-content-space-between gl-gap-3 gl-min-w-0"
>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue
new file mode 100644
index 00000000000..53149f62893
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue
@@ -0,0 +1,194 @@
+<script>
+import { GlButton, GlForm, GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
+import { isEmpty } from 'lodash';
+
+import { s__, __ } from '~/locale';
+
+export default {
+ i18n: {
+ none: s__('WorkItem|None'),
+ noMatchingResults: s__('WorkItem|No matching results'),
+ editButtonLabel: __('Edit'),
+ applyButtonLabel: __('Apply'),
+ resetButtonText: __('Clear'),
+ },
+ components: {
+ GlButton,
+ GlLoadingIcon,
+ GlForm,
+ GlCollapsibleListbox,
+ },
+ props: {
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ dropdownLabel: {
+ type: String,
+ required: true,
+ },
+ dropdownName: {
+ type: String,
+ required: true,
+ },
+ listItems: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ itemValue: {
+ type: Object,
+ required: false,
+ default: null,
+ },
+ loading: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ resetButtonLabel: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ headerText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ toggleDropdownText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ },
+ data() {
+ return {
+ isEditing: false,
+ localSelectedItem: this.itemValue?.id,
+ };
+ },
+ computed: {
+ hasValue() {
+ return this.itemValue != null || !isEmpty(this.item);
+ },
+ listboxText() {
+ return (
+ this.listItems.find(({ value }) => this.localSelectedItem === value)?.text ||
+ this.itemValue?.title ||
+ this.$options.i18n.none
+ );
+ },
+ inputId() {
+ return `work-item-dropdown-listbox-value-${this.dropdownName}`;
+ },
+ toggleText() {
+ return this.toggleDropdownText || this.listboxText;
+ },
+ resetButton() {
+ return this.resetButtonLabel || this.$options.i18n.resetButtonText;
+ },
+ },
+ watch: {
+ itemValue: {
+ handler(newVal) {
+ if (!this.isEditing) {
+ this.localSelectedItem = newVal?.id;
+ }
+ },
+ },
+ },
+ methods: {
+ setSearchKey(value) {
+ this.$emit('searchStarted', value);
+ },
+ handleItemClick(item) {
+ this.localSelectedItem = item;
+ this.$emit('updateValue', item);
+ },
+ onListboxShown() {
+ this.$emit('dropdownShown');
+ },
+ onListboxHide() {
+ this.isEditing = false;
+ },
+ unassignValue() {
+ this.localSelectedItem = null;
+ this.isEditing = false;
+ this.$emit('updateValue', null);
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div class="gl-display-flex gl-align-items-center gl-gap-3">
+ <!-- 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">
+ {{ dropdownLabel }}
+ </h3>
+ <gl-loading-icon v-if="updateInProgress" />
+ <gl-button
+ v-if="canUpdate && !isEditing"
+ data-testid="edit-button"
+ category="tertiary"
+ size="small"
+ class="gl-ml-auto gl-mr-2"
+ :disabled="updateInProgress"
+ @click="isEditing = true"
+ >{{ $options.i18n.editButtonLabel }}</gl-button
+ >
+ </div>
+ <gl-form v-if="isEditing">
+ <div class="gl-display-flex gl-justify-content-space-between gl-align-items-center">
+ <label :for="inputId" class="gl-mb-0">{{ dropdownLabel }}</label>
+ <gl-button
+ data-testid="apply-button"
+ category="tertiary"
+ size="small"
+ class="gl-mr-2"
+ :disabled="updateInProgress"
+ @click="isEditing = false"
+ >{{ $options.i18n.applyButtonLabel }}</gl-button
+ >
+ </div>
+ <gl-collapsible-listbox
+ :id="inputId"
+ block
+ searchable
+ start-opened
+ is-check-centered
+ fluid-width
+ :searching="loading"
+ :header-text="headerText"
+ :toggle-text="toggleText"
+ :no-results-text="$options.i18n.noMatchingResults"
+ :items="listItems"
+ :selected="localSelectedItem"
+ :reset-button-label="resetButton"
+ @reset="unassignValue"
+ @search="setSearchKey"
+ @select="handleItemClick"
+ @shown="onListboxShown"
+ @hidden="onListboxHide"
+ >
+ <template #list-item="{ item }">
+ <slot name="list-item" :item="item">{{ item.text }}</slot>
+ </template>
+ </gl-collapsible-listbox>
+ </gl-form>
+ <slot v-else-if="hasValue" name="readonly">
+ {{ listboxText }}
+ </slot>
+ <div v-else class="gl-text-secondary">
+ {{ $options.i18n.none }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
index c122db6c902..719507d1341 100644
--- a/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
+++ b/app/assets/javascripts/work_items/components/shared/work_item_token_input.vue
@@ -1,21 +1,25 @@
<script>
import { GlTokenSelector, GlAlert } from '@gitlab/ui';
import { debounce } from 'lodash';
-
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { isNumeric } from '~/lib/utils/number_utils';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import SafeHtml from '~/vue_shared/directives/safe_html';
+import { isSafeURL } from '~/lib/utils/url_utility';
+
import { highlighter } from 'ee_else_ce/gfm_auto_complete';
import groupWorkItemsQuery from '../../graphql/group_work_items.query.graphql';
import projectWorkItemsQuery from '../../graphql/project_work_items.query.graphql';
+import workItemsByReferencesQuery from '../../graphql/work_items_by_references.query.graphql';
import {
WORK_ITEMS_TYPE_MAP,
I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
I18N_WORK_ITEM_SEARCH_ERROR,
+ I18N_WORK_ITEM_NO_MATCHES_FOUND,
sprintfWorkItem,
} from '../../constants';
+import { isReference } from '../../utils';
export default {
components: {
@@ -55,41 +59,63 @@ export default {
},
},
apollo: {
- availableWorkItems: {
+ workspaceWorkItems: {
query() {
return this.isGroup ? groupWorkItemsQuery : projectWorkItemsQuery;
},
variables() {
- return {
- fullPath: this.fullPath,
- searchTerm: '',
- types: this.childrenType ? [this.childrenType] : [],
- isNumber: false,
- };
+ return this.queryVariables;
},
skip() {
- return !this.searchStarted;
+ return !this.searchStarted || this.isSearchingByReference;
},
update(data) {
return [
...this.filterItems(data.workspace.workItemsByIid?.nodes),
- ...this.filterItems(data.workspace.workItems.nodes),
+ ...this.filterItems(data.workspace.workItems?.nodes),
];
},
error() {
this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName);
},
},
+ workItemsByReference: {
+ query: workItemsByReferencesQuery,
+ variables() {
+ return {
+ contextNamespacePath: this.fullPath,
+ refs: [this.searchTerm],
+ };
+ },
+ skip() {
+ return !this.isSearchingByReference;
+ },
+ update(data) {
+ return data.workItemsByReference.nodes;
+ },
+ error() {
+ this.error = sprintfWorkItem(I18N_WORK_ITEM_SEARCH_ERROR, this.childrenTypeName);
+ },
+ },
},
data() {
return {
- availableWorkItems: [],
- query: '',
+ workspaceWorkItems: [],
+ searchTerm: '',
searchStarted: false,
error: '',
+ textInputAttrs: {
+ class: 'gl-min-w-fit-content!',
+ },
};
},
computed: {
+ availableWorkItems() {
+ return this.isSearchingByReference ? this.workItemsByReference : this.workspaceWorkItems;
+ },
+ isSearchingByReference() {
+ return isReference(this.searchTerm) || isSafeURL(this.searchTerm);
+ },
workItemsToAdd: {
get() {
return this.value;
@@ -99,10 +125,10 @@ export default {
},
},
isLoading() {
- return this.$apollo.queries.availableWorkItems.loading;
- },
- addInputPlaceholder() {
- return sprintfWorkItem(I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER, this.childrenTypeName);
+ return (
+ this.$apollo.queries.workspaceWorkItems.loading ||
+ this.$apollo.queries.workItemsByReference.loading
+ );
},
childrenTypeName() {
return WORK_ITEMS_TYPE_MAP[this.childrenType]?.name;
@@ -110,31 +136,25 @@ export default {
tokenSelectorContainerClass() {
return !this.areWorkItemsToAddValid ? 'gl-inset-border-1-red-500!' : '';
},
+ queryVariables() {
+ return {
+ fullPath: this.fullPath,
+ searchTerm: this.searchTerm,
+ types: this.childrenType ? [this.childrenType] : [],
+ in: this.searchTerm ? 'TITLE' : undefined,
+ iid: isNumeric(this.searchTerm) ? this.searchTerm : null,
+ searchByIid: isNumeric(this.searchTerm),
+ searchByText: true,
+ };
+ },
},
created() {
this.debouncedSearchKeyUpdate = debounce(this.setSearchKey, DEFAULT_DEBOUNCE_AND_THROTTLE_MS);
},
methods: {
getIdFromGraphQLId,
- setSearchKey(value) {
- this.query = value;
-
- // Query parameters for searching by text
- const variables = {
- searchTerm: value,
- in: value ? 'TITLE' : undefined,
- iid: null,
- isNumber: false,
- };
-
- // Check if it is a number, add iid as query parameter
- if (isNumeric(value) && value) {
- variables.iid = value;
- variables.isNumber = true;
- }
-
- // Fetch combined results of search by iid and search by title.
- this.$apollo.queries.availableWorkItems.refetch(variables);
+ async setSearchKey(value) {
+ this.searchTerm = value;
},
handleFocus() {
this.searchStarted = true;
@@ -154,16 +174,16 @@ export default {
focusInputText() {
this.$nextTick(() => {
if (this.areWorkItemsToAddValid) {
- this.$refs.tokenSelector.$el.querySelector('input[type="text"]').focus();
+ this.$refs.tokenSelector.focusTextInput();
}
});
},
formatResults(input) {
- if (!this.query) {
+ if (!this.searchTerm) {
return input;
}
- return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.query);
+ return highlighter(`<span class="gl-text-black-normal">${input}</span>`, this.searchTerm);
},
unsetError() {
this.error = '';
@@ -176,6 +196,10 @@ export default {
);
},
},
+ i18n: {
+ noMatchesFoundMessage: I18N_WORK_ITEM_NO_MATCHES_FOUND,
+ addInputPlaceholder: I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER,
+ },
};
</script>
<template>
@@ -188,10 +212,11 @@ export default {
v-model="workItemsToAdd"
:dropdown-items="availableWorkItems"
:loading="isLoading"
- :placeholder="addInputPlaceholder"
+ :placeholder="$options.i18n.addInputPlaceholder"
menu-class="gl-dropdown-menu-wide dropdown-reduced-height gl-min-h-7!"
:container-class="tokenSelectorContainerClass"
data-testid="work-item-token-select-input"
+ :text-input-attrs="textInputAttrs"
@text-input="debouncedSearchKeyUpdate"
@focus="handleFocus"
@mouseover.native="handleMouseOver"
@@ -210,6 +235,11 @@ export default {
<div v-safe-html="formatResults(dropdownItem.title)" class="gl-text-truncate"></div>
</div>
</template>
+ <template #no-results-content>
+ <span data-testid="no-match-found-namespace-message">{{
+ $options.i18n.noMatchesFoundMessage
+ }}</span>
+ </template>
</gl-token-selector>
</div>
</template>
diff --git a/app/assets/javascripts/work_items/components/widget_wrapper.vue b/app/assets/javascripts/work_items/components/widget_wrapper.vue
index 27de858fe4e..6feae8dd94e 100644
--- a/app/assets/javascripts/work_items/components/widget_wrapper.vue
+++ b/app/assets/javascripts/work_items/components/widget_wrapper.vue
@@ -57,28 +57,31 @@ export default {
</script>
<template>
- <div :id="widgetName" class="gl-new-card" :aria-expanded="isOpenString">
+ <div :id="widgetName" class="gl-new-card">
<div class="gl-new-card-header">
<div class="gl-new-card-title-wrapper">
- <h3 class="gl-new-card-title">
- <gl-link
- :id="anchorLinkId"
- class="gl-text-decoration-none"
- :href="anchorLink"
- aria-hidden="true"
- />
+ <h2 class="gl-new-card-title">
+ <div aria-hidden="true">
+ <gl-link
+ :id="anchorLinkId"
+ class="gl-text-decoration-none gl-display-none"
+ :href="anchorLink"
+ />
+ </div>
<slot name="header"></slot>
- </h3>
+ </h2>
<slot name="header-suffix"></slot>
</div>
<slot name="header-right"></slot>
<div class="gl-new-card-toggle">
+ <!-- https://www.w3.org/TR/wai-aria-1.2/#aria-expanded -->
<gl-button
category="tertiary"
size="small"
:icon="toggleIcon"
:aria-label="toggleLabel"
data-testid="widget-toggle"
+ :aria-expanded="isOpenString"
@click="toggle"
/>
</div>
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 b7206d502a6..79f0fdca061 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
@@ -17,14 +17,16 @@ import {
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 WorkItemMilestoneInline from './work_item_milestone_inline.vue';
+import WorkItemMilestoneWithEdit from './work_item_milestone_with_edit.vue';
import WorkItemParentInline from './work_item_parent_inline.vue';
import WorkItemParent from './work_item_parent_with_edit.vue';
export default {
components: {
WorkItemLabels,
- WorkItemMilestone,
+ WorkItemMilestoneInline,
+ WorkItemMilestoneWithEdit,
WorkItemAssignees,
WorkItemDueDate,
WorkItemParent,
@@ -34,7 +36,10 @@ export default {
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'),
+ WorkItemIterationInline: () =>
+ import('ee_component/work_items/components/work_item_iteration_inline.vue'),
+ WorkItemIteration: () =>
+ import('ee_component/work_items/components/work_item_iteration_with_edit.vue'),
WorkItemHealthStatus: () =>
import('ee_component/work_items/components/work_item_health_status_with_edit.vue'),
WorkItemHealthStatusInline: () =>
@@ -137,15 +142,28 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
- <work-item-milestone
- v-if="workItemMilestone"
- :full-path="fullPath"
- :work-item-id="workItem.id"
- :work-item-milestone="workItemMilestone.milestone"
- :work-item-type="workItemType"
- :can-update="canUpdate"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemMilestone">
+ <work-item-milestone-with-edit
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :can-update="canUpdate"
+ @error="$emit('error', $event)"
+ />
+ <work-item-milestone-inline
+ v-else
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :work-item-id="workItem.id"
+ :work-item-milestone="workItemMilestone.milestone"
+ :work-item-type="workItemType"
+ :can-update="canUpdate"
+ @error="$emit('error', $event)"
+ />
+ </template>
<template v-if="workItemWeight">
<work-item-weight
v-if="glFeatures.workItemsMvc2"
@@ -177,17 +195,30 @@ export default {
:work-item-type="workItemType"
@error="$emit('error', $event)"
/>
- <work-item-iteration
- v-if="workItemIteration"
- class="gl-mb-5"
- :full-path="fullPath"
- :iteration="workItemIteration.iteration"
- :can-update="canUpdate"
- :work-item-id="workItem.id"
- :work-item-iid="workItem.iid"
- :work-item-type="workItemType"
- @error="$emit('error', $event)"
- />
+ <template v-if="workItemIteration">
+ <work-item-iteration
+ v-if="glFeatures.workItemsMvc2"
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :iteration="workItemIteration.iteration"
+ :can-update="canUpdate"
+ :work-item-id="workItem.id"
+ :work-item-iid="workItem.iid"
+ :work-item-type="workItemType"
+ @error="$emit('error', $event)"
+ />
+ <work-item-iteration-inline
+ v-else
+ class="gl-mb-5"
+ :full-path="fullPath"
+ :iteration="workItemIteration.iteration"
+ :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="workItemHealthStatus">
<work-item-health-status
v-if="glFeatures.workItemsMvc2"
diff --git a/app/assets/javascripts/work_items/components/work_item_description.vue b/app/assets/javascripts/work_items/components/work_item_description.vue
index 77c573b47e4..4301dcca30b 100644
--- a/app/assets/javascripts/work_items/components/work_item_description.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description.vue
@@ -40,12 +40,27 @@ export default {
type: String,
required: true,
},
+ disableInlineEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ editMode: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ updateInProgress: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
markdownDocsPath: helpPagePath('user/markdown'),
data() {
return {
workItem: {},
- isEditing: false,
+ isEditing: this.editMode,
isSubmitting: false,
isSubmittingWithKeydown: false,
descriptionText: '',
@@ -126,6 +141,26 @@ export default {
autocompleteDataSources() {
return autocompleteDataSources(this.fullPath, this.workItem.iid);
},
+ saveButtonText() {
+ return this.editMode ? __('Save changes') : __('Save');
+ },
+ formGroupClass() {
+ return {
+ 'gl-border-t gl-pt-6': !this.disableInlineEditing,
+ 'gl-mb-5 common-note-form': true,
+ };
+ },
+ },
+ watch: {
+ updateInProgress(newValue) {
+ this.isSubmitting = newValue;
+ },
+ editMode(newValue) {
+ this.isEditing = newValue;
+ if (newValue) {
+ this.startEditing();
+ }
+ },
},
methods: {
checkForConflicts() {
@@ -159,6 +194,7 @@ export default {
}
this.isEditing = false;
+ this.$emit('cancelEditing');
clearDraft(this.autosaveKey);
},
onInput() {
@@ -175,6 +211,11 @@ export default {
this.isSubmittingWithKeydown = true;
}
+ if (this.disableInlineEditing) {
+ this.$emit('updateWorkItem');
+ return;
+ }
+
this.isSubmitting = true;
try {
@@ -210,6 +251,9 @@ export default {
},
setDescriptionText(newText) {
this.descriptionText = newText;
+ if (this.disableInlineEditing) {
+ this.$emit('updateDraft', this.descriptionText);
+ }
updateDraft(this.autosaveKey, this.descriptionText);
},
handleDescriptionTextUpdated(newText) {
@@ -224,12 +268,13 @@ export default {
<div>
<gl-form v-if="isEditing" @submit.prevent="updateWorkItem" @reset.prevent="cancelEditing">
<gl-form-group
- class="gl-mb-5 gl-border-t gl-pt-6 common-note-form"
+ :class="formGroupClass"
:label="__('Description')"
+ :label-sr-only="disableInlineEditing"
label-for="work-item-description"
>
<markdown-editor
- class="gl-my-5"
+ class="gl-mb-5"
:value="descriptionText"
:render-markdown-path="markdownPreviewPath"
:markdown-docs-path="$options.markdownDocsPath"
@@ -285,9 +330,9 @@ export default {
:loading="isSubmitting"
data-testid="save-description"
type="submit"
- >{{ __('Save') }}
+ >{{ saveButtonText }}
</gl-button>
- <gl-button category="tertiary" class="gl-ml-3" data-testid="cancel" type="reset"
+ <gl-button category="secondary" class="gl-ml-3" data-testid="cancel" type="reset"
>{{ __('Cancel') }}
</gl-button>
</template>
@@ -296,13 +341,14 @@ export default {
</gl-form>
<work-item-description-rendered
v-else
+ :disable-inline-editing="disableInlineEditing"
:work-item-description="workItemDescription"
:can-edit="canEdit"
@startEditing="startEditing"
@descriptionUpdated="handleDescriptionTextUpdated"
/>
<edited-at
- v-if="lastEditedAt"
+ v-if="lastEditedAt && !editMode"
:updated-at="lastEditedAt"
:updated-by-name="lastEditedByName"
:updated-by-path="lastEditedByPath"
diff --git a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
index 124e05db431..1699f6c419e 100644
--- a/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
+++ b/app/assets/javascripts/work_items/components/work_item_description_rendered.vue
@@ -22,6 +22,16 @@ export default {
type: Boolean,
required: true,
},
+ disableInlineEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ checkboxes: [],
+ };
},
computed: {
descriptionText() {
@@ -33,6 +43,12 @@ export default {
descriptionEmpty() {
return this.descriptionHtml?.trim() === '';
},
+ showEmptyDescription() {
+ return this.descriptionEmpty && !this.disableInlineEditing;
+ },
+ showEditButton() {
+ return this.canEdit && !this.disableInlineEditing;
+ },
},
watch: {
descriptionHtml: {
@@ -96,9 +112,11 @@ export default {
<template>
<div class="gl-mb-5">
<div class="gl-display-inline-flex gl-align-items-center gl-mb-3">
- <label class="d-block col-form-label gl-mr-5">{{ __('Description') }}</label>
+ <label v-if="!disableInlineEditing" class="d-block col-form-label gl-mr-5">{{
+ __('Description')
+ }}</label>
<gl-button
- v-if="canEdit"
+ v-if="showEditButton"
v-gl-tooltip
class="gl-ml-auto"
icon="pencil"
@@ -109,9 +127,9 @@ export default {
/>
</div>
- <div v-if="descriptionEmpty" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
+ <div v-if="showEmptyDescription" class="gl-text-secondary gl-mb-5">{{ __('None') }}</div>
<div
- v-else
+ v-else-if="!descriptionEmpty"
ref="gfm-content"
v-safe-html="descriptionHtml"
class="md gl-mb-5 gl-min-h-8 gl-clearfix"
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 b74cbc85379..85b981d9370 100644
--- a/app/assets/javascripts/work_items/components/work_item_detail.vue
+++ b/app/assets/javascripts/work_items/components/work_item_detail.vue
@@ -2,6 +2,7 @@
import { isEmpty } from 'lodash';
import { GlAlert, GlSkeletonLoader, GlButton, GlTooltipDirective, GlEmptyState } from '@gitlab/ui';
import noAccessSvg from '@gitlab/svgs/dist/illustrations/analytics/no-access.svg?raw';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
import { s__ } from '~/locale';
import { getParameterByName, updateHistory, setUrlParams } from '~/lib/utils/url_utility';
import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
@@ -18,6 +19,7 @@ import {
WIDGET_TYPE_AWARD_EMOJI,
WIDGET_TYPE_HIERARCHY,
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
+ WORK_ITEM_TYPE_VALUE_EPIC,
WIDGET_TYPE_NOTES,
WIDGET_TYPE_LINKED_ITEMS,
} from '../constants';
@@ -41,6 +43,7 @@ import WorkItemAwardEmoji from './work_item_award_emoji.vue';
import WorkItemRelationships from './work_item_relationships/work_item_relationships.vue';
import WorkItemStickyHeader from './work_item_sticky_header.vue';
import WorkItemAncestors from './work_item_ancestors/work_item_ancestors.vue';
+import WorkItemTitleWithEdit from './work_item_title_with_edit.vue';
export default {
i18n,
@@ -67,6 +70,7 @@ export default {
WorkItemRelationships,
WorkItemStickyHeader,
WorkItemAncestors,
+ WorkItemTitleWithEdit,
},
mixins: [glFeatureFlagMixin()],
inject: ['fullPath', 'isGroup', 'reportAbusePath'],
@@ -94,6 +98,8 @@ export default {
reportedUrl: '',
reportedUserId: 0,
isStickyHeaderShowing: false,
+ editMode: false,
+ draftData: {},
};
},
apollo: {
@@ -219,7 +225,7 @@ export default {
};
},
showIntersectionObserver() {
- return !this.isModal && this.workItemsMvc2Enabled;
+ return !this.isModal && this.workItemsMvc2Enabled && !this.editMode;
},
hasLinkedWorkItems() {
return this.glFeatures.linkedWorkItems;
@@ -227,19 +233,26 @@ export default {
workItemLinkedItems() {
return this.isWidgetPresent(WIDGET_TYPE_LINKED_ITEMS);
},
+ showWorkItemTree() {
+ return [WORK_ITEM_TYPE_VALUE_OBJECTIVE, WORK_ITEM_TYPE_VALUE_EPIC].includes(
+ this.workItemType,
+ );
+ },
showWorkItemLinkedItems() {
return this.hasLinkedWorkItems && this.workItemLinkedItems;
},
titleClassHeader() {
return {
'gl-sm-display-none!': this.parentWorkItem,
- 'gl-w-full': !this.parentWorkItem,
+ 'gl-w-full': !this.parentWorkItem && !this.editMode,
+ 'editable-wi-title': this.editMode && !this.parentWorkItem,
};
},
titleClassComponent() {
return {
'gl-sm-display-block!': !this.parentWorkItem,
'gl-display-none gl-sm-display-block!': this.parentWorkItem,
+ 'gl-mt-3 editable-wi-title': this.workItemsMvc2Enabled,
};
},
headerWrapperClass() {
@@ -258,6 +271,9 @@ export default {
}
},
methods: {
+ enableEditMode() {
+ this.editMode = true;
+ },
isWidgetPresent(type) {
return this.workItem.widgets?.find((widget) => widget.type === type);
},
@@ -349,6 +365,45 @@ export default {
this.isStickyHeaderShowing = true;
}
},
+ updateDraft(type, value) {
+ this.draftData[type] = value;
+ },
+ async updateWorkItem() {
+ this.updateInProgress = true;
+ try {
+ const {
+ data: { workItemUpdate },
+ } = await this.$apollo.mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItem.id,
+ title: this.draftData.title,
+ descriptionWidget: {
+ description: this.draftData.description,
+ },
+ },
+ },
+ });
+
+ const { errors } = workItemUpdate;
+
+ if (errors?.length) {
+ this.updateError = errors.join('\n');
+ throw new Error(this.updateError);
+ }
+
+ this.editMode = false;
+ } catch (error) {
+ Sentry.captureException(error);
+ } finally {
+ this.updateInProgress = false;
+ }
+ },
+ cancelEditing() {
+ this.draftData = {};
+ this.editMode = false;
+ },
},
WORK_ITEM_TYPE_VALUE_OBJECTIVE,
WORKSPACE_PROJECT,
@@ -388,8 +443,17 @@ export default {
:class="titleClassHeader"
data-testid="work-item-type"
>
+ <work-item-title-with-edit
+ v-if="workItem.title && workItemsMvc2Enabled"
+ ref="title"
+ class="gl-mt-3 gl-sm-display-block!"
+ :is-editing="editMode"
+ :title="workItem.title"
+ @updateWorkItem="updateWorkItem"
+ @updateDraft="updateDraft('title', $event)"
+ />
<work-item-title
- v-if="workItem.title"
+ v-else-if="workItem.title"
ref="title"
class="gl-sm-display-block!"
:work-item-id="workItem.id"
@@ -402,6 +466,14 @@ export default {
<div
class="detail-page-header-actions gl-display-flex gl-align-self-start gl-ml-auto gl-gap-3"
>
+ <gl-button
+ v-if="workItemsMvc2Enabled && !editMode"
+ category="secondary"
+ data-testid="work-item-edit-form-button"
+ @click="enableEditMode"
+ >
+ {{ __('Edit') }}
+ </gl-button>
<work-item-todos
v-if="showWorkItemCurrentUserTodos"
:work-item-id="workItem.id"
@@ -441,8 +513,17 @@ export default {
/>
</div>
<div>
+ <work-item-title-with-edit
+ v-if="workItem.title && workItemsMvc2Enabled && parentWorkItem"
+ ref="title"
+ :is-editing="editMode"
+ :class="titleClassComponent"
+ :title="workItem.title"
+ @updateWorkItem="updateWorkItem"
+ @updateDraft="updateDraft('title', $event)"
+ />
<work-item-title
- v-if="workItem.title && parentWorkItem"
+ v-else-if="workItem.title && parentWorkItem"
ref="title"
:class="titleClassComponent"
:work-item-id="workItem.id"
@@ -453,6 +534,7 @@ export default {
@error="updateError = $event"
/>
<work-item-created-updated
+ v-if="!editMode"
:full-path="fullPath"
:work-item-iid="workItemIid"
:update-in-progress="updateInProgress"
@@ -490,10 +572,16 @@ export default {
/>
<work-item-description
v-if="hasDescriptionWidget"
+ :class="workItemsMvc2Enabled ? '' : 'gl-pt-5'"
+ :disable-inline-editing="workItemsMvc2Enabled"
+ :edit-mode="editMode"
:full-path="fullPath"
:work-item-id="workItem.id"
:work-item-iid="workItem.iid"
- class="gl-pt-5"
+ :update-in-progress="updateInProgress"
+ @updateWorkItem="updateWorkItem"
+ @updateDraft="updateDraft('description', $event)"
+ @cancelEditing="cancelEditing"
@error="updateError = $event"
/>
<work-item-award-emoji
@@ -506,7 +594,7 @@ export default {
@emoji-updated="$emit('work-item-emoji-updated', $event)"
/>
<work-item-tree
- v-if="workItemType === $options.WORK_ITEM_TYPE_VALUE_OBJECTIVE"
+ v-if="showWorkItemTree"
:full-path="fullPath"
:work-item-type="workItemType"
:parent-work-item-type="workItem.workItemType.name"
diff --git a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
index f24b56cac36..cc46932539d 100644
--- a/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_links/work_item_links_form.vue
@@ -11,6 +11,7 @@ import {
import { __, s__, sprintf } from '~/locale';
import WorkItemTokenInput from '../shared/work_item_token_input.vue';
import { addHierarchyChild } from '../../graphql/cache_utils';
+import groupWorkItemTypesQuery from '../../graphql/group_work_item_types.query.graphql';
import projectWorkItemTypesQuery from '../../graphql/project_work_item_types.query.graphql';
import updateWorkItemMutation from '../../graphql/update_work_item.mutation.graphql';
import createWorkItemMutation from '../../graphql/create_work_item.mutation.graphql';
@@ -90,7 +91,9 @@ export default {
},
apollo: {
workItemTypes: {
- query: projectWorkItemTypesQuery,
+ query() {
+ return this.isGroup ? groupWorkItemTypesQuery : projectWorkItemTypesQuery;
+ },
variables() {
return {
fullPath: this.fullPath,
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone.vue b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue
index dbeb3d4d3ff..dbeb3d4d3ff 100644
--- a/app/assets/javascripts/work_items/components/work_item_milestone.vue
+++ b/app/assets/javascripts/work_items/components/work_item_milestone_inline.vue
diff --git a/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
new file mode 100644
index 00000000000..9588d21a3c5
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_milestone_with_edit.vue
@@ -0,0 +1,203 @@
+<script>
+import { GlLink } from '@gitlab/ui';
+import * as Sentry from '~/sentry/sentry_browser_wrapper';
+import Tracking from '~/tracking';
+import { s__, __ } from '~/locale';
+import { MILESTONE_STATE } from '~/sidebar/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+import WorkItemSidebarDropdownWidgetWithEdit from '~/work_items/components/shared/work_item_sidebar_dropdown_widget_with_edit.vue';
+import projectMilestonesQuery from '~/sidebar/queries/project_milestones.query.graphql';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ I18N_WORK_ITEM_ERROR_UPDATING,
+ sprintfWorkItem,
+ TRACKING_CATEGORY_SHOW,
+} from '../constants';
+
+export default {
+ i18n: {
+ milestone: s__('WorkItem|Milestone'),
+ none: s__('WorkItem|None'),
+ noMilestone: s__('WorkItem|No milestone'),
+ milestoneFetchError: s__(
+ 'WorkItem|Something went wrong while fetching milestones. Please try again.',
+ ),
+ expiredText: __('(expired)'),
+ },
+ components: {
+ WorkItemSidebarDropdownWidgetWithEdit,
+ GlLink,
+ },
+ mixins: [Tracking.mixin()],
+ props: {
+ fullPath: {
+ type: String,
+ required: true,
+ },
+ workItemId: {
+ type: String,
+ required: true,
+ },
+ workItemMilestone: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ workItemType: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canUpdate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ data() {
+ return {
+ searchTerm: '',
+ shouldFetch: false,
+ updateInProgress: false,
+ milestones: [],
+ localMilestone: this.workItemMilestone,
+ };
+ },
+ computed: {
+ tracking() {
+ return {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_milestone',
+ property: `type_${this.workItemType}`,
+ };
+ },
+ emptyPlaceholder() {
+ return this.canUpdate ? this.$options.i18n.noMilestone : this.$options.i18n.none;
+ },
+ expired() {
+ return this.localMilestone?.expired ? ` ${this.$options.i18n.expiredText}` : '';
+ },
+ dropdownText() {
+ return this.localMilestone?.title
+ ? `${this.localMilestone?.title}${this.expired}`
+ : this.emptyPlaceholder;
+ },
+ isLoadingMilestones() {
+ return this.$apollo.queries.milestones.loading;
+ },
+ milestonesList() {
+ return this.milestones.map(({ id, title, expired }) => ({
+ value: id,
+ text: title,
+ expired,
+ }));
+ },
+ },
+ apollo: {
+ milestones: {
+ query: projectMilestonesQuery,
+ debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+ variables() {
+ return {
+ fullPath: this.fullPath,
+ title: this.searchTerm,
+ state: MILESTONE_STATE.ACTIVE,
+ first: 20,
+ };
+ },
+ skip() {
+ return !this.shouldFetch;
+ },
+ update(data) {
+ return data?.workspace?.attributes?.nodes || [];
+ },
+ error() {
+ this.$emit('error', this.i18n.milestoneFetchError);
+ },
+ },
+ },
+ methods: {
+ onDropdownShown() {
+ this.searchTerm = '';
+ this.shouldFetch = true;
+ },
+ search(searchTerm) {
+ this.searchTerm = searchTerm;
+ this.shouldFetch = true;
+ },
+ itemExpiredText(item) {
+ return item.expired ? this.$options.i18n.expiredText : '';
+ },
+ updateMilestone(selectedMilestoneId) {
+ if (this.localMilestone?.id === selectedMilestoneId) {
+ return;
+ }
+
+ this.localMilestone = selectedMilestoneId
+ ? this.milestones.find(({ id }) => id === selectedMilestoneId)
+ : null;
+
+ this.track('updated_milestone');
+ this.updateInProgress = true;
+
+ this.$apollo
+ .mutate({
+ mutation: updateWorkItemMutation,
+ variables: {
+ input: {
+ id: this.workItemId,
+ milestoneWidget: {
+ milestoneId: selectedMilestoneId,
+ },
+ },
+ },
+ })
+ .then(({ data }) => {
+ if (data.workItemUpdate.errors.length) {
+ throw new Error(data.workItemUpdate.errors.join('\n'));
+ }
+ })
+ .catch((error) => {
+ this.localMilestone = this.workItemMilestone;
+ const msg = sprintfWorkItem(I18N_WORK_ITEM_ERROR_UPDATING, this.workItemType);
+ this.$emit('error', msg);
+ Sentry.captureException(error);
+ })
+ .finally(() => {
+ this.updateInProgress = false;
+ this.searchTerm = '';
+ this.shouldFetch = false;
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <work-item-sidebar-dropdown-widget-with-edit
+ :dropdown-label="$options.i18n.milestone"
+ :can-update="canUpdate"
+ dropdown-name="milestone"
+ :loading="isLoadingMilestones"
+ :list-items="milestonesList"
+ :item-value="localMilestone"
+ :update-in-progress="updateInProgress"
+ :toggle-dropdown-text="dropdownText"
+ :header-text="__('Select milestone')"
+ :reset-button-label="__('Clear')"
+ data-testid="work-item-milestone-with-edit"
+ @dropdownShown="onDropdownShown"
+ @searchStarted="search"
+ @updateValue="updateMilestone"
+ >
+ <template #list-item="{ item }">
+ <div>{{ item.text }}{{ itemExpiredText(item) }}</div>
+ <div v-if="item.title">{{ item.title }}</div>
+ </template>
+ <template #readonly>
+ <gl-link class="gl-text-gray-900!" :href="localMilestone.webPath">
+ {{ localMilestone.title }}{{ expired }}
+ </gl-link>
+ </template>
+ </work-item-sidebar-dropdown-widget-with-edit>
+</template>
diff --git a/app/assets/javascripts/work_items/components/work_item_parent_inline.vue b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
index 0c0842a3e05..bb75de677c3 100644
--- a/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
+++ b/app/assets/javascripts/work_items/components/work_item_parent_inline.vue
@@ -108,7 +108,7 @@ export default {
types: this.parentType,
in: this.search ? 'TITLE' : undefined,
iid: null,
- isNumber: false,
+ searchByIid: false,
};
},
skip() {
diff --git a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
index c98bd6ce1e9..10c59d677f7 100644
--- a/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
+++ b/app/assets/javascripts/work_items/components/work_item_relationships/work_item_add_relationship_form.vue
@@ -172,7 +172,7 @@ export default {
relatedToLabel: s__('WorkItem|relates to'),
blockingLabel: s__('WorkItem|blocks'),
blockedByLabel: s__('WorkItem|is blocked by'),
- linkItemInputLabel: s__('WorkItem|the following item(s)'),
+ linkItemInputLabel: s__('WorkItem|the following items'),
addLinkedItemErrorMessage: s__(
'WorkItem|Something went wrong when trying to link a item. Please try again.',
),
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 69752967efe..48884be54f6 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
@@ -38,6 +38,11 @@ export default {
required: false,
default: false,
},
+ hasComment: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
},
data() {
return {
@@ -49,9 +54,15 @@ export default {
return this.workItemState === STATE_OPEN;
},
toggleWorkItemStateText() {
- const baseText = this.isWorkItemOpen
+ let baseText = this.isWorkItemOpen
? __('Close %{workItemType}')
: __('Reopen %{workItemType}');
+
+ if (this.hasComment) {
+ baseText = this.isWorkItemOpen
+ ? __('Comment & close %{workItemType}')
+ : __('Comment & reopen %{workItemType}');
+ }
return sprintfWorkItem(baseText, this.workItemType);
},
tracking() {
@@ -96,6 +107,10 @@ export default {
Sentry.captureException(error);
}
+ if (this.hasComment) {
+ this.$emit('submit-comment');
+ }
+
this.updateInProgress = false;
},
},
diff --git a/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue
new file mode 100644
index 00000000000..6af564a6a91
--- /dev/null
+++ b/app/assets/javascripts/work_items/components/work_item_title_with_edit.vue
@@ -0,0 +1,45 @@
+<script>
+import { GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { __ } from '~/locale';
+
+export default {
+ components: {
+ GlFormGroup,
+ GlFormInput,
+ },
+ i18n: {
+ titleLabel: __('Title (required)'),
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ isEditing: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-form-group v-if="isEditing" :label="$options.i18n.titleLabel" label-for="work-item-title">
+ <gl-form-input
+ class="gl-w-full"
+ :value="title"
+ data-testid="work-item-title-with-edit"
+ @keydown.meta.enter="$emit('updateWorkItem')"
+ @keydown.ctrl.enter="$emit('updateWorkItem')"
+ @input="$emit('updateDraft', $event)"
+ />
+ </gl-form-group>
+ <h1
+ v-else
+ data-testid="work-item-title"
+ class="gl-w-full gl-font-weight-normal gl-sm-font-weight-bold gl-mb-1 gl-mt-0 gl-font-size-h-display"
+ >
+ {{ title }}
+ </h1>
+</template>
diff --git a/app/assets/javascripts/work_items/constants.js b/app/assets/javascripts/work_items/constants.js
index 41cf5d8932d..62fdc8a21c2 100644
--- a/app/assets/javascripts/work_items/constants.js
+++ b/app/assets/javascripts/work_items/constants.js
@@ -46,6 +46,9 @@ export const WORK_ITEM_TYPE_VALUE_REQUIREMENTS = 'Requirements';
export const WORK_ITEM_TYPE_VALUE_KEY_RESULT = 'Key Result';
export const WORK_ITEM_TYPE_VALUE_OBJECTIVE = 'Objective';
+export const NAMESPACE_GROUP = 'group';
+export const NAMESPACE_PROJECT = 'project';
+
export const WORK_ITEM_TITLE_MAX_LENGTH = 255;
export const i18n = {
@@ -91,10 +94,13 @@ export const I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR = s__(
export const I18N_WORK_ITEM_CREATE_BUTTON_LABEL = s__('WorkItem|Create %{workItemType}');
export const I18N_WORK_ITEM_ADD_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}');
export const I18N_WORK_ITEM_ADD_MULTIPLE_BUTTON_LABEL = s__('WorkItem|Add %{workItemType}s');
-export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__('WorkItem|Search existing items');
+export const I18N_WORK_ITEM_SEARCH_INPUT_PLACEHOLDER = s__(
+ 'WorkItem|Search existing items, paste URL, or enter reference ID',
+);
export const I18N_WORK_ITEM_SEARCH_ERROR = s__(
'WorkItem|Something went wrong while fetching the %{workItemType}. Please try again.',
);
+export const I18N_WORK_ITEM_NO_MATCHES_FOUND = s__('WorkItem|No matches found');
export const I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL = s__(
'WorkItem|This %{workItemType} is confidential and should only be visible to team members with at least Reporter access',
);
@@ -195,6 +201,8 @@ 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,
+ [WORK_ITEM_TYPE_VALUE_ISSUE]: WORK_ITEM_TYPE_ENUM_ISSUE,
+ [WORK_ITEM_TYPE_VALUE_EPIC]: WORK_ITEM_TYPE_ENUM_EPIC,
};
export const WORK_ITEMS_TREE_TEXT_MAP = {
@@ -208,9 +216,14 @@ export const WORK_ITEMS_TREE_TEXT_MAP = {
'WorkItem|No tasks are currently assigned. Use tasks to break down this issue into smaller parts.',
),
},
+ [WORK_ITEM_TYPE_VALUE_EPIC]: {
+ title: s__('WorkItem|Child items'),
+ empty: s__('WorkItem|No epics or issues are currently assigned.'),
+ },
};
export const WORK_ITEM_NAME_TO_ICON_MAP = {
+ Epic: 'epic',
Issue: 'issue-type-issue',
Task: 'issue-type-task',
Objective: 'issue-type-objective',
diff --git a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
index 7efd67467e5..17b338f7a8d 100644
--- a/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
+++ b/app/assets/javascripts/work_items/graphql/project_work_items.query.graphql
@@ -4,11 +4,12 @@ query projectWorkItems(
$types: [IssueType!]
$in: [IssuableSearchableField!]
$iid: String = null
- $isNumber: Boolean!
+ $searchByIid: Boolean = false
+ $searchByText: Boolean = true
) {
workspace: project(fullPath: $fullPath) {
id
- workItems(search: $searchTerm, types: $types, in: $in) {
+ workItems(search: $searchTerm, types: $types, in: $in) @include(if: $searchByText) {
nodes {
id
iid
@@ -16,7 +17,7 @@ query projectWorkItems(
confidential
}
}
- workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $isNumber) {
+ workItemsByIid: workItems(iid: $iid, types: $types) @include(if: $searchByIid) {
nodes {
id
iid
diff --git a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
index ef43b9c026d..c1ec3fe276f 100644
--- a/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
+++ b/app/assets/javascripts/work_items/graphql/work_item.fragment.graphql
@@ -12,6 +12,7 @@ fragment WorkItem on WorkItem {
createdAt
updatedAt
closedAt
+ webUrl
reference(full: true)
createNoteEmail
namespace {
diff --git a/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql b/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql
new file mode 100644
index 00000000000..1e8d62596b7
--- /dev/null
+++ b/app/assets/javascripts/work_items/graphql/work_items_by_references.query.graphql
@@ -0,0 +1,10 @@
+query getWorkItemsByReferences($contextNamespacePath: ID!, $refs: [String!]!) {
+ workItemsByReference(contextNamespacePath: $contextNamespacePath, refs: $refs) {
+ nodes {
+ id
+ iid
+ title
+ confidential
+ }
+ }
+}
diff --git a/app/assets/javascripts/work_items/utils.js b/app/assets/javascripts/work_items/utils.js
index c3c292c3dd9..6d304e7ebf0 100644
--- a/app/assets/javascripts/work_items/utils.js
+++ b/app/assets/javascripts/work_items/utils.js
@@ -55,3 +55,14 @@ export const markdownPreviewPath = (fullPath, iid) =>
`${
gon.relative_url_root || ''
}/${fullPath}/preview_markdown?target_type=WorkItem&target_id=${iid}`;
+
+export const isReference = (input) => {
+ /**
+ * The regular expression checks if the `value` is
+ * a project work item or group work item.
+ * e.g., gitlab-org/project-path#101 or gitlab-org&101
+ * or #1234
+ */
+
+ return /^([\w-]+(?:\/[\w-]+)*)?[#&](\d+)$/.test(input);
+};
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index ce8ccb2bc08..1b99a27b12c 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -13,10 +13,10 @@
@import 'page_specific_files';
// Component specific styles, will be moved to gitlab-ui
-@import 'components/**/*';
+@import 'components/index';
// Vendors specific styles
-@import 'vendors/**/*';
+@import 'vendors/index';
// Styles for JS behaviors.
@import 'behaviors';
@@ -27,7 +27,5 @@
// JH-only stylesheets
@import 'application_jh';
-/* print styles */
-@media print {
- @import 'print';
-}
+// print styles
+@import 'print';
diff --git a/app/assets/stylesheets/components/_index.scss b/app/assets/stylesheets/components/_index.scss
new file mode 100644
index 00000000000..f53837b5671
--- /dev/null
+++ b/app/assets/stylesheets/components/_index.scss
@@ -0,0 +1,11 @@
+@import './avatar';
+@import './collapsible_card';
+@import './content_editor';
+@import './deployment_instance';
+@import './detail_page';
+@import './ref_selector';
+@import './related_items_list';
+@import './severity/icons';
+@import './shortcuts_help';
+@import './upload_dropzone/upload_dropzone';
+@import './whats_new';
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index 97f2add4e77..c654eb16af5 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -35,6 +35,10 @@
background-color: transparent;
}
+ th[align] *, td[align] * {
+ text-align: inherit;
+ }
+
td,
th,
li,
@@ -149,6 +153,11 @@
padding: $gl-spacing-scale-1 $gl-spacing-scale-3 0 0;
margin: 0;
}
+
+ &[data-inapplicable] * {
+ text-decoration: line-through;
+ color: $gl-text-color-disabled;
+ }
}
}
diff --git a/app/assets/stylesheets/emoji_sprites.scss b/app/assets/stylesheets/emoji_sprites.scss
index 5a5f39a4b77..10bf54b4ffb 100644
--- a/app/assets/stylesheets/emoji_sprites.scss
+++ b/app/assets/stylesheets/emoji_sprites.scss
@@ -7176,7 +7176,7 @@
}
.emoji-icon {
- background-image: image-url('emoji.png');
+ background-image: url('emoji.png');
background-repeat: no-repeat;
color: transparent;
text-indent: -99em;
@@ -7190,7 +7190,7 @@
only screen and (min-device-pixel-ratio: 2),
only screen and (min-resolution: 192dpi),
only screen and (min-resolution: 2dppx) {
- background-image: image-url('emoji@2x.png');
+ background-image: url('emoji@2x.png');
background-size: 860px 840px;
}
/* stylelint-enable media-feature-name-no-vendor-prefix */
diff --git a/app/assets/stylesheets/fonts.scss b/app/assets/stylesheets/fonts.scss
index 6886e751b72..f776328ebdf 100644
--- a/app/assets/stylesheets/fonts.scss
+++ b/app/assets/stylesheets/fonts.scss
@@ -11,7 +11,7 @@ Usage:
font-style: normal;
/* stylelint-disable-next-line property-no-unknown */
font-named-instance: 'Regular';
- src: font-url('gitlab-sans/GitLabSans.woff2') format('woff2');
+ src: url('gitlab-sans/GitLabSans.woff2') format('woff2');
}
@font-face {
@@ -21,7 +21,7 @@ Usage:
font-style: italic;
/* stylelint-disable-next-line property-no-unknown */
font-named-instance: 'Regular';
- src: font-url('gitlab-sans/GitLabSans-Italic.woff2') format('woff2');
+ src: url('gitlab-sans/GitLabSans-Italic.woff2') format('woff2');
}
/* -------------------------------------------------------
@@ -35,7 +35,7 @@ Usage:
font-weight: 100 900;
font-display: swap;
font-style: normal;
- src: font-url('gitlab-mono/GitLabMono.woff2') format('woff2');
+ src: url('gitlab-mono/GitLabMono.woff2') format('woff2');
}
@font-face {
@@ -43,7 +43,7 @@ Usage:
font-weight: 100 900;
font-display: swap;
font-style: italic;
- src: font-url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2');
+ src: url('gitlab-mono/GitLabMono-Italic.woff2') format('woff2');
}
// This isn't the best solution, but we needed a quick fix
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 6f4f7a29334..dd4b6f51ebe 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -10,6 +10,7 @@
@import 'framework/animations';
@import 'framework/vue_transitions';
@import 'framework/blocks';
+@import 'framework/breadcrumbs';
@import 'framework/buttons';
@import 'framework/badges';
@import 'framework/calendar';
@@ -23,7 +24,9 @@
@import 'framework/gfm';
@import 'framework/kbd';
@import 'framework/header';
+@import 'framework/top_bar';
@import 'framework/highlight';
+@import 'framework/labels';
@import 'framework/lists';
@import 'framework/logo';
@import 'framework/markdown_area';
diff --git a/app/assets/stylesheets/framework/badges.scss b/app/assets/stylesheets/framework/badges.scss
index 3f1d742ca14..7c3684f7c2e 100644
--- a/app/assets/stylesheets/framework/badges.scss
+++ b/app/assets/stylesheets/framework/badges.scss
@@ -12,3 +12,56 @@
color: $green;
}
}
+
+// FF :simplified_badges
+//
+// Temporarily override badge styles
+// globally
+//
+// Once verified we will update the
+// badge component in GitLab UI
+// refactor GitLab and remove this
+// custom code
+//
+// see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3307
+.ff-simplified-badges-enabled {
+ // These changes will be moved to
+ // GitLab UI's badge component
+ .gl-badge,
+ .gl-badge.sm,
+ .gl-badge.md,
+ .gl-badge.lg {
+ @include gl-font-sm;
+ padding-block: $gl-spacing-scale-1;
+ padding-inline: calc(#{$gl-spacing-scale-3} - 2px);
+
+ > .gl-icon {
+ @include gl-ml-0;
+ }
+ }
+
+ // These changes will be moved to
+ // GitLab UI's button component
+ .gl-button .gl-badge {
+ @include gl-py-0;
+ }
+
+ // These changes will be moved to
+ // app/assets/stylesheets/framework/super_sidebar.scss
+ .super-sidebar-nav-item .gl-badge {
+ vertical-align: 2px;
+ }
+
+ // These changes will be moved to
+ // GitLab UI's tab component
+ .gl-tab-nav-item .gl-badge {
+ margin-block: -2px;
+ }
+
+ // Temporarily needed because of the
+ // speciality this FF adds
+ // the utility class gets overriden
+ .gl-badge.ci-icon {
+ @include gl-p-2;
+ }
+}
diff --git a/app/assets/stylesheets/framework/breadcrumbs.scss b/app/assets/stylesheets/framework/breadcrumbs.scss
new file mode 100644
index 00000000000..b71382f5570
--- /dev/null
+++ b/app/assets/stylesheets/framework/breadcrumbs.scss
@@ -0,0 +1,13 @@
+.breadcrumbs {
+ flex: 1;
+ min-width: 0;
+ align-self: center;
+ color: $gl-text-color-secondary;
+
+ .avatar-tile {
+ margin-right: 4px;
+ border: 1px solid $border-color;
+ border-radius: 50%;
+ vertical-align: sub;
+ }
+}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index b948a57ea33..497a8a08a6f 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -225,7 +225,7 @@ $diff-file-header: 41px;
width: 15px;
position: absolute;
top: 0;
- background: image-url('swipemode_sprites.gif') 0 3px no-repeat;
+ background: url('swipemode_sprites.gif') 0 3px no-repeat;
}
.bottom-handle {
@@ -234,7 +234,7 @@ $diff-file-header: 41px;
width: 15px;
position: absolute;
bottom: 0;
- background: image-url('swipemode_sprites.gif') 0 -11px no-repeat;
+ background: url('swipemode_sprites.gif') 0 -11px no-repeat;
}
}
}
@@ -272,7 +272,7 @@ $diff-file-header: 41px;
left: 12px;
height: 10px;
width: 276px;
- background: image-url('onion_skin_sprites.gif') -4px -20px repeat-x;
+ background: url('onion_skin_sprites.gif') -4px -20px repeat-x;
}
.dragger {
@@ -282,7 +282,7 @@ $diff-file-header: 41px;
top: 0;
height: 14px;
width: 14px;
- background: image-url('onion_skin_sprites.gif') 0 -34px repeat-x;
+ background: url('onion_skin_sprites.gif') 0 -34px repeat-x;
cursor: pointer;
}
@@ -293,7 +293,7 @@ $diff-file-header: 41px;
right: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px 0 no-repeat;
+ background: url('onion_skin_sprites.gif') -2px 0 no-repeat;
}
.opaque {
@@ -303,7 +303,7 @@ $diff-file-header: 41px;
left: 0;
height: 10px;
width: 10px;
- background: image-url('onion_skin_sprites.gif') -2px -10px no-repeat;
+ background: url('onion_skin_sprites.gif') -2px -10px no-repeat;
}
}
}
@@ -770,12 +770,12 @@ table.code {
.frame.click-to-comment,
.btn-transparent.image-diff-overlay-add-comment {
position: relative;
- cursor: image-url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ cursor: url('illustrations/image_comment_light_cursor.svg') $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
// Retina cursor
- cursor: image-set(image-url('illustrations/image_comment_light_cursor.svg') 1x,
- image-url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
+ cursor: image-set(url('illustrations/image_comment_light_cursor.svg') 1x,
+ url('illustrations/image_comment_light_cursor@2x.svg') 2x) $image-comment-cursor-left-offset $image-comment-cursor-top-offset,
auto;
.comment-indicator {
@@ -944,3 +944,12 @@ table.code {
left: -2px !important;
}
}
+
+.diff-file.pinned-file .file-title {
+ background-color: $blue-50;
+ border-color: $blue-200;
+}
+
+.diff-file.pinned-file .diff-content {
+ border-color: $blue-200;
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index e791a0dbbbd..2558ddec9b9 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -575,7 +575,7 @@
left: 1rem;
width: 1rem;
height: 1rem;
- mask-image: asset_url('icons-stacked.svg#check');
+ mask-image: url('icons-stacked.svg#check');
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center center;
@@ -806,28 +806,6 @@
}
}
-@include media-breakpoint-down(xs) {
- .navbar-gitlab {
- li.dropdown {
- position: static;
- }
- }
-
- header.navbar-gitlab .dropdown {
- .dropdown-menu {
- width: 100%;
- min-width: 100%;
- }
- }
-
- header.navbar-gitlab-new .header-content .dropdown {
- .dropdown-menu {
- left: 0;
- min-width: 100%;
- }
- }
-}
-
.dropdown-content-faded-mask {
position: relative;
@@ -959,3 +937,17 @@
width: 100%;
}
}
+
+.group-namespace-dropdown .gl-new-dropdown-custom-toggle {
+ display: flex;
+ flex: auto;
+
+ .gl-button-text {
+ display: flex;
+ @include gl-w-full;
+ }
+}
+
+.group-namespace-dropdown .gl-new-dropdown-item-text-wrapper {
+ word-break: break-word;
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 9cb264c992b..7dcde5f0b3c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -420,7 +420,7 @@ span.idiff {
@include gl-h-5;
@include gl-float-left;
background-color: $gray-400;
- mask-image: asset_url('icons-stacked.svg#doc-versions');
+ mask-image: url('icons-stacked.svg#doc-versions');
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 6b4f1478978..56667c10752 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -96,14 +96,6 @@ $notification-box-shadow-color: rgba(0, 0, 0, 0.25);
}
}
-@include media-breakpoint-down(sm) {
- ul.notes {
- .flash-container.timeline-content {
- margin-left: 0;
- }
- }
-}
-
.gl-browser-ie .flash-container {
position: fixed;
max-width: $limited-layout-width;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 23f40dfe4bf..84e69e40bc2 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -9,119 +9,6 @@
left: 0;
right: 0;
border-radius: 0;
-
- .close-icon {
- display: none;
- }
-
- .header-content {
- width: 100%;
- display: flex;
- justify-content: space-between;
- position: relative;
- min-height: var(--header-height);
- padding-left: 0;
-
- .title {
- padding-right: 0;
- color: currentColor;
- display: flex;
- position: relative;
- margin: 0;
- font-size: 18px;
- vertical-align: top;
- white-space: nowrap;
-
- img {
- height: 24px;
-
- + .logo-text {
- margin-left: 8px;
- }
- }
-
- &.wrap {
- white-space: normal;
- }
-
- &.initializing {
- opacity: 0;
- }
-
- a:not(.canary-badge) {
- display: flex;
- align-items: center;
- padding: 2px 8px;
- margin: 4px 2px 4px -8px;
- border-radius: $border-radius-default;
-
- &:active,
- &:focus {
- @include gl-focus($focus-ring: $focus-ring-dark);
- }
- }
- }
-
- .dropdown.open {
- > a {
- border-bottom-color: $white;
- }
- }
- }
-
- .container-fluid {
- padding: 0;
-
- .nav > li {
- > a {
- will-change: color;
- margin: 4px 0;
- padding: 6px 8px;
- height: 32px;
- }
- }
- }
-}
-
-.top-bar-container {
- min-height: $top-bar-height;
-}
-
-.top-bar-fixed {
- @include gl-inset-border-b-1-gray-100;
- background-color: $body-bg;
- left: var(--application-bar-left);
- position: fixed;
- right: var(--application-bar-right);
- top: $calc-application-bars-height;
- width: auto;
- z-index: $top-bar-z-index;
-
- @media (prefers-reduced-motion: no-preference) {
- transition: left $gl-transition-duration-medium, right $gl-transition-duration-medium;
- }
-}
-
-.breadcrumbs {
- flex: 1;
- min-width: 0;
- align-self: center;
- color: $gl-text-color-secondary;
-
- .avatar-tile {
- margin-right: 4px;
- border: 1px solid $border-color;
- border-radius: 50%;
- vertical-align: sub;
- }
-}
-
-.breadcrumb-item-text {
- text-decoration: inherit;
-
- @include media-breakpoint-down(xs) {
- @include str-truncated(128px);
- }
}
.navbar-empty {
@@ -173,17 +60,6 @@
@include media-breakpoint-down(xs) { margin-right: 3px; }
}
-.top-nav-menu-item {
- &.active,
- &:hover {
- background-color: $nav-active-bg !important;
- }
-
- .gl-icon {
- color: inherit !important;
- }
-}
-
.header-logged-out {
z-index: $header-zindex;
min-height: var(--header-height);
diff --git a/app/assets/stylesheets/framework/labels.scss b/app/assets/stylesheets/framework/labels.scss
new file mode 100644
index 00000000000..1933af5151c
--- /dev/null
+++ b/app/assets/stylesheets/framework/labels.scss
@@ -0,0 +1,56 @@
+// FF :simplified_labels
+//
+// Temporarily override label styles
+// globally
+//
+// Once verified we will update the
+// label component in GitLab UI
+// refactor GitLab and remove this
+// custom code
+//
+// see https://gitlab.com/gitlab-org/gitlab-ui/-/merge_requests/3307
+.ff-simplified-labels-enabled {
+ // These changes will be moved to
+ // GitLab UI's label component
+ .gl-label,
+ .gl-label-sm {
+ @include gl-vertical-align-bottom;
+
+ &:focus:active {
+ @include gl-reset-color;
+ @include gl-shadow-none;
+ @include gl-outline-none;
+ }
+
+ .gl-label-text,
+ .gl-label-text-scoped {
+ @include gl-font-sm;
+ padding-block: $gl-spacing-scale-1;
+ padding-inline: calc(#{$gl-spacing-scale-3} - 2px);
+ }
+
+ > .gl-label-close.gl-button {
+ width: px-to-rem(14px);
+ height: px-to-rem(14px);
+ margin-left: calc(#{-$gl-spacing-scale-2} - 1px);
+ margin-right: calc(#{$gl-spacing-scale-2} - 1px);
+ }
+ }
+
+ // These changes will be moved to
+ // app/assets/stylesheets/framework/sidebar.scss
+ .issuable-show-labels .gl-label {
+ margin-bottom: $gl-spacing-scale-2;
+ margin-right: $gl-spacing-scale-2;
+ }
+
+ // These changes will be moved to
+ // app/assets/stylesheets/framework/typography.scss
+ .md p > code {
+ font-size: px-to-rem(13px);
+ }
+
+ .md code {
+ @include gl-vertical-align-bottom;
+ }
+}
diff --git a/app/assets/stylesheets/framework/layout.scss b/app/assets/stylesheets/framework/layout.scss
index 7ec13c3d54c..4ef53c673f6 100644
--- a/app/assets/stylesheets/framework/layout.scss
+++ b/app/assets/stylesheets/framework/layout.scss
@@ -1,6 +1,4 @@
html {
- overflow-y: scroll;
-
&.touch .tooltip {
display: none !important;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0eecf7bddc1..04799a6b8f8 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -601,6 +601,7 @@
.gutter-toggle {
width: 100%;
height: $sidebar-toggle-height;
+ margin-top: 0;
margin-left: 0;
border-bottom: 1px solid $border-color;
border-radius: 0;
diff --git a/app/assets/stylesheets/framework/source_editor.scss b/app/assets/stylesheets/framework/source_editor.scss
index a09ab7ed64c..2b597634519 100644
--- a/app/assets/stylesheets/framework/source_editor.scss
+++ b/app/assets/stylesheets/framework/source_editor.scss
@@ -78,7 +78,7 @@
@include gl-mr-2;
@include gl-w-4;
@include gl-h-4;
- mask-image: asset_url('icons-stacked.svg#link');
+ mask-image: url('icons-stacked.svg#link');
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
diff --git a/app/assets/stylesheets/framework/super_sidebar.scss b/app/assets/stylesheets/framework/super_sidebar.scss
index 84f0612a7b4..5a9a739fb13 100644
--- a/app/assets/stylesheets/framework/super_sidebar.scss
+++ b/app/assets/stylesheets/framework/super_sidebar.scss
@@ -22,7 +22,8 @@ $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-context-header-color: inherit;
+ --super-sidebar-active-indicator-color: #{$blue-500};
--super-sidebar-notification-dot: #{$blue-500};
--super-sidebar-user-bar-bg: #{$t-gray-a-04};
@@ -42,6 +43,8 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
--super-sidebar-nav-item-current-bg: #{$t-gray-a-08};
--super-sidebar-nav-item-icon-color: #{$gray-500};
+ --super-sidebar-hr-mix-blend-mode: normal;
+
.gl-dark & {
--super-sidebar-border-color: #{$t-white-a-08};
--super-sidebar-user-bar-bg: #{$t-white-a-04};
@@ -58,7 +61,148 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
--super-sidebar-nav-item-current-bg: #{$t-white-a-08};
--super-sidebar-nav-item-icon-color: #{$gray-600};
}
+}
+
+@mixin super-sidebar-theme(
+ $background,
+ $user-bar-background,
+ $user-bar-button-color,
+ $user-bar-button-icon-color,
+ $context-header,
+ $active-indicator,
+ $notification-dot,
+) {
+ .super-sidebar {
+ --super-sidebar-bg: #{$background};
+ --super-sidebar-user-bar-bg: #{$user-bar-background};
+ --super-sidebar-context-header-color: #{$context-header};
+ --super-sidebar-active-indicator-color: #{$active-indicator};
+ --super-sidebar-notification-dot: #{$notification-dot};
+
+ --super-sidebar-user-bar-button-bg: #{$t-white-a-16};
+ --super-sidebar-user-bar-button-color: #{$user-bar-button-color};
+ --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: #{$user-bar-button-icon-color};
+ --super-sidebar-user-bar-button-icon-hover-color: #{$user-bar-button-icon-color};
+ --super-sidebar-user-bar-button-icon-mix-blend-mode: screen;
+
+ --super-sidebar-hr-mix-blend-mode: multiply;
+ }
+}
+
+.ui-blue {
+ @include super-sidebar-theme(
+ $background: $theme-blue-10,
+ $user-bar-background: $theme-blue-900,
+ $user-bar-button-color: $theme-blue-50,
+ $user-bar-button-icon-color: $theme-blue-100,
+ $context-header: $theme-blue-900,
+ $active-indicator: $theme-blue-900,
+ $notification-dot: $theme-blue-900,
+ );
+}
+
+.ui-gray {
+ @include super-sidebar-theme(
+ $background: $gray-10,
+ $user-bar-background: $gray-900,
+ $user-bar-button-color: $gray-50,
+ $user-bar-button-icon-color: $gray-100,
+ $context-header: $gray-900,
+ $active-indicator: $gray-900,
+ $notification-dot: $gray-900,
+ );
+}
+
+.ui-green {
+ @include super-sidebar-theme(
+ $background: $theme-green-10,
+ $user-bar-background: $theme-green-900,
+ $user-bar-button-color: $theme-green-50,
+ $user-bar-button-icon-color: $theme-green-100,
+ $context-header: $theme-green-900,
+ $active-indicator: $theme-green-900,
+ $notification-dot: $theme-green-900,
+ );
+}
+
+.ui-indigo {
+ @include super-sidebar-theme(
+ $background: $theme-indigo-10,
+ $user-bar-background: $theme-indigo-900,
+ $user-bar-button-color: $theme-indigo-50,
+ $user-bar-button-icon-color: $theme-indigo-100,
+ $context-header: $theme-indigo-900,
+ $active-indicator: $theme-indigo-900,
+ $notification-dot: $theme-indigo-900,
+ );
+}
+
+.ui-light-blue {
+ @include super-sidebar-theme(
+ $background: $theme-light-blue-10,
+ $user-bar-background: $theme-light-blue-700,
+ $user-bar-button-color: $theme-light-blue-50,
+ $user-bar-button-icon-color: $theme-light-blue-100,
+ $context-header: $theme-light-blue-900,
+ $active-indicator: $theme-light-blue-900,
+ $notification-dot: $theme-light-blue-900,
+ );
+}
+
+.ui-light-green {
+ @include super-sidebar-theme(
+ $background: $theme-green-10,
+ $user-bar-background: $theme-green-700,
+ $user-bar-button-color: $theme-green-50,
+ $user-bar-button-icon-color: $theme-green-100,
+ $context-header: $theme-green-900,
+ $active-indicator: $theme-green-900,
+ $notification-dot: $theme-green-900,
+ );
+}
+
+.ui-light-indigo {
+ @include super-sidebar-theme(
+ $background: $theme-indigo-10,
+ $user-bar-background: $theme-indigo-700,
+ $user-bar-button-color: $theme-indigo-50,
+ $user-bar-button-icon-color: $theme-indigo-100,
+ $context-header: $theme-indigo-900,
+ $active-indicator: $theme-indigo-900,
+ $notification-dot: $theme-indigo-900,
+ );
+}
+
+.ui-light-red {
+ @include super-sidebar-theme(
+ $background: $theme-light-red-10,
+ $user-bar-background: $theme-light-red-700,
+ $user-bar-button-color: $theme-light-red-50,
+ $user-bar-button-icon-color: $theme-light-red-100,
+ $context-header: $theme-light-red-900,
+ $active-indicator: $theme-light-red-900,
+ $notification-dot: $theme-light-red-900,
+ );
+}
+.ui-red {
+ @include super-sidebar-theme(
+ $background: $theme-red-10,
+ $user-bar-background: $theme-red-900,
+ $user-bar-button-color: $theme-red-50,
+ $user-bar-button-icon-color: $theme-red-100,
+ $context-header: $theme-red-900,
+ $active-indicator: $theme-red-900,
+ $notification-dot: $theme-red-900,
+ );
+}
+
+.super-sidebar {
display: flex;
flex-direction: column;
position: fixed;
@@ -167,8 +311,12 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
color: var(--super-sidebar-nav-item-icon-color);
}
+ hr {
+ mix-blend-mode: var(--super-sidebar-hr-mix-blend-mode);
+ }
+
.active-indicator {
- background-color: var(--super-sidebar-primary);
+ background-color: var(--super-sidebar-active-indicator-color);
}
.btn-with-notification {
@@ -200,6 +348,10 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.super-sidebar-context-header {
+ color: var(--super-sidebar-context-header-color);
+}
+
.super-sidebar-overlay {
display: none;
}
@@ -408,6 +560,13 @@ $super-sidebar-transition-hint-duration: $super-sidebar-transition-duration / 4;
}
}
+.super-sidebar-empty-pinned-text {
+ mix-blend-mode: multiply;
+
+ .gl-dark & {
+ mix-blend-mode: screen;
+ }
+}
// Styles for the ScrollScrim component.
// Should eventually be moved to gitlab-ui.
@@ -461,3 +620,17 @@ $scroll-scrim-height: 2.25rem;
opacity: 1;
}
}
+
+// Tweaks to the styles for the ScrollScrim component above (line 418)
+// are leaking into the collapsible list box dropdowns
+// https://gitlab.com/gitlab-org/gitlab/-/issues/435538
+
+.gl-new-dropdown {
+ .top-scrim-wrapper {
+ margin-bottom: 0;
+ }
+
+ .bottom-scrim-wrapper {
+ margin-top: 0;
+ }
+}
diff --git a/app/assets/stylesheets/framework/top_bar.scss b/app/assets/stylesheets/framework/top_bar.scss
new file mode 100644
index 00000000000..d4b36b82584
--- /dev/null
+++ b/app/assets/stylesheets/framework/top_bar.scss
@@ -0,0 +1,20 @@
+.top-bar-container {
+ min-height: $top-bar-height;
+}
+
+.top-bar-fixed {
+ @include gl-inset-border-b-1-gray-100;
+ background-color: $body-bg;
+ position: fixed;
+ left: var(--application-bar-left);
+ right: var(--application-bar-right);
+ top: $calc-application-bars-height;
+ width: calc(100% - var(--application-bar-left));
+ z-index: $top-bar-z-index;
+
+ @media (prefers-reduced-motion: no-preference) {
+ transition: left $gl-transition-duration-medium,
+ right $gl-transition-duration-medium,
+ width $gl-transition-duration-medium;
+ }
+}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index eefdbda8f4f..15e794fc347 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -495,7 +495,7 @@
&::after {
@include gl-dark-invert-keep-hue;
- content: image-url('icon_anchor.svg');
+ content: url('icon_anchor.svg');
visibility: hidden;
}
}
@@ -602,6 +602,20 @@
}
@include email-code-block;
+
+ &.gl-text-secondary {
+ color: $gl-text-color-secondary;
+
+ p,
+ h1,
+ h2,
+ h3,
+ h4,
+ h5,
+ table:not(.code) {
+ color: $gl-text-color-secondary;
+ }
+ }
}
/**
diff --git a/app/assets/stylesheets/highlight/common.scss b/app/assets/stylesheets/highlight/common.scss
index 23fa1326881..3fd72904655 100644
--- a/app/assets/stylesheets/highlight/common.scss
+++ b/app/assets/stylesheets/highlight/common.scss
@@ -118,7 +118,7 @@
@include gl-w-5;
@include gl-h-5;
background-color: rgba($color, 0.3);
- mask-image: asset_url('icons-stacked.svg##{$icon}');
+ mask-image: url('icons-stacked.svg##{$icon}');
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
diff --git a/app/assets/stylesheets/page_bundles/issuable.scss b/app/assets/stylesheets/page_bundles/issuable.scss
index 8b353b42f58..05563f8e314 100644
--- a/app/assets/stylesheets/page_bundles/issuable.scss
+++ b/app/assets/stylesheets/page_bundles/issuable.scss
@@ -105,8 +105,3 @@
@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/issuable_list.scss b/app/assets/stylesheets/page_bundles/issuable_list.scss
index 1ca0c5e7ce6..9084bffa951 100644
--- a/app/assets/stylesheets/page_bundles/issuable_list.scss
+++ b/app/assets/stylesheets/page_bundles/issuable_list.scss
@@ -90,12 +90,19 @@
.issuable-list li,
.issuable-info-container .controls {
.avatar-counter {
- display: inline-block;
- vertical-align: middle;
- min-width: 16px;
+ @include gl-pl-1
+ @include gl-pr-2;
+ @include gl-h-5;
+ @include gl-min-w-5;
line-height: 14px;
- height: 16px;
- padding-left: 2px;
- padding-right: 2px;
+ }
+}
+
+.merge-request {
+ .issuable-info-container .controls {
+ .avatar-counter {
+ @include gl-line-height-normal;
+ border: 0;
+ }
}
}
diff --git a/app/assets/stylesheets/page_bundles/labels.scss b/app/assets/stylesheets/page_bundles/labels.scss
index bc0bf4bc490..3204e678986 100644
--- a/app/assets/stylesheets/page_bundles/labels.scss
+++ b/app/assets/stylesheets/page_bundles/labels.scss
@@ -1,54 +1,5 @@
@import 'mixins_and_variables_and_functions';
-.suggest-colors {
- padding-top: 3px;
-
- a {
- border-radius: 4px;
- width: 30px;
- height: 30px;
- display: inline-block;
- margin-right: 10px;
- margin-bottom: 10px;
- text-decoration: none;
-
- &:focus,
- &:focus:active {
- position: relative;
- z-index: 1;
- @include gl-focus;
- }
- }
-
- &.suggest-colors-dropdown {
- margin-top: 10px;
- margin-bottom: 10px;
-
- a {
- border-radius: 0;
- width: (100% / 7);
- margin-right: 0;
- margin-bottom: -5px;
-
- &:first-of-type {
- border-top-left-radius: $gl-border-radius-base;
- }
-
- &:nth-of-type(7) {
- border-top-right-radius: $gl-border-radius-base;
- }
-
- &:nth-last-child(7) {
- border-bottom-left-radius: $gl-border-radius-base;
- }
-
- &:last-of-type {
- border-bottom-right-radius: $gl-border-radius-base;
- }
- }
- }
-}
-
.labels-select-contents-create {
.dropdown-input {
margin-bottom: 4px;
diff --git a/app/assets/stylesheets/page_bundles/login.scss b/app/assets/stylesheets/page_bundles/login.scss
index b63f199f7b9..11582ff72f0 100644
--- a/app/assets/stylesheets/page_bundles/login.scss
+++ b/app/assets/stylesheets/page_bundles/login.scss
@@ -16,43 +16,8 @@
top: 8px;
}
- .brand-holder {
- font-size: 18px;
- line-height: 1.5;
-
- p {
- font-size: 16px;
- color: $login-brand-holder-color;
- }
-
- h3 {
- font-size: 22px;
- }
-
- img {
- max-width: 100%;
- margin-bottom: 30px;
- }
-
- a {
- font-weight: $gl-font-weight-bold;
- }
- }
-
- p {
- font-size: 13px;
- }
-
- .signin-text {
- p {
- margin-bottom: 0;
- line-height: 1.5;
- }
- }
-
.borderless {
- .login-box,
- .omniauth-container {
+ .login-box {
box-shadow: none;
}
}
@@ -64,67 +29,6 @@
}
}
- .login-box,
- .omniauth-container {
- box-shadow: 0 0 0 1px $border-color;
- border-radius: $border-radius;
-
- .login-heading h3 {
- font-weight: $gl-font-weight-normal;
- line-height: 1.5;
- margin: 0 0 10px;
- }
-
- .login-footer {
- margin-top: 10px;
-
- p:last-child {
- margin-bottom: 0;
- }
- }
-
- a.forgot {
- float: right;
- padding-top: 6px;
- }
-
- .nav .active a {
- background: transparent;
- }
-
- // Styles the glowing border of focused input for username async validation
- .login-body {
- font-size: 13px;
-
- .username .validation-success {
- color: $green-600;
- }
-
- .username .validation-error {
- color: $red-500;
- }
-
- .terms .gl-form-checkbox {
- @include gl-reset-font-size;
- }
- }
- }
-
- .omniauth-container {
- border-radius: $border-radius;
- font-size: 13px;
-
- p {
- margin: 0;
- }
-
- form {
- padding: 0;
- border: 0;
- background: none;
- }
- }
-
.new-session-tabs {
&.nav-links-unboxed {
border-color: transparent;
@@ -143,30 +47,6 @@
border-top-right-radius: $border-radius-default;
border-top-left-radius: $border-radius-default;
- // Ldap configurations may need more tabs & the tab labels are user generated (arbitrarily long).
- // These styles prevent this from breaking the layout, and only applied when providers are configured.
- &.custom-provider-tabs {
- flex-wrap: wrap;
-
- li {
- min-width: 85px;
- flex-basis: auto;
-
- // This styles tab elements that have wrapped to a second line. We cannot easily predict when this will happen.
- // We are making somewhat of an assumption about the configuration here: that users do not have more than
- // 3 LDAP servers configured (in addition to standard login) and they are not using especially long names for any
- // of them. If either condition is false, this will work as expected. If both are true, there may be a missing border
- // above one of the bottom row elements. If you know a better way, please implement it!
- &:nth-child(n+5) {
- border-top: 1px solid $border-color;
- }
- }
-
- a {
- font-size: 16px;
- }
- }
-
li {
flex: 1;
text-align: center;
@@ -230,11 +110,8 @@
height: 100%;
body {
- padding-top: 48px; // Remove this line when the restyle_login_page feature flag is deleted. Instead, add self-align `center` to container, and maybe a top margin.
-
&.with-system-header {
padding-top: $system-header-height;
- padding-top: calc(#{$system-header-height} + 48px); // Remove this line when the restyle_login_page feature flag is deleted
}
&.with-system-footer {
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index d112fd83ebf..b30ec4b4253 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -259,7 +259,7 @@ $tabs-holder-z-index: 250;
position: sticky;
top: calc(#{$calc-application-header-height} + #{$mr-tabs-height} + #{$diff-file-header-top});
// height calc is fully delegated to the tree_list_height.vue component
- height: 0;
+ height: 100%;
min-height: 300px;
.drag-handle {
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 9bab5d65b59..c729bd7a380 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -130,6 +130,11 @@
.gl-pipeline-job-width {
width: 100%;
+ max-width: 400px;
+
+ .pipeline-graph-container & {
+ max-width: unset;
+ }
}
.gl-pipeline-job-width\! {
@@ -318,3 +323,13 @@
background-color: $gray-100;
}
}
+
+.scan-reports-summary-grid {
+ grid-template-columns: 1fr 1fr max-content;
+}
+
+@media (max-width: $breakpoint-sm) {
+ .scan-reports-summary-grid :nth-child(3n+1) {
+ grid-column: 1 / -1;
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/profile.scss b/app/assets/stylesheets/page_bundles/profile.scss
index 9a8eeb9c9d6..912f0145bf1 100644
--- a/app/assets/stylesheets/page_bundles/profile.scss
+++ b/app/assets/stylesheets/page_bundles/profile.scss
@@ -164,12 +164,6 @@
.user-profile {
.profile-header {
- margin: 0 $gl-padding;
-
- &.with-no-profile-tabs {
- margin-bottom: $gl-padding-24;
- }
-
.avatar-holder {
margin: 0 auto 10px;
}
diff --git a/app/assets/stylesheets/page_bundles/project.scss b/app/assets/stylesheets/page_bundles/project.scss
index c2ecf3702f9..bd24d991c8d 100644
--- a/app/assets/stylesheets/page_bundles/project.scss
+++ b/app/assets/stylesheets/page_bundles/project.scss
@@ -189,10 +189,6 @@
.project-page-sidebar-block {
width: $right-sidebar-width - 1px;
-
- &:first-of-type {
- padding-top: $gl-spacing-scale-1;
- }
}
.nav {
diff --git a/app/assets/stylesheets/page_bundles/search.scss b/app/assets/stylesheets/page_bundles/search.scss
index b145d046fa4..87d0d5b91d3 100644
--- a/app/assets/stylesheets/page_bundles/search.scss
+++ b/app/assets/stylesheets/page_bundles/search.scss
@@ -281,14 +281,6 @@ $language-filter-max-height: 20rem;
margin-right: 5px;
}
}
-
- .dropdown-menu-toggle,
- .gl-dropdown {
- @include media-breakpoint-up(sm) {
- width: 180px;
- margin-top: 0;
- }
- }
}
.search-page-form {
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index ed2c7662a98..6d85a4da035 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -101,7 +101,7 @@
}
.active > .wiki-list {
- background-color: $gray-50;
+ background-color: var(--gray-50, $gray-50);
}
.wiki-list {
@@ -110,7 +110,7 @@
@include gl-rounded-base;
&:hover {
- background: $gray-50;
+ background: var(--gray-50, $gray-50);
.wiki-list-create-child-button {
display: block;
@@ -150,10 +150,6 @@
.wiki-sidebar-header {
padding: 0 $gl-padding $gl-padding;
-
- .gutter-toggle {
- margin-top: 0;
- }
}
}
diff --git a/app/assets/stylesheets/page_bundles/work_items.scss b/app/assets/stylesheets/page_bundles/work_items.scss
index b9ab2450ff9..5b354f3575c 100644
--- a/app/assets/stylesheets/page_bundles/work_items.scss
+++ b/app/assets/stylesheets/page_bundles/work_items.scss
@@ -4,6 +4,7 @@
$work-item-field-inset-shadow: inset 0 0 0 $gl-border-size-1 var(--gray-200, $gray-200) !important;
$work-item-overview-right-sidebar-width: 23rem;
$work-item-sticky-header-height: 52px;
+$work-item-overview-gap-width: 2rem;
.gl-token-selector-token-container {
display: flex;
@@ -146,7 +147,7 @@ $work-item-sticky-header-height: 52px;
@include media-breakpoint-up(md) {
display: grid;
grid-template-columns: 1fr $work-item-overview-right-sidebar-width;
- gap: 2rem;
+ gap: $work-item-overview-gap-width;
}
}
@@ -216,6 +217,12 @@ $work-item-sticky-header-height: 52px;
}
}
+.editable-wi-title {
+ width: 100%;
+ @include media-breakpoint-up(md) {
+ width: calc(100% - #{$work-item-overview-right-sidebar-width} - #{$work-item-overview-gap-width});
+ }
+}
// Disclosure hierarchy component, used for Ancestors widget
$disclosure-hierarchy-chevron-dimension: 1.2rem;
diff --git a/app/assets/stylesheets/pages/colors.scss b/app/assets/stylesheets/pages/colors.scss
index 85c1f7da07f..aeaf2d7c1b3 100644
--- a/app/assets/stylesheets/pages/colors.scss
+++ b/app/assets/stylesheets/pages/colors.scss
@@ -29,3 +29,52 @@
.danger-title {
color: var(--red-500, $red-500);
}
+
+.suggest-colors {
+ padding-top: 3px;
+
+ a {
+ border-radius: 4px;
+ width: 30px;
+ height: 30px;
+ display: inline-block;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ text-decoration: none;
+
+ &:focus,
+ &:focus:active {
+ position: relative;
+ z-index: 1;
+ @include gl-focus;
+ }
+ }
+
+ &.suggest-colors-dropdown {
+ margin-top: 10px;
+ margin-bottom: 10px;
+
+ a {
+ border-radius: 0;
+ width: (100% / 7);
+ margin-right: 0;
+ margin-bottom: -5px;
+
+ &:first-of-type {
+ border-top-left-radius: $gl-border-radius-base;
+ }
+
+ &:nth-of-type(7) {
+ border-top-right-radius: $gl-border-radius-base;
+ }
+
+ &:nth-last-child(7) {
+ border-bottom-left-radius: $gl-border-radius-base;
+ }
+
+ &:last-of-type {
+ border-bottom-right-radius: $gl-border-radius-base;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 9748983d1ae..f57a8519992 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -225,7 +225,7 @@ ul.related-merge-requests > li gl-emoji {
&::after {
@include gl-dark-invert-keep-hue;
- content: image-url('icon_anchor.svg');
+ content: url('icon_anchor.svg');
visibility: hidden;
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8792c7f9a72..da03726fa64 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -88,18 +88,18 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
margin-top: 5px;
}
- .timeline-content:not(.flash-container) {
+ .timeline-content {
margin-left: 2.5rem;
border: 1px solid $border-color;
border-radius: $gl-border-radius-base;
padding: $gl-padding-4 $gl-padding-8;
}
- &:not(.target) .timeline-content:not(.flash-container) {
+ &:not(.target) .timeline-content {
background-color: $white;
}
- &.draft-note .timeline-content:not(.flash-container) {
+ &.draft-note .timeline-content {
border: 0;
}
@@ -127,7 +127,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
margin-top: 5px;
}
- .timeline-content:not(.flash-container) {
+ .timeline-content {
margin-left: 2.5rem;
border-left: 1px solid $border-color;
border-right: 1px solid $border-color;
@@ -138,11 +138,11 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
}
- &:not(.target) .timeline-content:not(.flash-container) {
+ &:not(.target) .timeline-content {
background-color: $white;
}
- &.draft-note .timeline-content:not(.flash-container) {
+ &.draft-note .timeline-content {
margin-left: 0;
border-top-left-radius: 0;
border-top-right-radius: 0;
@@ -154,7 +154,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
border-right: 1px solid $border-color;
background-color: $white;
- .timeline-content:not(.flash-container) {
+ .timeline-content {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 18px;
}
@@ -419,7 +419,6 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.system-note-commit-list-toggler {
color: $blue-600;
- padding: 10px 0 0;
cursor: pointer;
position: relative;
z-index: 2;
@@ -966,7 +965,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
.unified-diff-components-diff-note-button {
&::before {
background-color: $blue-500;
- mask-image: asset_url('icons-stacked.svg#comment');
+ mask-image: url('icons-stacked.svg#comment');
mask-repeat: no-repeat;
mask-size: cover;
mask-position: center;
@@ -1057,7 +1056,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
padding-left: 0;
ul.notes li.note-wrapper {
- .timeline-content:not(.flash-container) {
+ .timeline-content {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
@@ -1106,7 +1105,7 @@ $system-note-icon-m-left: $avatar-m-left + $icon-size-diff / $avatar-m-ratio;
}
.draft-note-component.draft-note.timeline-entry {
- .timeline-content:not(.flash-container) {
+ .timeline-content {
padding: $gl-padding-8 $gl-padding-8 $gl-padding-8 $gl-padding;
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 315b9c829a7..d6fcfb3461d 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -4,97 +4,99 @@
@import '@gitlab/ui/src/scss/variables';
@import '@gitlab/ui/src/scss/utility-mixins/index';
-.md h1,
-.md h2,
-.md h3,
-.md h4,
-.md h5,
-.md h6 {
- margin-top: 17px;
-}
+@media print {
+ .md h1,
+ .md h2,
+ .md h3,
+ .md h4,
+ .md h5,
+ .md h6 {
+ margin-top: 17px;
+ }
-.md h1 {
- font-size: 30px;
-}
+ .md h1 {
+ font-size: 30px;
+ }
-.md h2 {
- font-size: 22px;
-}
+ .md h2 {
+ font-size: 22px;
+ }
-.md h3 {
- font-size: 18px;
- font-weight: 600;
-}
+ .md h3 {
+ font-size: 18px;
+ font-weight: 600;
+ }
-.md {
- print-color-adjust: exact;
- -webkit-print-color-adjust: exact;
+ .md {
+ print-color-adjust: exact;
+ -webkit-print-color-adjust: exact;
- // fix blockquote style in print
- blockquote {
- &::before {
- position: absolute;
- top: 0;
- left: -4px;
- content: ' ';
- height: 100%;
- width: 4px;
- background-color: $gray-100;
- }
+ // fix blockquote style in print
+ blockquote {
+ &::before {
+ position: absolute;
+ top: 0;
+ left: -4px;
+ content: ' ';
+ height: 100%;
+ width: 4px;
+ background-color: $gray-100;
+ }
- position: relative;
- font-size: inherit;
- @include gl-text-gray-700;
- @include gl-py-3;
- @include gl-pl-6;
- @include gl-my-3;
- @include gl-mx-0;
- @include gl-inset-border-l-4-gray-100;
- margin-left: 4px;
- border: 0 !important;
+ position: relative;
+ font-size: inherit;
+ @include gl-text-gray-700;
+ @include gl-py-3;
+ @include gl-pl-6;
+ @include gl-my-3;
+ @include gl-mx-0;
+ @include gl-inset-border-l-4-gray-100;
+ margin-left: 4px;
+ border: 0 !important;
+ }
}
-}
-header,
-nav,
-.nav-sidebar,
-.super-sidebar,
-.profiler-results,
-.tree-ref-holder,
-.tree-holder .breadcrumb,
-.nav,
-.btn,
-ul.notes-form,
-.issuable-gutter-toggle,
-.gutter-toggle,
-.issuable-details .content-block-small,
-.edit-link,
-.note-action-button,
-.right-sidebar,
-.flash-container,
-copy-code,
-#js-peek {
- display: none !important;
-}
+ header,
+ nav,
+ .nav-sidebar,
+ .super-sidebar,
+ .profiler-results,
+ .tree-ref-holder,
+ .tree-holder .breadcrumb,
+ .nav,
+ .btn,
+ ul.notes-form,
+ .issuable-gutter-toggle,
+ .gutter-toggle,
+ .issuable-details .content-block-small,
+ .edit-link,
+ .note-action-button,
+ .right-sidebar,
+ .flash-container,
+ copy-code,
+ #js-peek {
+ display: none !important;
+ }
-pre {
- page-break-before: avoid;
- page-break-inside: auto;
-}
+ pre {
+ page-break-before: avoid;
+ page-break-inside: auto;
+ }
-.page-gutter {
- padding-top: 0;
- padding-left: 0;
-}
+ .page-gutter {
+ padding-top: 0;
+ padding-left: 0;
+ }
-.right-sidebar {
- top: 0;
-}
+ .right-sidebar {
+ top: 0;
+ }
-a[href]::after {
- content: none !important;
-}
+ a[href]::after {
+ content: none !important;
+ }
-.with-performance-bar .layout-page {
- padding-top: 0;
+ .with-performance-bar .layout-page {
+ padding-top: 0;
+ }
}
diff --git a/app/assets/stylesheets/snippets.scss b/app/assets/stylesheets/snippets.scss
index 91b381462be..e1b14df683e 100644
--- a/app/assets/stylesheets/snippets.scss
+++ b/app/assets/stylesheets/snippets.scss
@@ -15,8 +15,7 @@
.gl-snippet-icon {
display: inline-block;
- /* stylelint-disable-next-line function-url-quotes */
- background: url(asset_path('ext_snippet_icons/ext_snippet_icons.png')) no-repeat;
+ background: url('ext_snippet_icons/ext_snippet_icons.png') no-repeat;
overflow: hidden;
text-align: left;
width: 16px;
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
index 3ab3e195b06..59c2391d2e9 100644
--- a/app/assets/stylesheets/themes/dark_mode_overrides.scss
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -1,7 +1,6 @@
@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;
diff --git a/app/assets/stylesheets/themes/theme_blue.scss b/app/assets/stylesheets/themes/theme_blue.scss
deleted file mode 100644
index 1a373fbfeda..00000000000
--- a/app/assets/stylesheets/themes/theme_blue.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-blue {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-blue-50,
- $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
deleted file mode 100644
index 9a24142f286..00000000000
--- a/app/assets/stylesheets/themes/theme_gray.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-gray {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $gray-50,
- $gray-100,
- $gray-900,
- $gray-900,
- );
- }
- }
-}
diff --git a/app/assets/stylesheets/themes/theme_green.scss b/app/assets/stylesheets/themes/theme_green.scss
deleted file mode 100644
index a766fdddc78..00000000000
--- a/app/assets/stylesheets/themes/theme_green.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-green {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-green-50,
- $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
deleted file mode 100644
index c94a32891f6..00000000000
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ /dev/null
@@ -1,36 +0,0 @@
-@import '../page_bundles/mixins_and_variables_and_functions';
-/**
- * Styles the GitLab application with a specific color theme
- */
-@mixin gitlab-theme-super-sidebar(
- $theme-color-lightest,
- $theme-color-light,
- $theme-color,
- $theme-color-darkest,
-) {
- .super-sidebar {
- --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;
- }
-
- .super-sidebar-context-header {
- color: var(--super-sidebar-primary);
- }
- }
-}
diff --git a/app/assets/stylesheets/themes/theme_indigo.scss b/app/assets/stylesheets/themes/theme_indigo.scss
deleted file mode 100644
index d0a8d597b59..00000000000
--- a/app/assets/stylesheets/themes/theme_indigo.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-indigo {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-indigo-50,
- $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
deleted file mode 100644
index e712b6ae859..00000000000
--- a/app/assets/stylesheets/themes/theme_light_blue.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-light-blue {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-light-blue-50,
- $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
deleted file mode 100644
index 5cb9bee37b0..00000000000
--- a/app/assets/stylesheets/themes/theme_light_gray.scss
+++ /dev/null
@@ -1,2 +0,0 @@
-// "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
deleted file mode 100644
index 44e19b02e36..00000000000
--- a/app/assets/stylesheets/themes/theme_light_green.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-light-green {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-green-50,
- $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
deleted file mode 100644
index ab299ca9d84..00000000000
--- a/app/assets/stylesheets/themes/theme_light_indigo.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-light-indigo {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-indigo-50,
- $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
deleted file mode 100644
index 499cdace772..00000000000
--- a/app/assets/stylesheets/themes/theme_light_red.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-light-red {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-light-red-50,
- $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
deleted file mode 100644
index 9a17f98aa80..00000000000
--- a/app/assets/stylesheets/themes/theme_red.scss
+++ /dev/null
@@ -1,14 +0,0 @@
-@import './theme_helper';
-
-:root {
- &.ui-red {
- .page-with-super-sidebar {
- @include gitlab-theme-super-sidebar(
- $theme-red-50,
- $theme-red-100,
- $theme-red-900,
- $theme-red-900,
- );
- }
- }
-}
diff --git a/app/assets/stylesheets/utilities.scss b/app/assets/stylesheets/utilities.scss
index 79ea8d3cc70..7ae17f4c191 100644
--- a/app/assets/stylesheets/utilities.scss
+++ b/app/assets/stylesheets/utilities.scss
@@ -136,3 +136,13 @@
.gl-last-of-type-border-b-0:last-of-type {
@include gl-border-b-0;
}
+
+.gl-md-h-9 {
+ @include gl-media-breakpoint-up(md) {
+ height: $gl-spacing-scale-9;
+ }
+}
+
+.gl-pl-12 {
+ padding-left: $gl-spacing-scale-12;
+}
diff --git a/app/assets/stylesheets/vendors/_index.scss b/app/assets/stylesheets/vendors/_index.scss
new file mode 100644
index 00000000000..e26ba23d1b9
--- /dev/null
+++ b/app/assets/stylesheets/vendors/_index.scss
@@ -0,0 +1 @@
+@import './atwho';